Compare commits

..

32 Commits

Author SHA1 Message Date
github-actions
9643ca35a3 GitHub bot: changelog [nobuild] 2026-09-11 09:46:01 +00:00
Alexandre
ce2d729e1b Update config.yaml 2026-09-11 11:37:14 +02:00
github-actions
8ce92d80e2 GitHub bot: changelog [nobuild] 2026-09-11 08:53:41 +00:00
Alexandre
d247ed88b4 Update CHANGELOG.md 2026-09-11 10:52:50 +02:00
Alexandre
c9b5c54545 Document breaking change in CHANGELOG
Added a breaking change note for admin ingress visibility.
2026-09-11 10:52:46 +02:00
Alexandre
07c6d1ca5d Document breaking change in CHANGELOG.md
Added a note about a breaking change for admins.
2026-09-11 10:52:21 +02:00
Alexandre
972f2c02b8 nobuild 2026-09-11 10:51:47 +02:00
Alexandre
58949cb09a nobuild 2026-09-11 10:51:44 +02:00
Alexandre
8043104f30 Update config.yaml 2026-09-11 10:43:26 +02:00
Alexandre
ced15122d9 Migrate legacy addon_config maps to app_config (#2937) nobuild
* Add one-shot app_config linter migration workflow

* Add app_config linter compatibility action

* Add add-on map compatibility normalizer

* Test add-on map compatibility normalizer

* Run app_config compatibility preparation on pull request

* Validate app_config short and long map forms separately

* Export validated workflow updates

* Use app_config-compatible linter for PR checks

* Use app_config-compatible linter for builds

* Remove temporary linter preparation workflow

* Add one-shot app_config manifest migration

* Migrate legacy addon_config maps to app_config

* Preserve upstream legacy aliases without deprecated literals

* Test legacy alias compatibility without deprecated literals

* Validate completed app_config migration

* Count only add-on manifests during migration validation

* Remove temporary migration validation workflow

* Document legacy alias construction

* Remove obsolete local linter compatibility script

* Remove obsolete local linter compatibility tests

* fix(pr-2937-review): address reviewer findings on app_config migration

- Remove the orphaned .github/actions/addon-linter composite action: it
  references .github/scripts/prepare_addon_lint_config.py, which was never
  added (the normalization approach was superseded by pinning
  frenck/action-addon-linter directly in #2936). Confirmed unused - no
  workflow invokes it.
- cleanuparr: HA_DATA_DIR pointed at /app_configs/cleanuparr, a path not
  mounted inside the container. cleanuparr's map is app_config:rw, which
  Supervisor mounts at /config, so data was living on the ephemeral
  container filesystem. Point it at /config instead (pre-existing bug,
  not introduced by this PR - just carried the wrong path forward).
- joplin/README.md: joplin's config.yaml still declares config:rw, not
  app_config (it was never part of the 86-manifest migration). Revert the
  doc text that incorrectly started calling it the app_config mapping.
- qbittorrent/00-folders.sh: fix a pre-existing copy/paste bug in the
  migration marker/log for the main-folder migration block - it referenced
  "openvpn" and a "qBitorrent" typo instead of qBittorrent.

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-09-11 09:30:49 +02:00
Alexandre
d60f17685e skill(hassio-addon-workflow): fix the stage-3 trap's actual mechanism (#3058)
* skill(hassio-addon-workflow): fix the stage-3 trap's actual mechanism

traps.md attributed the shipped svc-xorg failure to that service's own shebang
("starts #!/usr/bin/env bashio, not with-contenv"). Verified against
.templates/ha_entrypoint.sh: that isn't what decides it. ha_entrypoint.sh
rewrites the first line of every cont-init.d script and every services.d/s6-rc.d
run script to one shebang chosen by probing candidate_shebangs in order. The
first candidate is /command/with-contenv bashio; in add-ons that override the
base ENTRYPOINT ["/init"] (confirmed live in ente, wger, free_games_claimer),
s6 stage 1 never runs, that candidate fails, and the probe falls through to
/usr/bin/env bashio for every service in the image, regardless of what shebang
it shipped with. So no service reads stage 3 in that case, not just svc-xorg.

Surfaced independently by two agents in an unrelated eval run for PR #3057 (both
named the rewrite; neither had the conditionality right), then verified against
the script and confirmed the ENTRYPOINT-override path is real rather than
hypothetical before writing this.

No add-on touched; references/ only. markdownlint clean.

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

* skill(hassio-addon-workflow): scope the stage-3 trap to the PID-1-entrypoint path

Flagged by both Codex and Copilot, reproduced against .templates/ha_entrypoint.sh
before accepting.

The previous commit's fix still overclaimed: it said ha_entrypoint.sh rewrites
every service's shebang, universally. It doesn't. The services.d/s6-rc.d rewrite
loop is gated on `if $PID1` (line 430) — it only runs when ha_entrypoint.sh
itself is the container's PID 1, i.e. when the add-on replaces the base
ENTRYPOINT ["/init"]. Under the normal /init path (the overwhelming majority of
add-ons), ha_entrypoint.sh runs as the stage-2 hook with PID1=false, never
touches service run files, s6's own stage 1 already created
container_environment before any cont-init script ran, and a service's shipped
with-contenv shebang reads it normally — the opposite of what the previous
wording implied. cont-init.d scripts are unconditional either way
(run_one_script has no $PID1 gate), so a cont-init script's own shebang was
never informative and that half stands.

Also fixed: the previous commit named wger as a live example of the
ENTRYPOINT override. Copilot caught that wger's override is written into its
Dockerfile but commented out — confirmed (`grep '^#ENTRYPOINT' wger/Dockerfile`).
Only ente and free_games_claimer currently have it active; wger dropped from
the list.

markdownlint clean; no add-on touched.

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

---------

Co-authored-by: BirdNET-Go Addon Builder <addon-builder@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 08:05:29 +02:00
Alexandre
fc07a2d1b4 skill(hassio-addon-workflow): deduplicate, route traps.md by section, fix preflight path (#3057)
* skill(hassio-addon-workflow): deduplicate, route traps.md by section, fix preflight path

Reviewed the skill against Anthropic's skill-authoring guidance (progressive
disclosure, single ownership of each rule, explain the why rather than issue
rigid rules) with a second, independent pass from Codex (gpt-5.6-sol).

Real defect fixed: preflight.sh defaulted its repo argument to the hardcoded
/data/claude/hassio-addons, so when run from a worktree it inspected the main
checkout and reported that branch — the exact stale-checkout trap the script
exists to catch. It now defaults to `git rev-parse --show-toplevel`.

Deduplicated, one owner each:
- measurement methodology: evidence.md owns it; traps.md's Measurement section
  is gone (its rtk note moved to Environment and workspace)
- defensive-branch reachability: the standing rule owns it; step 3 points back
- subagent delegation: SKILL.md's delegation note owns it; codex-review.md points
- review-ratchet handling: SKILL.md step 6 owns it, absorbing codex-review.md's
  "this is wrong" vs "this is undefended" split
- post-merge revert mechanics: traps.md owns them; step 9 states the check only
- simplify checklist: step 5 owns the questions; simplify.md keeps the cases
- version format: CLAUDE.md owns it; traps.md keeps only what it does not say

Corrections found by review and verified against the workflows and scripts:
- Super-Linter runs on every PR (lint.yml `pull_request`), not weekly; it is
  continue-on-error at both call sites, which is the part that matters
- the three hard gates are matrixed over changed add-ons, so they skip rather
  than pass on a PR touching no top-level config.*
- `--sandbox read-only` blocks writes, not reads or command execution; Codex
  can and does fall back to fetching the repo from GitHub
- env_trace.sh's process argument is optional; pr_review.sh's reply and resolve
  take more than a PR number

Also: step 4 now routes to the traps.md section by anchor instead of asking for
the whole ~290-line file, and step 5 gains a Depth check (fix the shared
mechanism once more than one add-on hits it) adapted from the built-in
/simplify skill's altitude pass.

No new markdownlint findings; shellcheck clean; preflight.sh verified reporting
this worktree.

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

* skill(hassio-addon-workflow): make step 4's traps.md routing actually save the tokens

The routing table added in the previous commit was advisory only: a markdown
anchor cannot be loaded on its own, so an agent told to read
references/traps.md#shell-and-bashio reads the file and pays for all 18 KB.

Measured overpay per edit, against what each section actually needs:
  shell / bashio            642 B needed, 18,070 B paid  (28x)
  Dockerfile / arch       2,655 B needed, 18,070 B paid  (7x)
  base-image env option   3,070 B needed, 18,070 B paid  (6x)
"CI and review bots" is 6,302 B of that — 35% of the file — and is needed at
steps 7-8, never at step 4.

scripts/traps.sh prints one section. Scripts are run rather than read, so the
helper itself costs no prompt tokens and the table now buys what it claims.
Kept traps.md whole rather than splitting it into six files: the split would
save the same bytes but fragments a file with a working table of contents and
breaks the references/traps.md#anchor cross-references that steps 5, 7 and 9
still use.

Ambiguous or unmatched keywords list the sections and exit non-zero rather than
silently printing the wrong one. Verified: every keyword in the step 4 table
resolves to exactly one section, headings containing '/' work, and the last
section reaches EOF. shellcheck clean; no new markdownlint findings.

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

* skill(hassio-addon-workflow): fix review findings — PATH form, preflight fallback, config.json claim

All three reproduced before accepting.

Step 4's routing table used a bare `traps.sh <keyword>`, which is not on PATH:
every row failed with "traps.sh: command not found" from the repo root. Flagged
independently by Codex, Copilot and CodeRabbit. Now uses the
`bash "$SKILL/scripts/traps.sh"` form already established above it; all five
keywords verified to run and resolve to exactly one section.

preflight.sh fell back to the hardcoded /data/claude/hassio-addons when
`git rev-parse` could not answer, recreating the stale-checkout bug this commit
series set out to fix — and worse than CodeRabbit described: from a non-git
directory it reported an unrelated branch of the main checkout as though it were
the caller's own. It now refuses and asks for an explicit path (exit 1).
Explicit-argument and in-worktree behaviour unchanged, both re-verified.

Codex correctly noted the description advertised config.json while preflight.sh
and validate.sh parse only config.yaml. Exactly one add-on uses config.json
(zzz_archived_tor, archived) against 136 using config.yaml, so the proportionate
fix is Codex's own alternative — narrow the description — rather than teaching
two scripts JSON for an archived add-on. config.json dropped from the
description.

shellcheck clean; no new markdownlint findings.

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

---------

Co-authored-by: BirdNET-Go Addon Builder <addon-builder@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 22:42:55 +02:00
Alexandre
a3bba58229 fix(templates): install the stop handler before startup, not after it (#3054)
* fix(templates): install the stop handler before startup, not after it

`.templates/ha_entrypoint.sh` installed its SIGTERM/SIGINT trap at the very
end of startup, after `wait_for_supervisor` and after the whole
`/etc/cont-init.d/*` loop. 105 of the 136 add-ons here set `init: false`,
which makes this script namespace PID 1, and the kernel discards a signal
whose handler is still SIG_DFL. A stop arriving during startup was therefore
lost outright: Home Assistant waited out the grace period, the container died
on SIGKILL, and the add-on surfaced as Error.

The trap now goes in immediately after the PID1 detection block, still behind
an `if $PID1` guard so the 31 add-ons running under Docker's own init keep
their current signal behaviour. `terminate()` itself is unchanged apart from
indentation.

Measured on this file, run as PID 1 in a PID+mount namespace with a
bind-mounted `/etc/cont-init.d`:

  SIGTERM 3s into an 8s cont-init script
    before: never exits (alive >40s)   after: exits 5s later, script wrote 8/8
  SIGTERM before cont-init starts
    before: never exits (alive >40s)   after: exits 8s later, script wrote 8/8
  SIGTERM during 8 fast cont-init scripts
    before: never exits (alive >20s)   after: exits in 0s, cleanly after 6 of 8

Nothing is interrupted mid-write. Only PID 1 is signalled and bash defers a
trap to the next command boundary, so the in-flight command always reaps
first: a `dd` writing 16,384,000 bytes over ~5s took the entrypoint's SIGTERM
at 2s and still wrote all 16,384,000 bytes.

The residual is that a stop during one long-running cont-init script waits for
that script. Backgrounding the loop and killing the child would remove it, but
that truncates the child mid-write, and `01-config_yaml.sh` and
`19-json_repair.sh` both write non-atomically into the persisted `/config`.
Waiting was preferred over risking a user's config.

omni-tools is bumped so this gets a CI build and the fix reaches the add-on
the report came from; every other add-on picks it up on its next rebuild.

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

* fix(templates): make terminate() errexit-safe before it can run early

Moving the trap to the top of the script exposed a latent hazard that only
existed once the handler could fire before the application was up.

`validate_shebang()` leaves `set -e` on in the outer shell, so `terminate()`
runs under errexit -- measured: the shell flags at the cont-init stage are
`ehB`. Under errexit the first failing command in a trap handler aborts the
handler and exits with that status, skipping the child-kill loop and the
`exit 0`.

Every command in the stock body is already guarded with `|| true`, `|| echo`
or an `if`, so this was inert. But postgres_15 and postgres_17 patch this
function at build time (both `Dockerfile:77`):

  sed -i "/Termination signal received/a gosu postgres pg_ctl -D \"$PGDATA\" -m fast stop"

That command is unguarded, and it fails whenever the stop arrives before the
database is up. With the trap installed late, that never happened: postgres was
already running by the time the handler existed. With the trap installed early
the window is real -- it spans `wait_for_supervisor` (up to 30s) and the whole
cont-init chain, which for postgres_15 includes a `chown -R` over `$PGDATA`.

Replaying that exact sed against the patched entrypoint and stopping during
cont-init:

  without set +e   exit 127, handler aborted before killing any child
  with set +e      exit 0,   handler ran to completion

Exiting non-zero on a stop is what Home Assistant reports as Error, so without
this the change would have reproduced the reported symptom for those two
add-ons in a narrower window.

`set +e` states the intent the guarded body already implies, and covers the
injected line without postgres_15/17 needing an edit. They are the only two
add-ons that patch terminate(); the other six Dockerfiles that sed the
entrypoint touch other regions.

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

---------

Co-authored-by: BirdNET-Go Addon Builder <addon-builder@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 14:15:44 +02:00
Alexandre
af2736a170 docs(birdnet-go-dev): document the first-daily-consensus setting (#3056)
* docs(birdnet-go-dev): document the first-daily-consensus setting

Adds a "Fork-only settings" section to the add-on README covering the
first-daily-detection consensus rule from alexbelgium/birdnet-go#63,
which this build compiles in because merge-prs.sh merges every open
non-draft PR on the fork.

The section leads with what the setting does NOT affect, because
"requires two models to confirm" reads as far more invasive than it is:
it is inert on a single-model install (the default), on bats and
non-bird sound classes, on species not known to every active bird model,
and when a dynamic threshold has already lowered the bar. Without that
list a user reasonably concludes the add-on will start dropping
detections.

Also qualifies the header's "everything below is identical to the
standard add-on" claim, which the new section would otherwise contradict.

Docs only; no add-on behaviour changes. Version bumped so Supervisor
offers the rebuild.

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

* docs(birdnet-go-dev): correct the first-daily-consensus wording

Copilot, Codex and CodeRabbit each independently flagged the same three
inaccuracies on PR #3056. Verified each against merge-prs.sh and the
alexbelgium/birdnet-go implementation before fixing.

- The "disappears once merged upstream" claim was backwards.
  merge-prs.sh syncs the fork's main with upstream BEFORE applying open
  PRs, so a PR that merges upstream stays in every future build via that
  sync step - it only stops being fork-only. Only a PR closed without
  merging drops out.
- "Every later detection... behaves exactly as today" undersold the
  rule. Every attempt for a species is held to the two-model bar until
  one is accepted, not just the first attempt - confirmed by reading
  firstDailyGateApplies, which re-evaluates every detection against the
  same memo until acceptedToday is true.
- "known to every active bird model" is decided per audio source
  (speciesSharedByActiveBirdModels scopes to sourceModelIDs, which comes
  from AnalysisBuffers(sourceID)), not add-on-wide across every source.

Docs only. validate.sh passes, no new lint findings.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-10 07:37:33 +02:00
github-actions
d31ccfa21c GitHub bot: changelog [nobuild] 2026-09-09 19:54:08 +00:00
Alexandre
9c277876ff Update config.yaml 2026-09-09 21:47:42 +02:00
github-actions
7e59aa8803 GitHub bot: changelog [nobuild] 2026-09-09 12:53:50 +00:00
Alexandre
360fd669fa Update config.yaml 2026-09-09 14:47:48 +02:00
github-actions
7bf77c8558 GitHub bot: changelog [nobuild] 2026-09-09 11:28:42 +00:00
Alexandre
a195d3ac39 Update version to 20260909.3 in config.yaml 2026-09-09 13:22:38 +02:00
github-actions
14834d4063 GitHub bot: changelog [nobuild] 2026-09-09 09:18:02 +00:00
Alexandre
f5caafd62c Update config.yaml 2026-09-09 11:12:33 +02:00
github-actions
fd61631ccd GitHub bot: changelog [nobuild] 2026-09-09 08:12:04 +00:00
Alexandre
578a7f8368 Update config.yaml 2026-09-09 10:06:30 +02:00
Alexandre
5115dcb93a Update config.yaml 2026-09-09 10:06:18 +02:00
github-actions
fa5e890c4c GitHub bot: changelog [nobuild] 2026-09-08 13:44:27 +00:00
Alexandre
addb0ca66f Update config.yaml 2026-09-08 15:38:17 +02:00
dependabot[bot]
16e112b21f build(deps): bump anthropics/claude-code-action from 1.0.210 to 1.0.216 (#3055)
Bumps [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) from 1.0.210 to 1.0.216.
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](a874e9ecd7...d75b94d5ad)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 15:35:47 +02:00
BirdNET-Go Addon Builder
040a4e0f2f birdnet-go-dev: rebuild 20260908.1 (upstream sync, 4 commits; fork PR #62) 2026-09-08 15:00:11 +02:00
alexbelgium
f72f7bef0f birdnet-go-dev: rebuild 20260908 (upstream sync, 7 commits) 2026-09-08 09:34:22 +02:00
BirdNET-Go Addon Builder
c18f45d822 birdnet-go-dev: rebuild 20260907 (fork PR #57 updated) 2026-09-07 14:39:59 +02:00
Alexandre
abf3d76873 birdnet-go-dev: add merge-prs.sh --check to catch build-breaking PR conflicts (#3053)
* birdnet-go-dev: add merge-prs.sh --check to catch build-breaking PR conflicts

The add-on build merges every open non-draft fork PR onto an upstream-synced
main. GitHub's `mergeable` field answers a different question: it compares a PR
against its *own* base ref, which for the stacked dashboard PRs is another
feature branch - sometimes one belonging to an already-closed PR. So a PR can
read MERGEABLE/CLEAN and still fail the build.

That is how run 34101694542 broke: PR #57 is MERGEABLE against the frozen branch
of closed PR #56, but conflicts with main on DetectionCard.svelte.

--check replays the exact same merge sequence, skips past conflicts instead of
stopping at the first one, and reports every offender as

    !!! CONFLICT pr=#N conflicts-with=<main|accumulated> files=... title=...

conflicts-with is probed in a throwaway worktree against the pristine synced
main, so it separates a PR that is merely stale (fixable in its own branch) from
one that only clashes with another open PR (needs a cross-PR decision).

Build behaviour is unchanged; --check is a no-op unless asked for, so no version
bump.

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

* birdnet-go-dev: fix --check misclassification, arg handling and temp leak

Review findings on #3053, all reproduced against git fixtures before fixing.

1. The probe did not apply the build's own merge policy (Codex).
   The real merge treats a *sole* frontend/package-lock.json conflict as
   non-fatal, but merges_onto_main() did a raw merge. A PR whose only clash
   with main is the generated lockfile was therefore reported as
   conflicts-with=main, which prints "the PR is stale against main; merge main
   into its branch" when in truth it merges onto main fine and only clashes
   with another open PR — the exact opposite remediation, from the feature
   whose entire job is to say which one it is.

   Extracted the policy into resolve_sole_lockfile() and routed both the real
   merge and the probe through it, so the two cannot drift apart again.

2. A second positional operand silently won (CodeRabbit).
   `merge-prs.sh a b` ran against b, where the pre-flag script used "${1}".
   A stray argument would have cloned into the wrong directory. Now exits 64.

3. mktemp parent directory leaked (CodeRabbit, Copilot).
   probe="$(mktemp -d)/probe" and only the child was removed, leaking one
   empty dir per checked conflict. Measured 3 leaked dirs over 3 calls; now 0.

Verified with throwaway repos: a lockfile-only clash now classifies as
"accumulated" (was "main"), a real source conflict with main still classifies
as "main", a clean PR still classifies as "accumulated", 0 leaked temp dirs,
0 stray worktrees, and the argument matrix behaves. shellcheck clean.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: BirdNET-Go Addon Builder <addon-builder@users.noreply.github.com>
2026-09-07 14:05:42 +02:00
458 changed files with 3300 additions and 3077 deletions

View File

@@ -4,29 +4,30 @@ description: >-
Workflow for alexbelgium/hassio-addons Home Assistant add-on work: diagnose with real Workflow for alexbelgium/hassio-addons Home Assistant add-on work: diagnose with real
measurements, independent Codex review, implement, open a PR, resolve CodeRabbit / Copilot / measurements, independent Codex review, implement, open a PR, resolve CodeRabbit / Copilot /
Codex bot review comments, verify in production. Use for any task touching an add-on in this Codex bot review comments, verify in production. Use for any task touching an add-on in this
repo — bugs, RAM/CPU/performance tuning, Dockerfile, config.yaml, cont-init.d or s6 changes, repo — bugs, RAM/CPU/performance tuning, Dockerfile, config.yaml, build.json,
version bumps, opening or iterating PRs — and when asked to "check with codex", "verify with updater.json, cont-init.d, services.d or s6 changes, version and CHANGELOG bumps, a failing
chatgpt", or resolve bot comments. Cheap for small asks: a light path skips the heavy steps. add-on CI check, opening or iterating PRs — and, on an add-on task, when asked to "check with
codex", "verify with chatgpt", or resolve bot comments. Cheap for small asks: a light path skips
the heavy steps.
--- ---
# Home Assistant add-on workflow # Home Assistant add-on workflow
**Answer style.** Chat replies are terse: no pleasantries, no tool-call narration, no decorative **Answer style.** Chat replies are terse no pleasantries, tool-call narration, decorative tables,
tables or emoji, no dumped logs quote the shortest decisive line, and don't re-read or re-print emoji or dumped logs; quote the shortest decisive line and don't re-print what is already in
what is already in context. Fragments and dropped articles are fine. Never compressed: uncertainty context. Telegraphic fragments are fine *there*. Two things outrank brevity, because dropping a
markers ("likely", "assumed", "not verified"), negations (`not`/`never`/`no`/`only`), numbers, word from either changes the meaning rather than shortening it: never compress uncertainty markers
units, technical terms, code blocks, error strings — step 9's Verified/Checked/Assumed distinction ("likely", "assumed", "not verified"), negations, numbers, units, technical terms, code or error
outranks brevity every time. Write in full prose, not fragments, for security warnings, strings — step 9's Verified/Checked/Assumed distinction wins every time; and write full prose
irreversible-action confirmations, and any multi-step sequence a fragment could make ambiguous. wherever a fragment could be read two ways — security warnings, irreversible-action confirmations,
Persisted text is prose too: commits, CHANGELOG entries, PR bodies, review-thread replies, the multi-step sequences, and everything persisted (commits, CHANGELOG entries, PR bodies, review-thread
step 10 report. replies, the step 10 report).
Triage first, then one of two paths: Triage first, then one of two paths:
- **Light** — typo/doc fixes, CHANGELOG edits, version bumps, one-file edits at ladder levels - **Light** — typo/doc fixes, CHANGELOG edits, version bumps, one-file edits at ladder levels
1-3 (below), simple questions: scope → implement → validate (`$SKILL/scripts/validate.sh 1-3 (below), simple questions: scope → implement → validate (step 4) → PR (version bump +
<addon> --vs-master`; `$SKILL` defined below) → PR (version bump + CHANGELOG still required) → CHANGELOG still required) → resolve bot comments.
resolve bot comments.
- **Full loop** — performance/RAM/CPU work, diagnosis, anything changing a shipped default, - **Full loop** — performance/RAM/CPU work, diagnosis, anything changing a shipped default,
ladder levels 4-6, or an explicit Codex-check request: scope → measure → plan → Codex reviews ladder levels 4-6, or an explicit Codex-check request: scope → measure → plan → Codex reviews
the plan → implement → simplify → Codex reviews the code → **simplify again** → PR → resolve the plan → implement → simplify → Codex reviews the code → **simplify again** → PR → resolve
@@ -47,9 +48,8 @@ reasoning about a hypothetical *host* either. A defensive branch is complexity l
the input that reaches it and the image or host where that happens, or delete it and let the case the input that reaches it and the image or host where that happens, or delete it and let the case
fail visibly instead. fail visibly instead.
**Repo layout.** `alexbelgium/hassio-addons`; each add-on is a top-level directory. This skill is **Skill root.** The canonical copy lives at `.claude/skills/hassio-addon-workflow/`. Set it once;
checked in at `.claude/skills/hassio-addon-workflow/` (canonical copy). Set the skill root once, every `scripts/…` and `references/…` path below is relative to it:
then every `scripts/…` and `references/…` path below is relative to it:
```bash ```bash
SKILL="$(git rev-parse --show-toplevel)/.claude/skills/hassio-addon-workflow" SKILL="$(git rev-parse --show-toplevel)/.claude/skills/hassio-addon-workflow"
@@ -63,9 +63,10 @@ bash "$SKILL/scripts/preflight.sh" # and likewise for the other scripts
**Delegate heavy output to a subagent.** Codex reviews and multi-thread PR triage produce output **Delegate heavy output to a subagent.** Codex reviews and multi-thread PR triage produce output
you don't need verbatim in your own context. For Codex's plan review (step 3), Codex's code you don't need verbatim in your own context. For Codex's plan review (step 3), Codex's code
review (step 6), and PR-comment listing when there are more than ~5 threads (step 8): launch a review (step 6), and PR-comment listing when there are more than ~5 threads (step 8): if subagents
subagent to run the command and report back only the objections/findings and your assessment of are available, launch one to run the command and report back only the objections/findings and your
each, not the raw transcript. assessment of each, not the raw transcript. Otherwise redirect the output to a file and read the
parts you need.
--- ---
@@ -85,9 +86,11 @@ Measure the running add-on rather than reasoning from source (`$BUILD_VERSION` s
- Is this flag/driver/package actually present? → inspect the artifact: `/proc/<pid>/cmdline`, - Is this flag/driver/package actually present? → inspect the artifact: `/proc/<pid>/cmdline`,
`command -v`, `/var/log/apt/history.log` `command -v`, `/var/log/apt/history.log`
Verify you're reading the right revision first — `scripts/preflight.sh` catches a stale branch Verify you're reading the right revision first — `scripts/preflight.sh` compares the checkout
before it costs a full analysis pass. Measurement methodology, gotchas, and real failure examples: against the running `$BUILD_VERSION`, which catches the usual stale branch before it costs a full
`references/evidence.md`. analysis pass (a version match does not prove the source is identical). Read
`references/evidence.md` before interpreting any number you did not get straight from
`measure.sh`, and for the failure modes this step exists to prevent.
## 3. Plan — choose the mechanism level, then Codex reviews it (full loop) ## 3. Plan — choose the mechanism level, then Codex reviews it (full loop)
@@ -117,45 +120,60 @@ Attack your own plan before implementing:
- What am I **inferring** that I could instead **detect at runtime** or **record explicitly**? - What am I **inferring** that I could instead **detect at runtime** or **record explicitly**?
Highest-yield question here — see `references/evidence.md`'s failure-mode section. Highest-yield question here — see `references/evidence.md`'s failure-mode section.
- For every branch that exists **only to survive something going wrong**: name the image or host - For every branch that exists **only to survive something going wrong**: name the image or host
where that input actually arrives, and go and look. Naming is the bar, not reproducing it here — where that input arrives (the standing rule above) and go and look, before you write it.
`references/simplify.md` works the `/dev/shm` guard and the `s6-dumpenv` fallback through that
distinction.
Full loop only, before writing code: get Codex's independent read on the plan. Spawn a subagent Full loop only, before writing code: get Codex's independent read on the plan, delegated as above —
whose prompt includes the path `references/codex-review.md` and tells it to follow that file's `references/codex-review.md` has the invocation and how to write the prompt.
invocation, then report back only Codex's objections and an assessment of each — not the raw
transcript.
## 4. Implement ## 4. Implement
Touching a shell script, Dockerfile, or env option? Read `references/traps.md` first — skim the Read the `references/traps.md` section matching what you're about to touch. It is ~18 KB and all
headings, read the sections you're about to touch; the bashio, s6-env, arch-guard and versioning but one section is irrelevant to any given edit, so print the one you need rather than reading the
traps are all live. (The light-path facts it holds — versioning format, CHANGELOG heading — are file — run it with no argument to list the sections:
already inline in step 7.) Validate with `scripts/validate.sh <addon> --vs-master`. Write
behavioural tests for anything with branches, targeting **the regression a reviewer described**, | Touching | Run |
not just the happy path. | --- | --- |
| an option or anything a base-image service reads | `bash "$SKILL/scripts/traps.sh" passing` |
| a file the app also writes itself | `bash "$SKILL/scripts/traps.sh" "app's own"` |
| shell, bashio, a symlinked script | `bash "$SKILL/scripts/traps.sh" bashio` |
| `Dockerfile`, `build.json`, an arch guard | `bash "$SKILL/scripts/traps.sh" dockerfile` |
| Chromium, Electron, Xvfb | `bash "$SKILL/scripts/traps.sh" chromium` |
Then validate with `scripts/validate.sh <addon> --vs-master`, and write behavioural tests for
anything with branches, targeting **the regression a reviewer described**, not just the happy
path.
## 5. Simplify ## 5. Simplify
Before requesting review, check: did the diff stay at the ladder level chosen in step 3? Can this Six questions over your own diff, before anyone else reads it:
be solved by deleting instead of adding? Is the fix bigger than what it fixes? How does it fail in
three years? And on reuse: does any hunk reimplement something `.templates/`, another script in - **Level** — did the diff stay at the ladder level chosen in step 3, or creep up one?
this add-on, or a sibling add-on already does — and if a future add-on hits this same problem, - **Deletion** — can this be solved by deleting instead of adding?
will it find one way to solve it or two? Fold a near-duplicate into the existing mechanism, or - **Size** — is the fix bigger than the thing it fixes?
justify the divergence in the PR body — but never at the cost of an isolation rule - **Reuse** — does any hunk reimplement what `.templates/`, another script in this add-on, or a
`references/traps.md` documents: scripts shared by symlink with the webtop add-ons take a new sibling add-on already does? If a future add-on hits this problem, will it find one way to solve
numbered script, not an edit. Case studies of what happens when this check is skipped: it or two? Fold near-duplicates in, or justify the divergence in the PR body.
`references/simplify.md`. - **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.
- **Longevity** — how does this fail in three years, when the base image or upstream has moved?
The standing exception to Reuse and Depth: scripts shared by symlink with the webtop add-ons take a
new numbered script, never an edit (`references/traps.md#shell-and-bashio`). Case studies for the rest, including
what shipped when this pass was skipped: `references/simplify.md`.
## 6. Codex attacks the code, then simplify what the review added (full loop only) ## 6. Codex attacks the code, then simplify what the review added (full loop only)
Same delegated invocation, pointed at `git diff origin/master...HEAD` plus your reasoning per Same delegated invocation, pointed at `git diff origin/master...HEAD` plus your reasoning per
hunk. Details in `references/codex-review.md`. hunk. Details in `references/codex-review.md`.
Then run step 5's checks again over the hunks the review changed. Adversarial review only ever Then run step 5's checks again over the hunks the review changed. Adversarial review mostly argues
argues *for* another branch — that is its job — so accepting objections ratchets the diff upward, *for* another branch — that is what it is asked to do — so accepting objections tends to ratchet
and nothing else in the loop walks it back down. For each accepted objection: is the case it the diff upward, and nothing else in the loop walks it back down. Sort each objection before you
defends one you have now demonstrated, or one you have merely been told about? Taking a write anything:
**"this is wrong"** is a bug and you fix it; **"this is undefended"** is a claim about some host,
and it needs the same demonstration you would demand of a measurement — is the case it defends one
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 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. to fewer lines than you started the review with is the normal outcome, not a suspicious one.
@@ -165,20 +183,23 @@ kind of edit that leaves a stray `fi` behind.
## 7. Open the PR ## 7. Open the PR
CI gates on a PR: **`CHANGELOG.md` updated** (hard fail), the **HA add-on linter** Three hard gates — **`CHANGELOG.md` updated**, the **HA add-on linter**
(`frenck/action-addon-linter`, blocking — not the weekly Super-Linter, which is non-blocking), and (`frenck/action-addon-linter`), and the **add-on image build** — but only on a PR that changes a
the **add-on image build**. Bump `version` anyway (`X.Y.Z.N`, never `X.Y.Z-N`, see top-level `config.*`. On a PR that doesn't (docs, `.github/`, `.claude/`) they *skip*, which is not
`references/traps.md#versioning`) — Supervisor won't offer a rebuild without it. Update the same as passing. Super-Linter runs on every PR and is `continue-on-error`, so it never blocks;
`README.md` if you added options; write the CHANGELOG heading as `## <version> (<date>)`, fix its real findings anyway. Nothing checks the version bump, so bump it yourself — Supervisor
matching the date format already in that file — almost always ISO `YYYY-MM-DD`, see won't offer a rebuild without one, and `CLAUDE.md` has the format. Update `README.md` if you added
`references/traps.md#ci-and-review-bots`. options; write the CHANGELOG heading as `## <version> (<date>)`, matching the date format already
in that file — almost always ISO `YYYY-MM-DD`, see `references/traps.md#ci-and-review-bots`.
Write the body to a file, `gh pr create --body-file`: state what was measured, what changed, Write the body to a file, `gh pr create --body-file`: state what was measured, what changed,
**what is not verified**, and how to roll back the riskiest hunk alone. **what is not verified**, and how to roll back the riskiest hunk alone.
## 8. Resolve review comments ## 8. Resolve review comments
`scripts/pr_review.sh list|reply|resolve|status|watch <PR>`. For every comment, **reproduce the `scripts/pr_review.sh list|status|watch <PR>` to read, `reply <PR> <COMMENT_ID> <text|@file>` and
`resolve <PR> <THREAD_ID…|--all>` to answer; run it with no arguments for the full usage. For every
comment, **reproduce the
claim before agreeing or disagreeing** — reviewers are frequently right and occasionally claim before agreeing or disagreeing** — reviewers are frequently right and occasionally
confidently wrong; a reproduction takes a minute and decides it either way. Reply with the confidently wrong; a reproduction takes a minute and decides it either way. Reply with the
evidence, then resolve. **Push back when you're right**, on the thread — a resolved-but-wrong evidence, then resolve. **Push back when you're right**, on the thread — a resolved-but-wrong
@@ -192,13 +213,12 @@ work" — either it was exercised, or say plainly it wasn't.
Light path: verification is `validate.sh` plus CI; anything beyond that is Assumed. Full loop: CI Light path: verification is `validate.sh` plus CI; anything beyond that is Assumed. Full loop: CI
passing proves the build works, not that the change does anything — re-run the measurement that passing proves the build works, not that the change does anything — re-run the measurement that
motivated the work once the rebuilt add-on is running. After merge, `git fetch origin master` motivated the work once the rebuilt add-on is running. Real "merged and inert" examples, and what
(the tracking ref is stale otherwise), then confirm the *changes* survived — `git diff to do when a fix cannot be self-verified: `references/evidence.md`. Then confirm the change
origin/master -- <the paths you touched>` comes back empty. Ancestry is not the check: a revert survived the merge: `git fetch origin master` first (the tracking ref is stale otherwise), then
leaves your commit in history and undoes its tree, so `--contains` reports success either way. The `git diff origin/master -- <the paths you touched>` must come back empty. Ancestry is not the
builder's revert-on-failure job can revert a merge for reasons unrelated to your diff (see check, and the builder reverts merges for reasons unrelated to your diff — both explained in
`references/traps.md#ci-and-review-bots`). Real "merged and inert" examples, and what `references/traps.md#ci-and-review-bots`.
to do when a fix can't be self-verified: `references/evidence.md`.
## 10. Calibrate and report ## 10. Calibrate and report

View File

@@ -2,9 +2,7 @@
Used for step 3 (plan review) and step 6 (code review), full loop only. Codex is a genuinely Used for step 3 (plan review) and step 6 (code review), full loop only. Codex is a genuinely
different model reading the files itself; on this workload it has repeatedly been worth the different model reading the files itself; on this workload it has repeatedly been worth the
minutes. Delegate the invocation to a subagent (see SKILL.md's subagent-delegation note) so its minutes. Run it through a subagent, per SKILL.md's delegation note.
output doesn't land verbatim in your context — have the subagent return only Codex's objections
and your assessment of each.
## Invocation ## Invocation
@@ -12,11 +10,12 @@ and your assessment of each.
on ~4 KB prompts (2026-08-03); the CLI with the same content succeeded. The MCP tool is still fine on ~4 KB prompts (2026-08-03); the CLI with the same content succeeded. The MCP tool is still fine
for short questions. for short questions.
`--sandbox read-only` lets Codex read files but blocks writes and command execution, and `--sandbox read-only` blocks writes, not reads, and `approval_policy=never` stops it asking for
`approval_policy=never` means it will not be prompted for permission to run anything either — so permission rather than stopping it acting — so it can still run read-only commands and often
paste every number into the prompt rather than expecting Codex to gather it. `- <` feeds the falls back to fetching the repo from GitHub instead of reading your worktree. Paste every number
prompt file on stdin. Run it in the background so you are not blocked for the several minutes it into the prompt rather than expecting it to gather them, and treat what it reports about *local*
takes (`&` here, or your harness's background-task mechanism): state as unverified. `- <` feeds the prompt file on stdin. Run it in the background so you are not
blocked for the several minutes it takes (`&` here, or your harness's background-task mechanism):
```bash ```bash
codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \ codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \
@@ -40,9 +39,5 @@ codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \
confidently, and separately caught a genuine methodology error in the same review. Treat its confidently, and separately caught a genuine methodology error in the same review. Treat its
confirmations with the same scepticism as its objections — especially about the build. confirmations with the same scepticism as its objections — especially about the build.
**Its objections ratchet complexity upward.** An adversarial reviewer is asked to find what could **Its objections only ever argue for more code** — it is asked what could go wrong, never whether
go wrong, so its output is a list of arguments for more code; it is never asked whether the branch the branch it wants is reachable. Sort them before writing anything; SKILL.md step 6 is that pass.
it wants is reachable. Separate "this is wrong" from "this is undefended" before you write
anything: the first is a bug and you fix it, the second is a claim about some host, and it needs
the same demonstration you would demand of a measurement. That is what step 6's second simplify
pass is for.

View File

@@ -1,16 +1,24 @@
# Evidence — measurement methodology and case studies # Evidence — measurement methodology and case studies
## Why summed RSS and reserved-vs-resident both matter ## How to read the numbers
- **Summed RSS double-counts shared pages.** Removing a duplicate process frees its *private* **Summed RSS overstates savings.** Shared library pages are counted once per process, so removing
memory, not its RSS. `scripts/measure.sh` reports PSS and private alongside RSS — quote a duplicate frees its *private* memory, not its RSS. Measured example: four MCP shims summed to
**private** when arguing "removing this saves N MB". 882 MB RSS but 643 MB PSS / 564 MB private, and per-process private ranged 54 MB down to 2 MB
- **A big mapping is not necessarily resident.** Large SysV/tmpfs segments are lazily populated; which completely changes which duplicate is worth removing. Quote private when arguing "removing
reserved size is reported separately from resident for this reason. this saves N MB".
- `/proc/meminfo` and `free` show **host** figures (no memory cgroup namespace here) — never
attribute those to the add-on. **A large mapping is often not resident.** SysV/tmpfs segments are lazily populated. Xvfb's
- Sample duration matters: a 3 s CPU sample measured 2.3% where a 20 s sample measured 21.6% for 506 MB framebuffer shows `Rss: 0` in `/proc/<pid>/smaps`. Check before calling anything a leak.
the same process. Use ≥20 s for anything you report.
**`/proc/meminfo` and `free` show host figures** — there is no memory cgroup namespace here.
Never attribute those totals to the add-on.
**A short CPU sample is not a CPU measurement.** A 3 s sample measured 2.3% where a 20 s sample
measured 21.6% for the same process. Use >= 20 s for anything you report.
`scripts/measure.sh` already reports PSS and private alongside RSS, and resident separately from
reserved, so these three only bite when you compute a figure yourself or quote one from `ps`.
## Before asserting anything, ask what would show it false ## Before asserting anything, ask what would show it false

View File

@@ -30,28 +30,24 @@ 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 It took the maintainer asking "is this the simplest way possible?" to run the pass that step 6
now requires. now requires.
## Checks worth running against your own diff ## Where SKILL.md step 5's questions get hard
- **Did the diff stay at the ladder level chosen in step 3?** If it crept up a level, either **Deletion is not automatically the safe direction.** The `/dev/shm` case in `evidence.md` is a
justify that out loud or redo it at the level you chose. removal that reintroduced a crash loop on hosts unlike this one. A removal that depends on a host
- **Can this be solved by deleting instead of adding?** A flag that shouldn't be passed, a default needs the same verification as an addition.
process that shouldn't start, a registration that shouldn't be duplicated. Deleting usually
shrinks the regression surface — but not always: the `/dev/shm` case in **Naming the case is the bar for a defensive branch, not reproducing it.** Docker's 64 MB
`references/evidence.md` is a removal that reintroduced a crash loop on hosts unlike this one. `/dev/shm` default is documented behaviour that Home Assistant does not override, so that guard
A removal that depends on a host default still needs the same verification as an addition. stays even though this host measured 7.7 GB. Nobody could name a single image shipping
- **Is the fix bigger than the thing it fixes?** That is a smell, not a rule — but it usually `with-contenv` without `s6-dumpenv`, so that fallback went. If you cannot name the case, delete the
means the problem was framed one level too deep. branch: the situation then fails the way it already fails today, visibly, instead of through a
- **For each defensive branch: what input reaches it, on which image or host?** Go and check, second path that is never exercised and silently rots as the base images move. Weigh the cost both
the way you would check a measurement. The bar is being able to **name** the case, not to ways — a one-flag guard against a crash you cannot rule out is cheap; a second code path that
reproduce it here: Docker's 64 MB `/dev/shm` default is documented behaviour that HA does not degrades to the pre-fix behaviour anyway is not. Write in the PR body what you cut and why, so the
override, so the bullet above keeps that guard even though this host measured 7.7 GB. Nobody next person does not re-add it from the same reasoning.
could name a single image shipping `with-contenv` without `s6-dumpenv`, so that fallback went.
If you cannot name the case, delete the branch — the situation then fails the way it already **"Bigger than the thing it fixes" is a smell, not a rule** — but it usually means the problem was
fails today, visibly, instead of through a second path that is never exercised and silently framed one level too deep.
rots as the base images move. Weigh the cost too: a one-flag guard against a crash you cannot
rule out is cheap, a second code path that degrades to the pre-fix behaviour anyway is not. **Three-year failure** favours code that reads a documented knob. Code that reaches into private
Write down in the PR body what you cut and why, so the next person does not re-add it from the internals does not survive the base image moving.
same reasoning.
- **How does this fail in three years**, when the base image, Electron, or upstream has moved?
Code that reads a documented knob keeps working. Code that reaches into private internals
does not.

View File

@@ -9,7 +9,6 @@ workflows and lint rules — that is not repeated here.
## Contents ## Contents
- [Environment and workspace](#environment-and-workspace) - [Environment and workspace](#environment-and-workspace)
- [Measurement](#measurement)
- [Passing values into base-image services](#passing-values-into-base-image-services) - [Passing values into base-image services](#passing-values-into-base-image-services)
- [Writing into an app's own config](#writing-into-an-apps-own-config) - [Writing into an app's own config](#writing-into-an-apps-own-config)
- [Shell and bashio](#shell-and-bashio) - [Shell and bashio](#shell-and-bashio)
@@ -46,20 +45,6 @@ gate. One observed run took ~3 hours, with 20+ runs queued against 2 executing
runner contention, not the diff. Check `gh run list` before concluding your PR is stuck. Poll in runner contention, not the diff. Check `gh run list` before concluding your PR is stuck. Poll in
a background task, and never claim the build is verified when it hasn't run. a background task, and never claim the build is verified when it hasn't run.
## Measurement
**Summed RSS overstates savings.** Shared library pages are counted once per process, so removing
a duplicate frees its *private* memory, not its RSS. Measured example: four MCP shims summed to
882 MB RSS but 643 MB PSS / 564 MB private, and per-process private ranged 54 MB down to 2 MB —
which completely changes which duplicate is worth removing. Quote private when arguing "removing
this saves N MB".
**A large mapping is often not resident.** SysV/tmpfs segments are lazily populated. Xvfb's
506 MB framebuffer shows `Rss: 0` in `/proc/<pid>/smaps`. Check before calling anything a leak.
**`/proc/meminfo` and `free` show host figures** — there is no memory cgroup namespace here.
Never attribute those totals to the add-on.
**`rtk` filters some command output.** For a complete listing, redirect to a file and read that **`rtk` filters some command output.** For a complete listing, redirect to a file and read that
(`ps ... > $SP/ps.txt`), or use `rtk proxy <cmd>`. (`ps ... > $SP/ps.txt`), or use `rtk proxy <cmd>`.
@@ -85,10 +70,28 @@ service run scripts including `svc-xorg`, and Xvfb runs with `-vfbdevice /dev/dr
- `00-global_var.sh` is cont-init **00**. Any cont-init script numbered higher runs *after* the - `00-global_var.sh` is cont-init **00**. Any cont-init script numbered higher runs *after* the
injection, so it cannot change what a service will see through stage 2. injection, so it cannot change what a service will see through stage 2.
- LSIO's `svc-xorg` starts `#!/usr/bin/env bashio`, **not** `with-contenv`, so it never reads - **Whether a service's own shebang still decides stage 3 depends on whether `ha_entrypoint.sh`
stage 3 at all. Writing `container_environment` for it is a silent no-op — that shipped: the runs as PID 1** — i.e. whether the add-on replaces the base `ENTRYPOINT ["/init"]` with one that
file was written 6 seconds before Xvfb started, and Xvfb still came up at the base-image makes `ha_entrypoint.sh` itself the container's entrypoint (live today in `ente` and
default. `free_games_claimer`; `wger` has the override written into its Dockerfile but commented out, so
it is not currently one of these — check the Dockerfile, not this list). Under the normal
`/init` path this script runs as the stage-2 hook, `$PID1` is false, and it never touches
`services.d/*/run` or `s6-overlay/s6-rc.d/*/run` at all (`.templates/ha_entrypoint.sh:430`,
gated on `if $PID1`) — s6's own stage 1 already created `/run/s6/container_environment` before
any cont-init script ran, so a service's shipped `with-contenv` shebang reads it normally.
Only the `ENTRYPOINT`-override path breaks this, and it breaks it twice over: s6 stage 1 never
runs, so nothing ever creates the envdir; and because `ha_entrypoint.sh` is now PID 1, it
rewrites the first line of every service `run` file to whichever shebang its own
`candidate_shebangs` probe landed on. That probe's first candidate,
`/command/with-contenv bashio`, fails precisely because the envdir was never created, so it
falls through to `/usr/bin/env bashio` for every service — the real mechanism behind the shipped
`svc-xorg` failure (its envdir file was written 6 seconds before Xvfb started, and Xvfb still
came up at the base-image default). `cont-init.d` scripts are a separate case: `run_one_script`
rewrites their shebang unconditionally, with no `$PID1` gate, so a cont-init script's own
shebang is never informative either way. `ha_entrypoint.sh` dumps the environment itself to
compensate for the missing envdir, and the envdir writes in `00-global_var.sh` /
`01-config_yaml.sh` are `if [ -d ]` guarded, so they take effect only once something has created
that directory.
**Renaming an option to match a base-image env var moves validation out of your script and into **Renaming an option to match a base-image env var moves validation out of your script and into
the schema.** `00-global_var.sh` exports empty strings (only objects/arrays/nulls are dropped), the schema.** `00-global_var.sh` exports empty strings (only objects/arrays/nulls are dropped),
@@ -190,11 +193,9 @@ build.
## Versioning ## Versioning
**`X.Y.Z.N`, never `X.Y.Z-N`.** A hyphen parses as a semver pre-release, which Supervisor treats `CLAUDE.md` owns the format (`X.Y.Z.N`, never `X.Y.Z-N`, and why). The one thing it does not say:
as *older* than `X.Y.Z` — the update is never offered. date-based versions (`2026.08.03`) are common here, so check whether master has already moved to
the version you were about to use before you pick it.
Date-based versions (`2026.08.03`) are common here. Check whether master has already moved to the
version you were about to use.
## Chromium / Electron under Xvfb ## Chromium / Electron under Xvfb

View File

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

View File

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

View File

@@ -5,7 +5,14 @@
# Usage: preflight.sh [repo-path] [addon-slug] # Usage: preflight.sh [repo-path] [addon-slug]
set -uo pipefail set -uo pipefail
REPO="${1:-/data/claude/hassio-addons}" # Default to the checkout this is run from, never a fixed path: a fixed path silently inspected
# the main checkout while the caller worked in a worktree, reporting a branch nobody was editing —
# the exact stale-checkout trap this script exists to catch. Falling back to one when git cannot
# answer would recreate it, so refuse instead and make the caller say which repo they mean.
REPO="${1:-}"
if [ -z "$REPO" ]; then
REPO=$(git rev-parse --show-toplevel 2> /dev/null) || REPO=""
fi
SLUG="${2:-}" SLUG="${2:-}"
echo "== tools ==" echo "== tools =="
@@ -26,9 +33,14 @@ fi
echo echo
echo "== repo ==" echo "== repo =="
# git-aware check: in a worktree .git is a file, not a directory # git-aware check: in a worktree .git is a file, not a directory
if [ -z "$REPO" ]; then
echo " not inside a git checkout, and no repo path given"
echo " -> pass one explicitly: preflight.sh <repo-path> [addon-slug]"
exit 1
fi
if ! git -C "$REPO" rev-parse --git-dir > /dev/null 2>&1; then if ! git -C "$REPO" rev-parse --git-dir > /dev/null 2>&1; then
echo " no git repo at $REPO" echo " no git repo at $REPO"
exit 0 exit 1
fi fi
cd "$REPO" || exit 0 cd "$REPO" || exit 0
branch=$(git branch --show-current 2> /dev/null || echo "(detached)") branch=$(git branch --show-current 2> /dev/null || echo "(detached)")
@@ -47,10 +59,7 @@ if [ -n "${BUILD_VERSION:-}" ]; then
if [ -z "$SLUG" ] && [ -n "${HOSTNAME:-}" ]; then if [ -z "$SLUG" ] && [ -n "${HOSTNAME:-}" ]; then
base=$(printf '%s' "$HOSTNAME" | sed 's/^[0-9a-f]\{8\}-//') base=$(printf '%s' "$HOSTNAME" | sed 's/^[0-9a-f]\{8\}-//')
for cand in "$(printf '%s' "$base" | tr '-' '_')" "$base"; do for cand in "$(printf '%s' "$base" | tr '-' '_')" "$base"; do
[ -f "$REPO/$cand/config.yaml" ] && { [ -f "$REPO/$cand/config.yaml" ] && { SLUG="$cand"; break; }
SLUG="$cand"
break
}
done done
[ -z "$SLUG" ] && SLUG="$base" [ -z "$SLUG" ] && SLUG="$base"
fi fi
@@ -72,8 +81,8 @@ if [ -n "${BUILD_VERSION:-}" ]; then
else else
echo " MISMATCH — this branch is NOT what is running." echo " MISMATCH — this branch is NOT what is running."
git fetch origin master --quiet 2> /dev/null git fetch origin master --quiet 2> /dev/null
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null \ master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null |
| grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}') grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
echo " origin/master version = ${master:-unknown}" echo " origin/master version = ${master:-unknown}"
echo " -> work from origin/master; analysing this branch will mislead you." echo " -> work from origin/master; analysing this branch will mislead you."
echo echo

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Print one section of references/traps.md.
#
# traps.md is ~18 KB and every section but one is irrelevant to any given edit: a shell fix needs
# 642 bytes of it, a Dockerfile fix 2.6 KB, and "CI and review bots" — 35% of the file — is needed
# at steps 7-8 and never at step 4. A markdown anchor cannot be loaded on its own, so reading the
# file to reach one section pays for all of them. This prints just the section, so the routing
# table in SKILL.md step 4 costs what it claims to.
#
# Usage: traps.sh <keyword> # substring of a section heading, case-insensitive
# traps.sh # list the sections
set -uo pipefail
TRAPS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/references/traps.md"
[ -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() {
echo "sections (pass any substring):"
grep '^## ' "$TRAPS" | grep -v '^## Contents' | sed 's/^## / /'
}
[ $# -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/^## //')
case "${#matches[@]}" in
0)
echo "no section matching '$1'" >&2
list >&2
exit 1
;;
1) ;;
*)
echo "'$1' matches ${#matches[@]} sections — be more specific:" >&2
printf ' %s\n' "${matches[@]}" >&2
exit 1
;;
esac
awk -v want="## ${matches[0]}" '
$0 == want { inside = 1; print; next }
inside && /^## / { exit }
inside { print }
' "$TRAPS"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ slug="${slug#*_}"
if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then
# New config location # New config location
CONFIGLOCATION="/config" CONFIGLOCATION="/config"
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/config.yaml" CONFIGFILEBROWSER="/app_configs/${HOSTNAME/-/_}/config.yaml"
else else
# Legacy config location # Legacy config location
CONFIGLOCATION="/config/addons_config/${slug}" CONFIGLOCATION="/config/addons_config/${slug}"
@@ -138,9 +138,9 @@ append_unique_line() {
# $1=file, $2=line # $1=file, $2=line
local _file="$1" local _file="$1"
local _line="$2" local _line="$2"
mkdir -p "$(dirname "$_file")" 2> /dev/null || true mkdir -p "$(dirname "$_file")" 2>/dev/null || true
touch "$_file" 2> /dev/null || true touch "$_file" 2>/dev/null || true
grep -qxF -- "$_line" "$_file" 2> /dev/null || echo "$_line" >> "$_file" grep -qxF -- "$_line" "$_file" 2>/dev/null || echo "$_line" >> "$_file"
} }
while IFS= read -r line; do while IFS= read -r line; do

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
- Migrate legacy add-on configuration map names to current app configuration terminology.
## 2026.08 (2026-08-01) ## 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 - Addon versions written in config.yaml now always comply with Home Assistant versioning: an upstream tag Home Assistant cannot order (`version-bf9e0b4f`, `ubuntu-2026-06-01`, ...) or would sort as older (`1.2.3-2`, `1.2.3+4`) no longer lands in config.yaml. The addon number is incremented instead, while the raw upstream tag stays in updater.json so the same release is never published twice
@@ -31,7 +32,7 @@
## 3.19 ## 3.19
- New HA config logic implemented. Files are now located in the addon config file, that can be accessed from the addon_configs folder from my filebrowser or cloudcommander addons. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ - New HA config logic implemented. Files are now located in the addon config file, that can be accessed from the app_configs folder from my filebrowser or cloudcommander addons. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
- Feat : if there is no releases in a github repo, check if there is a valid package and get the highest tag https://github.com/alexbelgium/hassio-addons/issues/1168 - Feat : if there is no releases in a github repo, check if there is a valid package and get the highest tag https://github.com/alexbelgium/hassio-addons/issues/1168
- Feat : github_exclude applies to dockerhub - Feat : github_exclude applies to dockerhub

View File

@@ -75,7 +75,7 @@ You can add the following tags in the file :
### Addon version numbering ### Addon version numbering
The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is _older_), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all. The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is *older*), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all.
The addon version is therefore derived from the upstream tag: The addon version is therefore derived from the upstream tag:
@@ -115,7 +115,7 @@ verbose: "false"
### Custom Scripts and Environment Variables ### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping: This addon supports custom scripts and environment variables through the `app_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons) - **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **Environment variables**: Use the add-on `env_vars` option and see [Add Environment Variables to your Addon](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon) for details. - **Environment variables**: Use the add-on `env_vars` option and see [Add Environment Variables to your Addon](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon) for details.

View File

@@ -8,7 +8,7 @@ environment:
image: ghcr.io/alexbelgium/addons_updater-{arch} image: ghcr.io/alexbelgium/addons_updater-{arch}
init: false init: false
map: map:
- addon_config:rw - app_config:rw
name: Repository Updater name: Repository Updater
options: options:
date_iso8601: true date_iso8601: true

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ ports_description:
webui: http://[HOST]:[PORT:3001] webui: http://[HOST]:[PORT:3001]
map: map:
- addon_config:rw - app_config:rw
- share:rw - share:rw
- media:rw - media:rw

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
- Migrate legacy add-on configuration map names to current app configuration terminology.
## 0.12.1 (2026-09-03) ## 0.12.1 (2026-09-03)
- Update to Baikal 0.12.1 from 0.10.1 (changelog : <https://github.com/sabre-io/Baikal/releases>). This includes the 0.12.1 fix for an XSS vulnerability that let an authenticated user take over the admin interface by renaming a calendar - Update to Baikal 0.12.1 from 0.10.1 (changelog : <https://github.com/sabre-io/Baikal/releases>). This includes the 0.12.1 fix for an XSS vulnerability that let an authenticated user take over the admin interface by renaming a calendar
@@ -28,7 +29,7 @@
## 0.9.5 (2024-04-27) ## 0.9.5 (2024-04-27)
- Update to latest version from ckulka/baikal-docker (changelog : https://github.com/ckulka/baikal-docker/releases) - Update to latest version from ckulka/baikal-docker (changelog : https://github.com/ckulka/baikal-docker/releases)
## 0.9.4-3 (2024-04-26) ## 0.9.4-3 (2024-04-26)
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/baikal to a folder only accessible from my Filebrowser addon called /addon_configs/something-baikal. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ - ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/baikal to a folder only accessible from my Filebrowser addon called /app_configs/something-baikal. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
## 0.9.4-2 (2024-01-14) ## 0.9.4-2 (2024-01-14)

View File

@@ -50,7 +50,7 @@ Configurations can be done through the app webUI, except for the following optio
### Custom Scripts and Environment Variables ### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping: This addon supports custom scripts and environment variables through the `app_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons) - **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **Environment variables**: See [Add Environment Variables to your Addon](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon) - **Environment variables**: See [Add Environment Variables to your Addon](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon)

View File

@@ -66,7 +66,7 @@ devices:
- /dev/nvme2 - /dev/nvme2
image: ghcr.io/alexbelgium/baikal-{arch} image: ghcr.io/alexbelgium/baikal-{arch}
map: map:
- addon_config:rw - app_config:rw
- homeassistant_config:rw - homeassistant_config:rw
- share:rw - share:rw
- ssl:ro - ssl:ro

View File

@@ -1,3 +1,4 @@
- Migrate legacy add-on configuration map names to current app configuration terminology.
## 2026.02.28 (28-02-2026) ## 2026.02.28 (28-02-2026)
- Minor bugs fixed - Minor bugs fixed
## 2026.02.25-2 (25-02-2026) ## 2026.02.25-2 (25-02-2026)

View File

@@ -86,7 +86,7 @@ This addon supports mounting both local drives and remote SMB shares:
### Custom Scripts and Environment Variables ### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping: This addon supports custom scripts and environment variables through the `app_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons) - **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details. - **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.

View File

@@ -64,7 +64,7 @@ ingress: true
ingress_stream: true ingress_stream: true
init: false init: false
map: map:
- addon_config:rw - app_config:rw
- media:rw - media:rw
- share:rw - share:rw
- ssl - ssl

View File

@@ -1,3 +1,4 @@
- Migrate legacy add-on configuration map names to current app configuration terminology.
## 1.6.0.2 (2026-07-27) ## 1.6.0.2 (2026-07-27)
@@ -26,7 +27,7 @@
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases) - Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
## 1.5.4-1 (2026-01-08) ## 1.5.4-1 (2026-01-08)
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/addons_config/bazarr to a folder only accessible from my Filebrowser addon called /addon_configs/xxx-bazarr. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ - ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/addons_config/bazarr to a folder only accessible from my Filebrowser addon called /app_configs/xxx-bazarr. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/
## 1.5.4 (2026-01-08) ## 1.5.4 (2026-01-08)
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases) - Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)

View File

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

View File

@@ -76,7 +76,7 @@ ingress: true
ingress_entry: bazarr ingress_entry: bazarr
init: false init: false
map: map:
- addon_config:rw - app_config:rw
- homeassistant_config:rw - homeassistant_config:rw
- share:rw - share:rw
- media:rw - media:rw

View File

@@ -23,7 +23,7 @@ fi
slug=bazarr slug=bazarr
if [ -d "/homeassistant/addons_config/$slug" ]; then if [ -d "/homeassistant/addons_config/$slug" ]; then
echo "Migrating /homeassistant/addons_config/$slug to /addon_configs/xxx-$slug" echo "Migrating /homeassistant/addons_config/$slug to /app_configs/xxx-$slug"
cp -rnf /homeassistant/addons_config/"$slug"/. /config/ || true cp -rnf /homeassistant/addons_config/"$slug"/. /config/ || true
mv /homeassistant/addons_config/"$slug" /homeassistant/addons_config/"$slug"_migrated mv /homeassistant/addons_config/"$slug" /homeassistant/addons_config/"$slug"_migrated
fi fi

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

View File

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

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

View File

@@ -1,3 +1,29 @@
## 20260910.2 (11-09-2026)
- Minor bugs fixed
## 20260910 (11-09-2026)
- Migrate legacy add-on configuration map names to current app configuration terminology.
## 20260909.7 (10-09-2026)
- Correct the "first daily detection consensus" README section (review feedback on PR #3056): a merged-upstream setting stays in the build rather than disappearing, every attempt for a species is held back until one is accepted rather than only the first, and the "known to every active bird model" exemption is scoped per audio source, matching the implementation.
## 20260909.6 (10-09-2026)
- Document the fork-only "first daily detection consensus" setting (alexbelgium/birdnet-go#63): requires a second model to confirm each bird species' first detection of the day. Off by default; no behaviour change unless enabled.
## 20260909.5 (09-09-2026)
- Minor bugs fixed
## 20260909.4 (09-09-2026)
- Minor bugs fixed
## 20260909.3 (09-09-2026)
- Minor bugs fixed
## 20260909.2 (09-09-2026)
- Minor bugs fixed
## 20260909 (09-09-2026)
- Minor bugs fixed
## 20260908.2 (08-09-2026)
- Minor bugs fixed
## 20260908.1 (08-09-2026)
- Synced with upstream birdnet-go (4 commits); re-merges the open fork PRs, adding fork PR #62 (reanalyze a clip with every loaded model + one-click correction)
## 20260908 (08-09-2026)
- Synced with upstream birdnet-go (7 commits); re-merges the open fork PRs
## 20260907 (07-09-2026)
- Rebuild: re-merges the open fork PRs, picking up the updated fork PR #57
## 20260901.4 (01-09-2026) ## 20260901.4 (01-09-2026)
- Minor bugs fixed - Minor bugs fixed
## 20260901.3 (01-09-2026) ## 20260901.3 (01-09-2026)

View File

@@ -25,7 +25,7 @@ MQTT password : Ri5ahV1aipeiw0aelerooteixai5ohtoeNg6oo3mo0thi5te0phiezuge4Phoore
MQTT broker : tcp://core-mosquitto:1883 MQTT broker : tcp://core-mosquitto:1883
--- ---
Edit this section of config.yaml found in addon_configs/db21ed7f_birdnet-go/: Edit this section of config.yaml found in app_configs/db21ed7f_birdnet-go/:
mqtt: mqtt:
enabled: true # true to enable MQTT enabled: true # true to enable MQTT
broker: tcp://core-mosquitto:1883 # MQTT (tcp://host:port) broker: tcp://core-mosquitto:1883 # MQTT (tcp://host:port)

View File

@@ -1,6 +1,6 @@
# Home assistant add-on: Birdnet-Go (from source) # Home assistant add-on: Birdnet-Go (from source)
> **⚠️ Test build.** This is a special variant of the [standard birdnet-go add-on](https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go). Instead of pulling the prebuilt `ghcr.io/tphakala/birdnet-go` image, it **compiles BirdNET-Go from the [`alexbelgium/birdnet-go`](https://github.com/alexbelgium/birdnet-go) fork**. At build time it syncs the fork's `main` with the `tphakala/birdnet-go` upstream and **merges every open non-draft ("in review") pull request on the fly** (see [`merge-prs.sh`](./merge-prs.sh)), so the binary reflects upstream main plus all work currently under review. Everything below is identical to the standard add-on. > **⚠️ Test build.** This is a special variant of the [standard birdnet-go add-on](https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go). Instead of pulling the prebuilt `ghcr.io/tphakala/birdnet-go` image, it **compiles BirdNET-Go from the [`alexbelgium/birdnet-go`](https://github.com/alexbelgium/birdnet-go) fork**. At build time it syncs the fork's `main` with the `tphakala/birdnet-go` upstream and **merges every open non-draft ("in review") pull request on the fly** (see [`merge-prs.sh`](./merge-prs.sh)), so the binary reflects upstream main plus all work currently under review. Everything below is identical to the standard add-on, except the fork-only settings listed under [Fork-only settings](#fork-only-settings).
@@ -65,6 +65,38 @@ Additional variables can be configured using the config.yaml file found in /conf
- Config_env.yaml - Config_env.yaml
Additional environment variables can be configured there Additional environment variables can be configured there
### Fork-only settings
These are currently fork-only settings from the [`alexbelgium/birdnet-go`](https://github.com/alexbelgium/birdnet-go) fork and are **not** in the standard add-on. [`merge-prs.sh`](./merge-prs.sh) syncs the fork's `main` with upstream before applying open PRs, so once a PR merges upstream the setting stays in this build — it just arrives via that sync instead of the PR-merge step, and stops being fork-only. Only a PR that is **closed without merging** drops its setting from later builds.
#### First daily detection consensus
Requires a second model to confirm the **first** detection of each bird species each day. Until one is accepted, every attempt for that species is held to the same two-model bar; only once a detection clears it does every later detection that day behave exactly as it does today, on a single model.
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`:
```yaml
realtime:
firstdailyconsensus:
enabled: true
```
The setting is re-read on each detection cycle, so it takes effect without restarting the add-on.
It deliberately does **nothing** in these cases, all of which keep today's single-model behaviour:
- 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
- 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
In practice it only bites when a single audio source has two or more bird models (for example BirdNET plus Perch) analyzing it, on species all of them can identify. The trade is fewer false new-species entries, at the cost of occasionally delaying a genuine first sighting until a second model agrees.
Requires [alexbelgium/birdnet-go#63](https://github.com/alexbelgium/birdnet-go/pull/63).
### MQTT and MariaDB auto-configuration (opt-in) ### MQTT and MariaDB auto-configuration (opt-in)
If the Home Assistant **MQTT** addon is installed and running and you set `mqtt_auto_config: true` in the addon options, the addon writes the HA Mosquitto credentials directly into BirdNET-Go's `config.yaml` on every startup: `realtime.mqtt.enabled`, `broker`, `username`, and `password` are populated, and the topic defaults to `birdnet`. In addition, it enables BirdNET-Go's **native Home Assistant MQTT auto-discovery** (`realtime.mqtt.homeassistant.enabled`), so the detection sensors show up in Home Assistant automatically — **no manual MQTT sensor YAML required** (the hand-written sensors in [HAINTEGRATION.md](./HAINTEGRATION.md) remain available if you prefer to build your own). Messages are also retained (`realtime.mqtt.retain: true`) so sensor states survive Home Assistant restarts. When the option is `false` (the default), the addon still logs the broker details and reminds you about the option whenever Mosquitto is detected — nothing is written. If the Home Assistant **MQTT** addon is installed and running and you set `mqtt_auto_config: true` in the addon options, the addon writes the HA Mosquitto credentials directly into BirdNET-Go's `config.yaml` on every startup: `realtime.mqtt.enabled`, `broker`, `username`, and `password` are populated, and the topic defaults to `birdnet`. In addition, it enables BirdNET-Go's **native Home Assistant MQTT auto-discovery** (`realtime.mqtt.homeassistant.enabled`), so the detection sensors show up in Home Assistant automatically — **no manual MQTT sensor YAML required** (the hand-written sensors in [HAINTEGRATION.md](./HAINTEGRATION.md) remain available if you prefer to build your own). Messages are also retained (`realtime.mqtt.retain: true`) so sensor states survive Home Assistant restarts. When the option is `false` (the default), the addon still logs the broker details and reminds you about the option whenever Mosquitto is detected — nothing is written.
@@ -82,7 +114,7 @@ This addon supports mounting both local drives and remote SMB shares:
### Custom Scripts and Environment Variables ### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping: This addon supports custom scripts and environment variables through the `app_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons) - **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details. - **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.

View File

@@ -80,7 +80,7 @@ ingress_entry: "ui/dashboard"
ingress_stream: true ingress_stream: true
init: false init: false
map: map:
- addon_config:rw - app_config:rw
- media:rw - media:rw
- share:rw - share:rw
name: Birdnet-go (customized and built from source) name: Birdnet-go (customized and built from source)
@@ -127,5 +127,5 @@ slug: birdnet-go-dev
udev: true udev: true
url: https://github.com/alexbelgium/hassio-addons url: https://github.com/alexbelgium/hassio-addons
usb: true usb: true
version: "20260901.4" version: "20260910.2"
video: true video: true

View File

@@ -11,6 +11,15 @@
# stamping) is written to the directory given as $1 so the Docker build can # stamping) is written to the directory given as $1 so the Docker build can
# compile it. # compile it.
# #
# With --check the script does not build anything: it performs the exact same
# merge sequence, skips (instead of failing on) every conflicting PR, and prints
# one "!!! CONFLICT pr=#N conflicts-with=... files=... " line per offender before
# exiting 2. Use it to find conflicts *before* a build burns on them. This is a
# different question from GitHub's `mergeable` field, which compares a PR against
# its own base ref - for a stacked PR that base is another feature branch (often
# stale, sometimes belonging to a closed PR), so GitHub can report CLEAN for a PR
# that does not merge onto main at all.
#
# Environment: # Environment:
# BIRDNET_FORK owner/repo of the fork (default alexbelgium/birdnet-go) # BIRDNET_FORK owner/repo of the fork (default alexbelgium/birdnet-go)
# BIRDNET_UPSTREAM owner/repo of the upstream (default tphakala/birdnet-go) # BIRDNET_UPSTREAM owner/repo of the upstream (default tphakala/birdnet-go)
@@ -19,7 +28,26 @@
# #
set -euo pipefail set -euo pipefail
TARGET_DIR="${1:?usage: merge-prs.sh <target-dir>}" CHECK_ONLY="${MERGE_PRS_CHECK:-0}"
TARGET_DIR=""
while [ "$#" -gt 0 ]; do
case "$1" in
--check) CHECK_ONLY=1 ;;
-*) 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
# quietly change which operand counts.
if [ -n "${TARGET_DIR}" ]; then
echo "usage: merge-prs.sh [--check] <target-dir>" >&2
exit 64
fi
TARGET_DIR="$1"
;;
esac
shift
done
: "${TARGET_DIR:?usage: merge-prs.sh [--check] <target-dir>}"
FORK="${BIRDNET_FORK:-alexbelgium/birdnet-go}" FORK="${BIRDNET_FORK:-alexbelgium/birdnet-go}"
UPSTREAM="${BIRDNET_UPSTREAM:-tphakala/birdnet-go}" UPSTREAM="${BIRDNET_UPSTREAM:-tphakala/birdnet-go}"
@@ -33,6 +61,52 @@ GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
log() { echo ">>> $*"; } log() { echo ">>> $*"; }
# Conflicting PRs collected in --check mode: "number|scope|files|title".
conflicting=()
LOCKFILE="frontend/package-lock.json"
# The single place that decides whether a conflicted merge is still acceptable.
# package-lock.json is generated content and stacked PRs can carry an older copy even when
# their source changes merge cleanly, so keep the lockfile already assembled on the base side
# — but only when it is the sole conflict. Any source conflict stays fatal.
# Returns 0 when it resolved and committed such a merge, 1 when the conflict is real.
# BOTH the real merge and the --check probe must go through here: when only the real merge
# applied the policy, the probe called a PR "conflicts-with=main" that the build would have
# merged fine, and printed the opposite remediation to the true one.
resolve_sole_lockfile() {
local dir="$1"
local -a conflicted
mapfile -t conflicted < <(git -C "${dir}" diff --name-only --diff-filter=U)
if [ "${#conflicted[@]}" -ne 1 ] || [ "${conflicted[0]}" != "${LOCKFILE}" ]; then
return 1
fi
log "Resolving generated ${LOCKFILE} conflict using the base tree"
git -C "${dir}" checkout --ours -- "${LOCKFILE}" || return 1
git -C "${dir}" add "${LOCKFILE}" || return 1
git -C "${dir}" commit --no-edit > /dev/null || return 1
}
# Does ${1} merge cleanly onto the pristine upstream-synced main? Probed in a
# throwaway worktree so the accumulated tree is left untouched. Tells apart a PR
# that is simply stale against main (fixable inside that PR's own branch) from
# one that only clashes with another open PR (needs a cross-PR decision).
merges_onto_main() {
local sha="$1" tmpdir probe rc=0
tmpdir="$(mktemp -d)"
probe="${tmpdir}/probe"
git worktree add --quiet --detach "${probe}" "${MAIN_SYNCED}"
if ! git -C "${probe}" merge --no-edit --no-ff -m probe "${sha}" > /dev/null 2>&1; then
# Same policy as the real merge, or this misclassifies a lockfile-only clash.
resolve_sole_lockfile "${probe}" > /dev/null 2>&1 || rc=1
fi
git worktree remove --force "${probe}" > /dev/null 2>&1 || true
# worktree remove only takes the child back; without this the mktemp parent is left behind
# on every checked conflict.
rmdir "${tmpdir}" > /dev/null 2>&1 || true
return "${rc}"
}
git config --global user.email "addon-builder@users.noreply.github.com" git config --global user.email "addon-builder@users.noreply.github.com"
git config --global user.name "BirdNET-Go Addon Builder" git config --global user.name "BirdNET-Go Addon Builder"
git config --global advice.detachedHead false git config --global advice.detachedHead false
@@ -47,6 +121,7 @@ git remote add upstream "${UPSTREAM_URL}"
git fetch --no-tags upstream main git fetch --no-tags upstream main
# --no-ff keeps an explicit sync commit; a no-op when main is already current. # --no-ff keeps an explicit sync commit; a no-op when main is already current.
git merge --no-edit --no-ff upstream/main git merge --no-edit --no-ff upstream/main
MAIN_SYNCED="$(git rev-parse HEAD)"
log "Querying open non-draft PRs from ${FORK}" log "Querying open non-draft PRs from ${FORK}"
auth_header=() auth_header=()
@@ -79,27 +154,43 @@ for entry in "${prs[@]}"; do
if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then
mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U) mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U)
# package-lock.json is generated content and stacked PRs can carry an if resolve_sole_lockfile .; then
# older copy even when their source changes merge cleanly. Keep the : # generated lockfile only - the merge is committed and the build continues
# lockfile already assembled from upstream and earlier PRs, but only
# when it is the sole conflict. Any source conflict remains fatal.
if [ "${#conflicted_files[@]}" -eq 1 ] \
&& [ "${conflicted_files[0]}" = "frontend/package-lock.json" ]; then
log "Resolving generated frontend/package-lock.json conflict using the accumulated tree"
git checkout --ours -- frontend/package-lock.json
git add frontend/package-lock.json
git commit --no-edit
else else
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2 echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
if [ "${#conflicted_files[@]}" -gt 0 ]; then if [ "${#conflicted_files[@]}" -gt 0 ]; then
printf '!!! Conflicting file: %s\n' "${conflicted_files[@]}" >&2 printf '!!! Conflicting file: %s\n' "${conflicted_files[@]}" >&2
fi fi
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
git merge --abort || true git merge --abort || true
if [ "${CHECK_ONLY}" = "1" ]; then
scope="accumulated"
merges_onto_main "${sha}" || scope="main"
conflicting+=("${number}|${scope}|${conflicted_files[*]:-}|${title}")
log "check mode: skipping PR #${number}, continuing with the rest"
continue
fi
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
exit 1 exit 1
fi fi
fi fi
done done
if [ "${CHECK_ONLY}" = "1" ]; then
if [ "${#conflicting[@]}" -eq 0 ]; then
log "CHECK OK: every open non-draft PR merges into the combined build tree"
exit 0
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}"
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
echo "!!! conflicts-with=accumulated -> the PR only clashes with another open PR; decide which one owns the hunk." >&2
exit 2
fi
log "Merged HEAD: $(git rev-parse --short HEAD)" log "Merged HEAD: $(git rev-parse --short HEAD)"
log "Source tree ready at ${TARGET_DIR}" log "Source tree ready at ${TARGET_DIR}"

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ MQTT password : Ri5ahV1aipeiw0aelerooteixai5ohtoeNg6oo3mo0thi5te0phiezuge4Phoore
MQTT broker : tcp://core-mosquitto:1883 MQTT broker : tcp://core-mosquitto:1883
--- ---
Edit this section of config.yaml found in addon_configs/db21ed7f_birdnet-go/: Edit this section of config.yaml found in app_configs/db21ed7f_birdnet-go/:
mqtt: mqtt:
enabled: true # true to enable MQTT enabled: true # true to enable MQTT
broker: tcp://core-mosquitto:1883 # MQTT (tcp://host:port) broker: tcp://core-mosquitto:1883 # MQTT (tcp://host:port)

View File

@@ -79,7 +79,7 @@ This addon supports mounting both local drives and remote SMB shares:
### Custom Scripts and Environment Variables ### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping: This addon supports custom scripts and environment variables through the `app_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons) - **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details. - **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.

View File

@@ -81,7 +81,7 @@ ingress_entry: "ui/dashboard"
ingress_stream: true ingress_stream: true
init: false init: false
map: map:
- addon_config:rw - app_config:rw
- media:rw - media:rw
- share:rw - share:rw
name: Birdnet-go name: Birdnet-go

View File

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

View File

@@ -1,3 +1,4 @@
- Migrate legacy add-on configuration map names to current app configuration terminology.
## 2026.08.15 (15-08-2026) ## 2026.08.15 (15-08-2026)
- Fix: `ALSA_CARD` now really selects the microphone. Its value was copied as-is into `REC_CARD`, but BirdNET-Pi hands `REC_CARD` to `arecord -D` / `ffmpeg -f alsa -i`, which expect an ALSA PCM name: a card index such as `1` gave `Unknown PCM 1` and no recording at all. It is now converted to `plughw:CARD=<value>,DEV=0`, while a value that already is a PCM name (`dsnoop:CARD=Audio,DEV=0`, `default`, `null`, `pulse`, `pipewire`, ...) is used as provided - Fix: `ALSA_CARD` now really selects the microphone. Its value was copied as-is into `REC_CARD`, but BirdNET-Pi hands `REC_CARD` to `arecord -D` / `ffmpeg -f alsa -i`, which expect an ALSA PCM name: a card index such as `1` gave `Unknown PCM 1` and no recording at all. It is now converted to `plughw:CARD=<value>,DEV=0`, while a value that already is a PCM name (`dsnoop:CARD=Audio,DEV=0`, `default`, `null`, `pulse`, `pipewire`, ...) is used as provided

View File

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

View File

@@ -93,7 +93,7 @@ This addon supports mounting both local drives and remote SMB shares:
### Custom Scripts and Environment Variables ### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping: This addon supports custom scripts and environment variables through the `app_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons) - **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details. - **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.

View File

@@ -61,7 +61,7 @@ image: ghcr.io/alexbelgium/birdnet-pi-zach-{arch}
ingress: true ingress: true
init: false init: false
map: map:
- addon_config:rw - app_config:rw
- media:rw - media:rw
- share:rw - share:rw
- ssl - ssl

View File

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

View File

@@ -1,3 +1,4 @@
- Migrate legacy add-on configuration map names to current app configuration terminology.
## 2026.08.15 (15-08-2026) ## 2026.08.15 (15-08-2026)
- Fix: `ALSA_CARD` now really selects the microphone. Its value was copied as-is into `REC_CARD`, but BirdNET-Pi hands `REC_CARD` to `arecord -D` / `ffmpeg -f alsa -i`, which expect an ALSA PCM name: a card index such as `1` gave `Unknown PCM 1` and no recording at all. It is now converted to `plughw:CARD=<value>,DEV=0`, while a value that already is a PCM name (`dsnoop:CARD=Audio,DEV=0`, `default`, `null`, `pulse`, `pipewire`, ...) is used as provided - Fix: `ALSA_CARD` now really selects the microphone. Its value was copied as-is into `REC_CARD`, but BirdNET-Pi hands `REC_CARD` to `arecord -D` / `ffmpeg -f alsa -i`, which expect an ALSA PCM name: a card index such as `1` gave `Unknown PCM 1` and no recording at all. It is now converted to `plughw:CARD=<value>,DEV=0`, while a value that already is a PCM name (`dsnoop:CARD=Audio,DEV=0`, `default`, `null`, `pulse`, `pipewire`, ...) is used as provided
@@ -156,7 +157,7 @@
- [ALL] Fix non-avx2 cpu support - [ALL] Fix non-avx2 cpu support
## 2025.02.23 (2025-02-16) ## 2025.02.23 (2025-02-16)
- WARNING 2025.02.14/16 was buggy. If you installed it you need to restore a backup or delete manually your /addon_configs/xxx-birdnet-pi/birdnet.conf file and recreate it - WARNING 2025.02.14/16 was buggy. If you installed it you need to restore a backup or delete manually your /app_configs/xxx-birdnet-pi/birdnet.conf file and recreate it
- Allow usage as a standalone container (thanks @gotschi) https://github.com/mcguirepr89/BirdNET-Pi/issues/211#issuecomment-2650095952 - Allow usage as a standalone container (thanks @gotschi) https://github.com/mcguirepr89/BirdNET-Pi/issues/211#issuecomment-2650095952
- Corrected a bug preventing to create db - Corrected a bug preventing to create db
- Corrected a bug to ensure the the most up-to-date birdnet.conf on fresh start - Corrected a bug to ensure the the most up-to-date birdnet.conf on fresh start

View File

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

View File

@@ -93,7 +93,7 @@ This addon supports mounting both local drives and remote SMB shares:
### Custom Scripts and Environment Variables ### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping: This addon supports custom scripts and environment variables through the `app_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons) - **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details. - **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.

View File

@@ -61,7 +61,7 @@ image: ghcr.io/alexbelgium/birdnet-pi-{arch}
ingress: true ingress: true
init: false init: false
map: map:
- addon_config:rw - app_config:rw
- media:rw - media:rw
- share:rw - share:rw
- ssl - ssl

View File

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

View File

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

View File

@@ -47,7 +47,7 @@ This add-on supports mounting both local drives and remote SMB shares:
### Custom Scripts and Environment Variables ### Custom Scripts and Environment Variables
This add-on supports custom scripts and environment variables through the `addon_config` mapping: This add-on supports custom scripts and environment variables through the `app_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons) - **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details. - **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.

View File

@@ -67,13 +67,13 @@ ingress: true
ingress_stream: true ingress_stream: true
init: false init: false
map: map:
- addon_config:rw - app_config:rw
- ssl:rw - ssl:rw
- share:rw - share:rw
name: BirdNET-PiPy name: BirdNET-PiPy
options: options:
env_vars: [] env_vars: []
ICECAST_PASSWORD: "" ICECAST_PASSWORD: ''
data_location: /config/data data_location: /config/data
panel_icon: mdi:bird panel_icon: mdi:bird
ports: ports:

View File

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

View File

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

View File

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

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

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

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

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ image: ghcr.io/alexbelgium/brave-{arch}
ingress: true ingress: true
init: false init: false
map: map:
- addon_config:rw - app_config:rw
- media:rw - media:rw
- share:rw - share:rw
- ssl - ssl

View File

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

View File

@@ -26,7 +26,7 @@ image: ghcr.io/alexbelgium/chromium-{arch}
ingress: true ingress: true
init: false init: false
map: map:
- addon_config:rw - app_config:rw
- media:rw - media:rw
- share:rw - share:rw
- ssl - ssl

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

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

View File

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

View File

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

View File

@@ -50,7 +50,7 @@ TIMEOUT: 60000
### Custom Scripts and Environment Variables ### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping: This addon supports custom scripts and environment variables through the `app_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons) - **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details. - **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.

View File

@@ -69,7 +69,7 @@ environment:
image: ghcr.io/alexbelgium/browserless_chrome-{arch} image: ghcr.io/alexbelgium/browserless_chrome-{arch}
init: false init: false
map: map:
- addon_config:rw - app_config:rw
name: Browserless Chromium name: Browserless Chromium
options: options:
env_vars: [] env_vars: []

View File

@@ -1,16 +1,17 @@
- Migrate legacy add-on configuration map names to current app configuration terminology.
## 9.14.0 (2026-08-29) ## 9.14.0 (2026-08-29)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases) - Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.13.0 (2026-08-08) ## 9.13.0 (2026-08-08)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases) - Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.12.0 (2026-08-01) ## 9.12.0 (2026-08-01)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases) - Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.11.0 (2026-07-04) ## 9.11.0 (2026-07-04)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases) - Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.10.0 (2026-06-27) ## 9.10.0 (2026-06-27)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases) - Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
@@ -223,7 +224,7 @@
- Update to latest version from linuxserver/docker-calibre - Update to latest version from linuxserver/docker-calibre
- BREAKING CHANGE : please relink your library according to https://github.com/alexbelgium/hassio-addons/issues/1154#issuecomment-1879182729 - BREAKING CHANGE : please relink your library according to https://github.com/alexbelgium/hassio-addons/issues/1154#issuecomment-1879182729
- ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/calibre to a folder only accessible from my Filebrowser addon called /addon_configs/something-calibre. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ https://github.com/alexbelgium/hassio-addons/issues/1154 - ⚠ MAJOR CHANGE : switch to the new config logic from homeassistant. Your configuration files will have migrated from /config/hassio_addons/calibre to a folder only accessible from my Filebrowser addon called /app_configs/something-calibre. This avoids the addon to mess with your homeassistant configuration folder, and allows to backup the options. Migration of data, custom configs, and custom scripts should be automatic. Please be sure to update all your links however ! For more information, see here : https://developers.home-assistant.io/blog/2023/11/06/public-addon-config/ https://github.com/alexbelgium/hassio-addons/issues/1154
## 7.2.0 (2023-12-16) ## 7.2.0 (2023-12-16)

View File

@@ -62,7 +62,7 @@ Please read the upstream container documentation for further info: https://githu
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
| -------- | ------ | --------- | ------------- | |--------|------|---------|-------------|
| `PGID` | int | `0` | Group ID for file permissions | | `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions | | `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) | | `TZ` | str | | Timezone (e.g., `Europe/London`) |
@@ -98,7 +98,7 @@ This addon supports mounting both local drives and remote SMB shares:
### Custom Scripts and Environment Variables ### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping: This addon supports custom scripts and environment variables through the `app_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons) - **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details. - **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.

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