diff --git a/.github/scripts/weekly-digest.mjs b/.github/scripts/weekly-digest.mjs new file mode 100644 index 00000000..911c43fa --- /dev/null +++ b/.github/scripts/weekly-digest.mjs @@ -0,0 +1,498 @@ +const repository = process.env.GITHUB_REPOSITORY || "CCOSTAN/Home-AssistantConfig"; +const [owner, repo] = repository.split("/"); +const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; +const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com"; +const token = process.env.GITHUB_TOKEN || ""; +const dryRun = process.env.DIGEST_DRY_RUN === "1" || process.argv.includes("--dry-run"); + +if (!owner || !repo) { + throw new Error(`GITHUB_REPOSITORY must be owner/repo, got "${repository}"`); +} + +if (!token && !dryRun) { + throw new Error("GITHUB_TOKEN is required unless DIGEST_DRY_RUN=1 is set."); +} + +const now = process.env.DIGEST_NOW ? new Date(process.env.DIGEST_NOW) : new Date(); +const end = process.env.DIGEST_END ? new Date(process.env.DIGEST_END) : now; +const start = process.env.DIGEST_START + ? new Date(process.env.DIGEST_START) + : new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000); + +for (const [name, date] of [["DIGEST_START", start], ["DIGEST_END", end]]) { + if (Number.isNaN(date.getTime())) { + throw new Error(`${name} is not a valid date.`); + } +} + +const defaultHeaders = { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "home-assistant-config-weekly-digest", +}; + +if (token) { + defaultHeaders.Authorization = `Bearer ${token}`; +} + +function formatDigestDate(date) { + const day = new Intl.DateTimeFormat("en-US", { day: "numeric", timeZone: "UTC" }).format(date); + const month = new Intl.DateTimeFormat("en-US", { month: "long", timeZone: "UTC" }).format(date); + const year = new Intl.DateTimeFormat("en-US", { year: "numeric", timeZone: "UTC" }).format(date); + return `${day} ${month}, ${year}`; +} + +function formatCountVerb(count, singular, plural) { + return count === 1 ? singular : plural; +} + +function searchRange(field) { + return `${field}:${start.toISOString()}..${end.toISOString()}`; +} + +function isWithinWindow(value) { + if (!value) { + return false; + } + + const date = new Date(value); + return !Number.isNaN(date.getTime()) && date >= start && date <= end; +} + +function markdownLink(label, url) { + const safeLabel = String(label || "unknown").replace(/[[\]]/g, "\\$&"); + return `[${safeLabel}](${url})`; +} + +function userLink(user) { + if (!user || !user.login) { + return "unknown"; + } + + return markdownLink(user.login, user.html_url || `${serverUrl}/${user.login}`); +} + +function issueLine(item, icon) { + return `${icon} #${item.number} ${markdownLink(item.title, item.html_url)}, by ${userLink(item.user)}`; +} + +function commitLine(commit) { + const title = commit.commit?.message?.split("\n")[0] || commit.sha; + const author = commit.author + ? userLink(commit.author) + : commit.commit?.author?.name || "unknown"; + return `:hammer_and_wrench: ${markdownLink(title, commit.html_url)} by ${author}`; +} + +function releaseLine(release) { + const label = release.name || release.tag_name; + return `:bookmark: ${markdownLink(label, release.html_url)}`; +} + +function stargazerLine(star) { + return `:star: ${userLink(star.user)}`; +} + +function sectionDivider(lines) { + lines.push(""); + lines.push(" - - - "); +} + +async function githubRequest(method, path, options = {}) { + const query = options.query || {}; + const url = new URL(`${apiUrl}${path}`); + + for (const [key, value] of Object.entries(query)) { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + } + + const response = await fetch(url, { + method, + headers: { + ...defaultHeaders, + ...(options.headers || {}), + }, + body: options.body ? JSON.stringify(options.body) : undefined, + }); + + const text = await response.text(); + const data = text ? JSON.parse(text) : null; + + if (!response.ok) { + const message = data?.message || text || response.statusText; + throw new Error(`${method} ${url.pathname} failed: ${response.status} ${message}`); + } + + return { data, headers: response.headers }; +} + +async function getAllPages(path, options = {}) { + const results = []; + let page = 1; + + while (true) { + const { data } = await githubRequest("GET", path, { + ...options, + query: { + ...(options.query || {}), + per_page: 100, + page, + }, + }); + + if (!Array.isArray(data) || data.length === 0) { + break; + } + + results.push(...data); + + if (data.length < 100) { + break; + } + + page += 1; + } + + return results; +} + +async function searchIssues(query) { + const results = []; + let page = 1; + + while (true) { + const { data } = await githubRequest("GET", "/search/issues", { + query: { + q: query, + sort: "created", + order: "asc", + per_page: 100, + page, + }, + }); + + const items = data.items || []; + results.push(...items); + + if (items.length < 100 || results.length >= data.total_count) { + break; + } + + page += 1; + } + + return results; +} + +function hasLabel(item, labelName) { + return (item.labels || []).some((label) => label.name === labelName); +} + +async function getCreatedIssueItems() { + const items = await searchIssues(`repo:${owner}/${repo} ${searchRange("created")}`); + return items.filter((item) => !hasLabel(item, "weekly-digest")); +} + +async function getPullRequests() { + const updated = await searchIssues(`repo:${owner}/${repo} is:pr ${searchRange("updated")}`); + const merged = await searchIssues(`repo:${owner}/${repo} is:pr ${searchRange("merged")}`); + const byNumber = new Map(); + + for (const item of [...updated, ...merged]) { + byNumber.set(item.number, item); + } + + const pulls = []; + for (const item of byNumber.values()) { + const { data } = await githubRequest("GET", `/repos/${owner}/${repo}/pulls/${item.number}`); + pulls.push({ + ...item, + merged_at: data.merged_at, + }); + } + + return pulls.sort((a, b) => a.number - b.number); +} + +async function getCommits() { + const commits = await getAllPages(`/repos/${owner}/${repo}/commits`, { + query: { + since: start.toISOString(), + until: end.toISOString(), + }, + }); + + return commits.sort((a, b) => new Date(a.commit.author.date) - new Date(b.commit.author.date)); +} + +async function getReleases() { + const releases = await getAllPages(`/repos/${owner}/${repo}/releases`); + return releases + .filter((release) => isWithinWindow(release.published_at || release.created_at)) + .sort((a, b) => new Date(a.published_at || a.created_at) - new Date(b.published_at || b.created_at)); +} + +async function getStargazers() { + const stars = await getAllPages(`/repos/${owner}/${repo}/stargazers`, { + headers: { + Accept: "application/vnd.github.star+json", + }, + }); + + return stars + .filter((star) => isWithinWindow(star.starred_at)) + .sort((a, b) => new Date(a.starred_at) - new Date(b.starred_at)); +} + +function getReactionTotal(item) { + const reactions = item.reactions || {}; + return (reactions["+1"] || 0) + (reactions.smile || 0) + (reactions.tada || 0) + (reactions.heart || 0); +} + +function appendIssues(lines, createdItems) { + const openItems = createdItems.filter((item) => item.state === "open"); + const closedItems = createdItems.filter((item) => item.state === "closed"); + const likedItem = [...createdItems].sort((a, b) => getReactionTotal(b) - getReactionTotal(a))[0]; + const noisyItem = [...createdItems].sort((a, b) => (b.comments || 0) - (a.comments || 0))[0]; + + lines.push("# ISSUES"); + lines.push(`Last week ${createdItems.length} issues were created.`); + lines.push(`Of these, ${closedItems.length} issues have been closed and ${openItems.length} issues are still open.`); + + if (openItems.length > 0) { + lines.push("## OPEN ISSUES"); + lines.push(...openItems.map((item) => issueLine(item, ":green_heart:"))); + } + + if (closedItems.length > 0) { + lines.push("## CLOSED ISSUES"); + lines.push(...closedItems.map((item) => issueLine(item, ":heart:"))); + } + + if (likedItem && getReactionTotal(likedItem) > 0) { + const reactions = likedItem.reactions || {}; + lines.push("## LIKED ISSUE"); + lines.push(issueLine(likedItem, ":+1:")); + lines.push( + `It received :+1: x${reactions["+1"] || 0}, :smile: x${reactions.smile || 0}, :tada: x${ + reactions.tada || 0 + } and :heart: x${reactions.heart || 0}.`, + ); + } + + if (noisyItem && noisyItem.comments > 0) { + lines.push("## NOISY ISSUE"); + lines.push(issueLine(noisyItem, ":speaker:")); + lines.push(`It received ${noisyItem.comments} comments.`); + } +} + +function appendPullRequests(lines, pulls) { + const openPulls = pulls.filter((pull) => pull.state === "open"); + const mergedPulls = pulls.filter((pull) => isWithinWindow(pull.merged_at)); + const closedPulls = pulls.filter((pull) => pull.state === "closed" && !isWithinWindow(pull.merged_at)); + + lines.push("# PULL REQUESTS"); + + if (pulls.length === 0) { + lines.push("Last week, no pull requests were created, updated or merged."); + return; + } + + lines.push(`Last week, ${pulls.length} pull requests were created, updated or merged.`); + + if (openPulls.length > 0) { + lines.push(`## OPEN PULL ${formatCountVerb(openPulls.length, "REQUEST", "REQUESTS")}`); + lines.push(...openPulls.map((item) => issueLine(item, ":green_heart:"))); + } + + if (closedPulls.length > 0) { + lines.push(`## CLOSED PULL ${formatCountVerb(closedPulls.length, "REQUEST", "REQUESTS")}`); + lines.push(...closedPulls.map((item) => issueLine(item, ":heart:"))); + } + + if (mergedPulls.length > 0) { + lines.push(`## MERGED PULL ${formatCountVerb(mergedPulls.length, "REQUEST", "REQUESTS")}`); + lines.push(...mergedPulls.map((item) => issueLine(item, ":purple_heart:"))); + } +} + +function appendCommits(lines, commits) { + lines.push("# COMMITS"); + + if (commits.length === 0) { + lines.push("Last week there were no commits."); + return; + } + + lines.push(`Last week there were ${commits.length} commits.`); + lines.push(...commits.map(commitLine)); +} + +function appendContributors(lines, commits, createdItems, pulls) { + const contributors = new Map(); + + for (const commit of commits) { + if (commit.author?.login) { + contributors.set(commit.author.login, commit.author); + } + } + + if (contributors.size === 0) { + for (const item of [...createdItems, ...pulls]) { + if (item.user?.login) { + contributors.set(item.user.login, item.user); + } + } + } + + lines.push("# CONTRIBUTORS"); + lines.push(`Last week there ${formatCountVerb(contributors.size, "was", "were")} ${contributors.size} contributors.`); + lines.push(...[...contributors.values()].map((user) => `:bust_in_silhouette: ${userLink(user)}`)); +} + +function appendStargazers(lines, stargazers) { + lines.push("# STARGAZERS"); + + if (stargazers.length === 0) { + lines.push("Last week there were no stagazers."); + return; + } + + lines.push(`Last week there were ${stargazers.length} stagazers.`); + lines.push(...stargazers.slice(0, 75).map(stargazerLine)); + + if (stargazers.length > 75) { + lines.push(`...and ${stargazers.length - 75} more.`); + } + + lines.push("You all are the stars! :star2:"); +} + +function appendReleases(lines, releases) { + lines.push("# RELEASES"); + + if (releases.length === 0) { + lines.push("Last week there were no releases."); + return; + } + + lines.push(`Last week there ${formatCountVerb(releases.length, "was", "were")} ${releases.length} releases.`); + lines.push(...releases.map(releaseLine)); +} + +function buildDigestBody({ createdItems, pulls, commits, stargazers, releases }) { + const repoUrl = `${serverUrl}/${owner}/${repo}`; + const lines = [ + `Here's the **Weekly Digest** for [*${owner}/${repo}*](${repoUrl}):`, + ]; + + sectionDivider(lines); + appendIssues(lines, createdItems); + sectionDivider(lines); + appendPullRequests(lines, pulls); + sectionDivider(lines); + appendCommits(lines, commits); + sectionDivider(lines); + appendContributors(lines, commits, createdItems, pulls); + sectionDivider(lines); + appendStargazers(lines, stargazers); + sectionDivider(lines); + appendReleases(lines, releases); + sectionDivider(lines); + lines.push(""); + lines.push( + `That's all for last week, please :eyes: **Watch** and :star: **Star** the repository [*${owner}/${repo}*](${repoUrl}) to receive next weekly updates. :smiley:`, + ); + lines.push(""); + lines.push( + `*You can also [view all Weekly Digests by clicking here](${repoUrl}/issues?q=is:open+is:issue+label:weekly-digest).* `, + ); + lines.push(""); + lines.push(`> Your [**Weekly Digest**](${repoUrl}/actions/workflows/weekly-digest.yml) bot. :calendar:`); + + return lines.join("\n"); +} + +async function getOpenDigestIssues() { + return getAllPages(`/repos/${owner}/${repo}/issues`, { + query: { + state: "open", + labels: "weekly-digest", + }, + }); +} + +async function createDigestIssue(title, body) { + const { data } = await githubRequest("POST", `/repos/${owner}/${repo}/issues`, { + body: { + title, + body, + labels: ["weekly-digest"], + }, + }); + + return data; +} + +async function closeOldDigestIssue(issue, newIssue) { + const labels = new Set((issue.labels || []).map((label) => label.name)); + labels.add("oldnews"); + + await githubRequest("POST", `/repos/${owner}/${repo}/issues/${issue.number}/comments`, { + body: { + body: `A new weekly digest is available: #${newIssue.number}. Closing this older digest so only the latest one stays open.`, + }, + }); + + await githubRequest("PATCH", `/repos/${owner}/${repo}/issues/${issue.number}`, { + body: { + state: "closed", + state_reason: "completed", + }, + }); + + try { + await githubRequest("PATCH", `/repos/${owner}/${repo}/issues/${issue.number}`, { + body: { + labels: [...labels], + }, + }); + } catch (error) { + console.warn(`Could not add oldnews label to #${issue.number}: ${error.message}`); + } +} + +async function main() { + const title = `Weekly Digest (${formatDigestDate(start)} - ${formatDigestDate(end)})`; + const openDigestIssues = await getOpenDigestIssues(); + const [createdItems, pulls, commits, stargazers, releases] = await Promise.all([ + getCreatedIssueItems(), + getPullRequests(), + getCommits(), + getStargazers(), + getReleases(), + ]); + const body = buildDigestBody({ createdItems, pulls, commits, stargazers, releases }); + + if (dryRun) { + console.log(`DRY RUN: would create "${title}"`); + console.log(body); + console.log(`DRY RUN: would close ${openDigestIssues.length} older digest issues.`); + return; + } + + const newIssue = await createDigestIssue(title, body); + const oldIssues = openDigestIssues.filter((issue) => issue.number !== newIssue.number); + + for (const issue of oldIssues) { + await closeOldDigestIssue(issue, newIssue); + } + + console.log(`Created ${newIssue.html_url}`); + console.log(`Closed ${oldIssues.length} older digest issues.`); +} + +await main(); diff --git a/.github/workflows/weekly-digest.yml b/.github/workflows/weekly-digest.yml new file mode 100644 index 00000000..22525f60 --- /dev/null +++ b/.github/workflows/weekly-digest.yml @@ -0,0 +1,31 @@ +name: Weekly Digest + +on: + schedule: + - cron: "37 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + issues: write + pull-requests: read + +jobs: + weekly-digest: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Create weekly digest issue + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + run: node .github/scripts/weekly-digest.mjs diff --git a/README.md b/README.md index 179c83e7..96f554da 100755 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ Live, personal Home Assistant configuration shared for **browsing and inspiration**. This is not a turnkey clone-and-run setup; borrow ideas, adapt entity IDs/secrets, and test in your own environment. +### Latest video: Clean Home Assistant dashboards +[![Build Clean Home Assistant Dashboards with YAML Partials and Templates](https://img.youtube.com/vi/aFis2YPeSuY/maxresdefault.jpg)](https://youtu.be/aFis2YPeSuY) + +This walkthrough shows how the Infrastructure dashboard is organized with a boring `dashboard.yaml`, thin view files, focused partials, and shared button-card templates. [Watch the video](https://youtu.be/aFis2YPeSuY), read the [companion post](https://www.vcloudinfo.com/2026/02/home-assistant-dashboard-design-system-button-card.html), and browse the YAML under [config/dashboards/infrastructure](config/dashboards/infrastructure). + ### Quick navigation - You are here: `/` (root repo guide) - [Blog](https://www.vcloudinfo.com) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) | [Diagram](config/www/custom_ui/floorplan/images/branding/Bear-Stone-Docker-Diagram.jpg) | [YouTube](https://youtube.com/vCloudInfo) diff --git a/codex_skills/README.md b/codex_skills/README.md index 04eb74d8..5343b3ae 100644 --- a/codex_skills/README.md +++ b/codex_skills/README.md @@ -18,6 +18,10 @@ Codex skills stored in-repo so they can be shared with the community. These are documentation + helper scripts only (no secrets). +### Walkthroughs +- Dashboard design skill: [Build Clean Home Assistant Dashboards with YAML Partials and Templates](https://youtu.be/aFis2YPeSuY) +- Companion post: [Home Assistant Dashboard Design System (Button-Card First)](https://www.vcloudinfo.com/2026/02/home-assistant-dashboard-design-system-button-card.html) + ### Quick navigation - You are here: `codex_skills/` - [Repo overview](../README.md) | [Dashboards](../config/dashboards/README.md) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) @@ -26,7 +30,7 @@ Codex skills stored in-repo so they can be shared with the community. These are - `homeassistant-dashboard-designer/`: Constrained, button-card-first Lovelace dashboard design system + YAML lint helper. - `homeassistant-yaml-dry-verifier/`: Home Assistant YAML DRY verifier to detect redundant triggers/conditions/actions/sequence blocks and suggest refactors. -- `infrastructure-doc-sync/`: Session closeout workflow to update AGENTS/README/Dashy shortcuts/Infra Info snapshot consistently after infra changes. +- `infrastructure-doc-sync/`: Session closeout workflow to update AGENTS/README/Dashy shortcuts/BearClaw infrastructure snapshot consistently after infra changes. - `network-architecture-diagrammer/`: Mermaid-first homelab/network architecture diagram workflow for Excalidraw-friendly topology and service maps. ### Notes diff --git a/codex_skills/homeassistant-dashboard-designer/README.md b/codex_skills/homeassistant-dashboard-designer/README.md index 2f2fd223..a1af47e4 100644 --- a/codex_skills/homeassistant-dashboard-designer/README.md +++ b/codex_skills/homeassistant-dashboard-designer/README.md @@ -20,6 +20,10 @@ This directory contains the `homeassistant-dashboard-designer` Codex skill, stored in-repo so it can be shared with the community. +### Walkthrough +- Video: [Build Clean Home Assistant Dashboards with YAML Partials and Templates](https://youtu.be/aFis2YPeSuY) +- Companion post: [Home Assistant Dashboard Design System (Button-Card First)](https://www.vcloudinfo.com/2026/02/home-assistant-dashboard-design-system-button-card.html) + ### Quick navigation - You are here: `codex_skills/homeassistant-dashboard-designer/` - [Repo overview](../../README.md) | [Codex skills](../README.md) | [Dashboards](../../config/dashboards/README.md) | [Issues](https://github.com/CCOSTAN/Home-AssistantConfig/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) diff --git a/codex_skills/homeassistant-yaml-dry-verifier/README.md b/codex_skills/homeassistant-yaml-dry-verifier/README.md index cef39c0d..25355101 100644 --- a/codex_skills/homeassistant-yaml-dry-verifier/README.md +++ b/codex_skills/homeassistant-yaml-dry-verifier/README.md @@ -31,6 +31,7 @@ This directory contains the `homeassistant-yaml-dry-verifier` skill and the CLI - Detects duplicate entries within a single block (`INTRA`). - Detects package-defined scripts called from multiple files (`CENTRAL_SCRIPT`). - Collapses noisy ENTRY reports when they are already fully explained by an identical `FULL_BLOCK` finding. +- Adds workflow guardrails for automation refactors that rename/remove entity references or introduce cleanup behavior: stale-reference checks, dry-run/preview expectations, explicit confirmation, and audit/backup output. ## CLI Usage @@ -70,6 +71,8 @@ Exit codes: - This verifier intentionally keeps text output and a small CLI surface. - It does not implement suppression files, severity scoring, JSON output, or diff-only mode. +- It is not an orphan entity cleaner and should not delete Home Assistant registry entries during normal DRY runs. +- Treat generic `unavailable`, disabled, or no-`config_entry_id` entities as audit signals only; YAML, helper, REST, command-line, MQTT, finance, YouTube, and local infrastructure telemetry can be intentional. - Use it as a fast pre-refactor signal and pair with Home Assistant config validation before restart/reload. **All of my configuration files are tested against the most stable version of home-assistant.** diff --git a/codex_skills/homeassistant-yaml-dry-verifier/SKILL.md b/codex_skills/homeassistant-yaml-dry-verifier/SKILL.md index 28ca6399..a5fed600 100644 --- a/codex_skills/homeassistant-yaml-dry-verifier/SKILL.md +++ b/codex_skills/homeassistant-yaml-dry-verifier/SKILL.md @@ -1,6 +1,6 @@ --- name: homeassistant-yaml-dry-verifier -description: "Verify Home Assistant YAML for DRY and efficiency issues by detecting redundant trigger/condition/action/sequence structures and repeated blocks across automations, scripts, and packages. Use when creating, reviewing, or refactoring YAML in config/packages, config/automations, config/scripts, or dashboard-related YAML where duplication risk is high." +description: "Verify Home Assistant YAML for DRY and efficiency issues by detecting redundant trigger/condition/action/sequence structures and repeated blocks across automations, scripts, and packages. Use when creating, reviewing, or refactoring YAML in config/packages, config/automations, config/scripts, or dashboard-related YAML where duplication risk is high. Include a read-only entity/reference safety pass when automation changes rename/remove entities or introduce maintenance cleanup behavior." --- # Home Assistant YAML DRY Verifier @@ -13,13 +13,15 @@ Use this skill to lint Home Assistant YAML for repeat logic before or after edit - Resolve the findings in the same task by refactoring YAML to remove duplication. - Re-run the verifier after refactoring and iterate until targeted findings are cleared. - If a finding cannot be safely resolved, explicitly document the blocker and the smallest safe follow-up. +- If touched YAML performs cleanup, registry hygiene, purge, deletion, or other destructive maintenance, require preview/dry-run behavior, explicit confirmation, and audit/backup output before any destructive action is considered complete. ## Quick Start 1. Run the verifier script on the file(s) you edited. 2. Review repeated block findings first (highest confidence). 3. Refactor into shared scripts/helpers/templates where appropriate. -4. Re-run the verifier and then run your normal Home Assistant config check. +4. If the change renames/removes entity references or adds maintenance cleanup behavior, perform the read-only entity/reference safety pass. +5. Re-run the verifier and then run your normal Home Assistant config check. ```bash python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py config/packages/life360.yaml --strict @@ -53,15 +55,25 @@ python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.p - Repeated triggers: consolidate where behavior is equivalent, or split by intent if readability improves. - For cooldown/throttle behavior, prefer automation-local `this.attributes.last_triggered` with custom event handoff before adding new helper entities, unless shared persistent state is required across automations. -5. Validate after edits: +5. Entity/Registry Safety Pass: +- Keep this pass read-only during normal DRY work. Report possible stale references or risky cleanup behavior; do not delete Home Assistant registry entries as part of this skill. +- When refactors rename, remove, or consolidate automations, scripts, helpers, entities, service calls, or dashboard targets, search touched and adjacent YAML for stale references before closing the task. +- Use live Home Assistant context or registry/state evidence when available and in scope, especially before changing entity IDs or automations that depend on device/integration state. +- Do not treat generic `unavailable`, disabled, or no-`config_entry_id` entities as safe deletion candidates. In YAML-heavy setups these are often intentional. +- Treat these platforms as common false-positive sources unless stronger evidence proves otherwise: `automation`, `script`, `scene`, `template`, helpers (`input_*`, `group`, `timer`, `counter`, `schedule`, `zone`, `person`, `tag`), `command_line`, `rest`, `mqtt`, `yahoofinance`, `youtube`, and local infrastructure telemetry. +- For cleanup or maintenance automations, prefer a preview/report action first, persistent ignore rules where repeated noise is expected, and an audit artifact that records what would change or did change. +- Destructive cleanup must be gated by explicit operator confirmation and should have backup/audit output. A dry-run-only recommendation is acceptable when the evidence is not strong enough. + +6. Validate after edits: - Re-run this verifier. - Run Home Assistant config validation before reload/restart. -6. Enforce closure: +7. Enforce closure: - Treat unresolved `FULL_BLOCK`/`ENTRY` findings in touched files as incomplete work unless a blocker is documented. - Prefer consolidating duplicated automation triggers/conditions/actions into shared logic or a single branching automation. - Treat unresolved `CENTRAL_SCRIPT` findings in touched scope as incomplete unless documented as deferred-with-blocker. - Move shared package scripts to `config/script/.yaml` when they are used cross-file. +- Treat unresolved stale-reference or cleanup-safety concerns in touched scope as incomplete unless documented as `deferred-with-blocker`. ## Dashboard Designer Integration @@ -77,6 +89,8 @@ Always report: - Script caller detection should include direct `service: script.` and `script.turn_on`-style entity targeting when present. - Concrete refactor recommendation per group. - Resolution status for each finding (`resolved`, `deferred-with-blocker`). +- Entity/reference hygiene status when this pass is in scope (`checked`, `not-in-scope`, or `deferred-with-blocker`). +- For cleanup or destructive maintenance YAML, preview/dry-run, confirmation, and audit/backup status. Strict behavior: - `--strict` returns non-zero for any reported finding (`FULL_BLOCK`, `ENTRY`, `INTRA`, `CENTRAL_SCRIPT`). diff --git a/codex_skills/infrastructure-doc-sync/SKILL.md b/codex_skills/infrastructure-doc-sync/SKILL.md index f0f4bf63..c74d971f 100644 --- a/codex_skills/infrastructure-doc-sync/SKILL.md +++ b/codex_skills/infrastructure-doc-sync/SKILL.md @@ -1,6 +1,6 @@ --- name: infrastructure-doc-sync -description: "Use when infra/container placement changes require synchronized AGENTS, docs, and Infra Info updates while keeping AGENTS concise, scoped, and non-runbook." +description: "Use when infra/container placement changes require synchronized AGENTS, docs, Dashy, and BearClaw infrastructure snapshot updates while keeping AGENTS concise, scoped, and non-runbook." --- # Infrastructure Doc Sync @@ -31,8 +31,8 @@ Keep `AGENTS.md` short and task-scoped; move long runbooks to dedicated docs. 5. Dashy shortcuts (if any service URL/host changed): - `h:\hass\docker_files\dashy/conf.yml` - Reload Dashy on docker_17 after edits: `ssh hass@192.168.10.17 "cd ~/docker_files && docker compose up -d dashy"` -6. Infra Info snapshot JSON: - - `docker_69:/home/hass/docker_files/infra_info/data/overview.json` +6. BearClaw infrastructure snapshot: + - `docker_17/codex_appliance` environment map and `/api/admin/infrastructure` ## Workflow @@ -42,12 +42,13 @@ Keep `AGENTS.md` short and task-scoped; move long runbooks to dedicated docs. 4. Move long operational/runbook details out of `AGENTS.md` into a dedicated doc when needed. 5. If end-user entry points changed, update Dashy shortcuts (`dashy/conf.yml`) to match reality. 6. Update README/skill docs impacted by the change (short, factual, no drift). -7. Update `overview.json` to mirror the same outcome at a high level. +7. Refresh/check BearClaw infrastructure context so it mirrors the same outcome at a high level. 8. Validate: - JSON is valid (`python -m json.tool` equivalent). - Dashy `conf.yml` references the intended hostname(s)/ports (no stale LAN IPs unless intentionally required). - AGENTS and README statements do not conflict with runtime. - Repo-level AGENTS do not contain long runbooks duplicated from dedicated docs. + - BearClaw `/api/admin/infrastructure` returns the intended topology/context. ## AGENTS Quality Rules @@ -57,9 +58,9 @@ Keep `AGENTS.md` short and task-scoped; move long runbooks to dedicated docs. - Keep specialized/deeper-scoped AGENTS concise and task-specific. - De-duplicate repeated policy lines across global/workspace/repo scopes. -## Infra Info Content Rules +## BearClaw Snapshot Content Rules -- Keep `overview.json` high-level and planning-focused. +- Keep infrastructure snapshot content high-level and planning-focused. - Do not include secrets, tokens, passwords, or internal file paths. - Avoid step-by-step runbooks. - Prefer host IDs and roles over low-level implementation detail. @@ -79,5 +80,6 @@ Always report: - Final intended topology/placement. - Any Dashy shortcuts touched (or explicitly state "no Dashy updates needed"). - Whether runbook content was moved from AGENTS into a dedicated ops doc. +- BearClaw infrastructure snapshot validation result. - Any unresolved follow-up items. diff --git a/codex_skills/infrastructure-doc-sync/agents/openai.yaml b/codex_skills/infrastructure-doc-sync/agents/openai.yaml index 2029468b..8c06937a 100644 --- a/codex_skills/infrastructure-doc-sync/agents/openai.yaml +++ b/codex_skills/infrastructure-doc-sync/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Infrastructure Doc Sync" short_description: "Sync infra docs with concise AGENTS" - default_prompt: "Use $infrastructure-doc-sync to keep AGENTS concise and scoped, move runbook content into dedicated docs when needed, and sync README/Dashy/infra_info overview.json after infra changes." + default_prompt: "Use $infrastructure-doc-sync to keep AGENTS concise and scoped, move runbook content into dedicated docs when needed, and sync README/Dashy/BearClaw infrastructure snapshot context after infra changes." diff --git a/codex_skills/network-architecture-diagrammer/references/excalidraw_mermaid_rules.md b/codex_skills/network-architecture-diagrammer/references/excalidraw_mermaid_rules.md index 344ba187..933b9524 100644 --- a/codex_skills/network-architecture-diagrammer/references/excalidraw_mermaid_rules.md +++ b/codex_skills/network-architecture-diagrammer/references/excalidraw_mermaid_rules.md @@ -50,11 +50,11 @@ flowchart TD end subgraph docker69[docker69] Tunnel[Cloudflared] - Info[Infra Info] + PublicApps[Public Apps] end Tunnel --> Appliance - Tunnel --> Info + Tunnel --> PublicApps HA --> MQTT Frigate --> HA ``` diff --git a/config/.HA_VERSION b/config/.HA_VERSION index ee73ae9f..a495d645 100755 --- a/config/.HA_VERSION +++ b/config/.HA_VERSION @@ -1 +1,2 @@ -2026.5.3 \ No newline at end of file +2026.5.3 +2026.5.2 diff --git a/config/.cache/brands/integrations/_placeholder/icon.png b/config/.cache/brands/integrations/_placeholder/icon.png deleted file mode 100644 index 02cc160d..00000000 Binary files a/config/.cache/brands/integrations/_placeholder/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/ai_task/icon.png b/config/.cache/brands/integrations/ai_task/icon.png deleted file mode 100644 index 54615129..00000000 Binary files a/config/.cache/brands/integrations/ai_task/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/airly/icon.png b/config/.cache/brands/integrations/airly/icon.png deleted file mode 100644 index 9ead38d3..00000000 Binary files a/config/.cache/brands/integrations/airly/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/alarm_control_panel/icon.png b/config/.cache/brands/integrations/alarm_control_panel/icon.png deleted file mode 100644 index cb75862b..00000000 Binary files a/config/.cache/brands/integrations/alarm_control_panel/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/alert/icon.png b/config/.cache/brands/integrations/alert/icon.png deleted file mode 100644 index ee55f2f6..00000000 Binary files a/config/.cache/brands/integrations/alert/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/alexa/icon.png b/config/.cache/brands/integrations/alexa/icon.png deleted file mode 100644 index 605cdf60..00000000 Binary files a/config/.cache/brands/integrations/alexa/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/analytics/icon.png b/config/.cache/brands/integrations/analytics/icon.png deleted file mode 100644 index e411a7b8..00000000 Binary files a/config/.cache/brands/integrations/analytics/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/api/icon.png b/config/.cache/brands/integrations/api/icon.png deleted file mode 100644 index 226fb689..00000000 Binary files a/config/.cache/brands/integrations/api/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/application_credentials/icon.png b/config/.cache/brands/integrations/application_credentials/icon.png deleted file mode 100644 index 3c1836f7..00000000 Binary files a/config/.cache/brands/integrations/application_credentials/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/assist_pipeline/icon.png b/config/.cache/brands/integrations/assist_pipeline/icon.png deleted file mode 100644 index da4e5159..00000000 Binary files a/config/.cache/brands/integrations/assist_pipeline/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/august/icon.png b/config/.cache/brands/integrations/august/icon.png deleted file mode 100644 index 2bfe9cdd..00000000 Binary files a/config/.cache/brands/integrations/august/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/auth/icon.png b/config/.cache/brands/integrations/auth/icon.png deleted file mode 100644 index bfeea1c7..00000000 Binary files a/config/.cache/brands/integrations/auth/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/automation/icon.png b/config/.cache/brands/integrations/automation/icon.png deleted file mode 100644 index 6ae16793..00000000 Binary files a/config/.cache/brands/integrations/automation/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/backup/icon.png b/config/.cache/brands/integrations/backup/icon.png deleted file mode 100644 index 199cec42..00000000 Binary files a/config/.cache/brands/integrations/backup/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/battery_notes/icon.png b/config/.cache/brands/integrations/battery_notes/icon.png deleted file mode 100644 index 8f5e6c7d..00000000 Binary files a/config/.cache/brands/integrations/battery_notes/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/binary_sensor/icon.png b/config/.cache/brands/integrations/binary_sensor/icon.png deleted file mode 100644 index aa4e658a..00000000 Binary files a/config/.cache/brands/integrations/binary_sensor/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/blink/icon.png b/config/.cache/brands/integrations/blink/icon.png deleted file mode 100644 index 17f545e8..00000000 Binary files a/config/.cache/brands/integrations/blink/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/blitzortung/icon.png b/config/.cache/brands/integrations/blitzortung/icon.png deleted file mode 100644 index ec6dc1c1..00000000 Binary files a/config/.cache/brands/integrations/blitzortung/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/blueprint/icon.png b/config/.cache/brands/integrations/blueprint/icon.png deleted file mode 100644 index aeadc6d5..00000000 Binary files a/config/.cache/brands/integrations/blueprint/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/bluetooth/icon.png b/config/.cache/brands/integrations/bluetooth/icon.png deleted file mode 100644 index 4ee2e081..00000000 Binary files a/config/.cache/brands/integrations/bluetooth/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/bluetooth_adapters/icon.png b/config/.cache/brands/integrations/bluetooth_adapters/icon.png deleted file mode 100644 index 641b5a21..00000000 Binary files a/config/.cache/brands/integrations/bluetooth_adapters/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/brands/icon.png b/config/.cache/brands/integrations/brands/icon.png deleted file mode 100644 index e69de29b..00000000 diff --git a/config/.cache/brands/integrations/button/icon.png b/config/.cache/brands/integrations/button/icon.png deleted file mode 100644 index a16b6b4e..00000000 Binary files a/config/.cache/brands/integrations/button/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/calendar/icon.png b/config/.cache/brands/integrations/calendar/icon.png deleted file mode 100644 index 30d782ad..00000000 Binary files a/config/.cache/brands/integrations/calendar/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/camera/icon.png b/config/.cache/brands/integrations/camera/icon.png deleted file mode 100644 index 69a2a575..00000000 Binary files a/config/.cache/brands/integrations/camera/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/cast/icon.png b/config/.cache/brands/integrations/cast/icon.png deleted file mode 100644 index 55119969..00000000 Binary files a/config/.cache/brands/integrations/cast/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/climate/icon.png b/config/.cache/brands/integrations/climate/icon.png deleted file mode 100644 index ee58d5f5..00000000 Binary files a/config/.cache/brands/integrations/climate/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/cloud/icon.png b/config/.cache/brands/integrations/cloud/icon.png deleted file mode 100644 index 874cd91d..00000000 Binary files a/config/.cache/brands/integrations/cloud/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/co2signal/icon.png b/config/.cache/brands/integrations/co2signal/icon.png deleted file mode 100644 index 3c2bada0..00000000 Binary files a/config/.cache/brands/integrations/co2signal/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/command_line/icon.png b/config/.cache/brands/integrations/command_line/icon.png deleted file mode 100644 index a3ec392f..00000000 Binary files a/config/.cache/brands/integrations/command_line/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/config/icon.png b/config/.cache/brands/integrations/config/icon.png deleted file mode 100644 index f23d170e..00000000 Binary files a/config/.cache/brands/integrations/config/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/conversation/icon.png b/config/.cache/brands/integrations/conversation/icon.png deleted file mode 100644 index acb638a3..00000000 Binary files a/config/.cache/brands/integrations/conversation/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/counter/icon.png b/config/.cache/brands/integrations/counter/icon.png deleted file mode 100644 index 7c116040..00000000 Binary files a/config/.cache/brands/integrations/counter/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/cover/icon.png b/config/.cache/brands/integrations/cover/icon.png deleted file mode 100644 index c29ea574..00000000 Binary files a/config/.cache/brands/integrations/cover/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/default_config/icon.png b/config/.cache/brands/integrations/default_config/icon.png deleted file mode 100644 index 65508a5b..00000000 Binary files a/config/.cache/brands/integrations/default_config/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/device_automation/icon.png b/config/.cache/brands/integrations/device_automation/icon.png deleted file mode 100644 index 94af35b7..00000000 Binary files a/config/.cache/brands/integrations/device_automation/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/device_tracker/icon.png b/config/.cache/brands/integrations/device_tracker/icon.png deleted file mode 100644 index cec946d5..00000000 Binary files a/config/.cache/brands/integrations/device_tracker/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/dhcp/icon.png b/config/.cache/brands/integrations/dhcp/icon.png deleted file mode 100644 index 8974c5ab..00000000 Binary files a/config/.cache/brands/integrations/dhcp/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/diagnostics/icon.png b/config/.cache/brands/integrations/diagnostics/icon.png deleted file mode 100644 index 93cb2748..00000000 Binary files a/config/.cache/brands/integrations/diagnostics/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/dlna_dmr/icon.png b/config/.cache/brands/integrations/dlna_dmr/icon.png deleted file mode 100644 index 763d2422..00000000 Binary files a/config/.cache/brands/integrations/dlna_dmr/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/dreame_vacuum/icon.png b/config/.cache/brands/integrations/dreame_vacuum/icon.png deleted file mode 100644 index c59df818..00000000 Binary files a/config/.cache/brands/integrations/dreame_vacuum/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/duke_energy/icon.png b/config/.cache/brands/integrations/duke_energy/icon.png deleted file mode 100644 index c7e46d2d..00000000 Binary files a/config/.cache/brands/integrations/duke_energy/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/econet/icon.png b/config/.cache/brands/integrations/econet/icon.png deleted file mode 100644 index 8f47c948..00000000 Binary files a/config/.cache/brands/integrations/econet/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/energy/icon.png b/config/.cache/brands/integrations/energy/icon.png deleted file mode 100644 index ba5bc370..00000000 Binary files a/config/.cache/brands/integrations/energy/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/esphome/icon.png b/config/.cache/brands/integrations/esphome/icon.png deleted file mode 100644 index 0a4a375d..00000000 Binary files a/config/.cache/brands/integrations/esphome/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/event/icon.png b/config/.cache/brands/integrations/event/icon.png deleted file mode 100644 index cd40834b..00000000 Binary files a/config/.cache/brands/integrations/event/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/fan/icon.png b/config/.cache/brands/integrations/fan/icon.png deleted file mode 100644 index e7c61fe8..00000000 Binary files a/config/.cache/brands/integrations/fan/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/feedreader/icon.png b/config/.cache/brands/integrations/feedreader/icon.png deleted file mode 100644 index 425a6e56..00000000 Binary files a/config/.cache/brands/integrations/feedreader/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/ffmpeg/icon.png b/config/.cache/brands/integrations/ffmpeg/icon.png deleted file mode 100644 index 96c2ba68..00000000 Binary files a/config/.cache/brands/integrations/ffmpeg/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/file/icon.png b/config/.cache/brands/integrations/file/icon.png deleted file mode 100644 index 933266ac..00000000 Binary files a/config/.cache/brands/integrations/file/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/file_upload/icon.png b/config/.cache/brands/integrations/file_upload/icon.png deleted file mode 100644 index 4482e3f8..00000000 Binary files a/config/.cache/brands/integrations/file_upload/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/flux_led/icon.png b/config/.cache/brands/integrations/flux_led/icon.png deleted file mode 100644 index 27d9e3f2..00000000 Binary files a/config/.cache/brands/integrations/flux_led/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/frigate/icon.png b/config/.cache/brands/integrations/frigate/icon.png deleted file mode 100644 index a6cbb8b9..00000000 Binary files a/config/.cache/brands/integrations/frigate/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/frigate/icon@2x.png b/config/.cache/brands/integrations/frigate/icon@2x.png deleted file mode 100644 index aff73596..00000000 Binary files a/config/.cache/brands/integrations/frigate/icon@2x.png and /dev/null differ diff --git a/config/.cache/brands/integrations/frigate/logo.png b/config/.cache/brands/integrations/frigate/logo.png deleted file mode 100644 index 90efa1ff..00000000 Binary files a/config/.cache/brands/integrations/frigate/logo.png and /dev/null differ diff --git a/config/.cache/brands/integrations/frontend/icon.png b/config/.cache/brands/integrations/frontend/icon.png deleted file mode 100644 index e5267faf..00000000 Binary files a/config/.cache/brands/integrations/frontend/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/fully_kiosk/icon.png b/config/.cache/brands/integrations/fully_kiosk/icon.png deleted file mode 100644 index 48cbd767..00000000 Binary files a/config/.cache/brands/integrations/fully_kiosk/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/fully_kiosk/icon@2x.png b/config/.cache/brands/integrations/fully_kiosk/icon@2x.png deleted file mode 100644 index 48cbd767..00000000 Binary files a/config/.cache/brands/integrations/fully_kiosk/icon@2x.png and /dev/null differ diff --git a/config/.cache/brands/integrations/geo_location/icon.png b/config/.cache/brands/integrations/geo_location/icon.png deleted file mode 100644 index 9e6d1f95..00000000 Binary files a/config/.cache/brands/integrations/geo_location/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/glances/icon.png b/config/.cache/brands/integrations/glances/icon.png deleted file mode 100644 index fda5b488..00000000 Binary files a/config/.cache/brands/integrations/glances/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/go2rtc/icon.png b/config/.cache/brands/integrations/go2rtc/icon.png deleted file mode 100644 index 1c2f4b82..00000000 Binary files a/config/.cache/brands/integrations/go2rtc/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/google_assistant/icon.png b/config/.cache/brands/integrations/google_assistant/icon.png deleted file mode 100644 index 668c9649..00000000 Binary files a/config/.cache/brands/integrations/google_assistant/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/google_generative_ai_conversation/icon.png b/config/.cache/brands/integrations/google_generative_ai_conversation/icon.png deleted file mode 100644 index ceed146b..00000000 Binary files a/config/.cache/brands/integrations/google_generative_ai_conversation/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/group/icon.png b/config/.cache/brands/integrations/group/icon.png deleted file mode 100644 index e33c8aae..00000000 Binary files a/config/.cache/brands/integrations/group/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/hardware/icon.png b/config/.cache/brands/integrations/hardware/icon.png deleted file mode 100644 index c388aaf0..00000000 Binary files a/config/.cache/brands/integrations/hardware/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/history/icon.png b/config/.cache/brands/integrations/history/icon.png deleted file mode 100644 index 7136e837..00000000 Binary files a/config/.cache/brands/integrations/history/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/history_stats/icon.png b/config/.cache/brands/integrations/history_stats/icon.png deleted file mode 100644 index 09fe90f3..00000000 Binary files a/config/.cache/brands/integrations/history_stats/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/homeassistant/icon.png b/config/.cache/brands/integrations/homeassistant/icon.png deleted file mode 100644 index 39f0c14e..00000000 Binary files a/config/.cache/brands/integrations/homeassistant/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/homeassistant_alerts/icon.png b/config/.cache/brands/integrations/homeassistant_alerts/icon.png deleted file mode 100644 index 8166b8d9..00000000 Binary files a/config/.cache/brands/integrations/homeassistant_alerts/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/homekit/icon.png b/config/.cache/brands/integrations/homekit/icon.png deleted file mode 100644 index defa5d0b..00000000 Binary files a/config/.cache/brands/integrations/homekit/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/homekit_controller/icon.png b/config/.cache/brands/integrations/homekit_controller/icon.png deleted file mode 100644 index defa5d0b..00000000 Binary files a/config/.cache/brands/integrations/homekit_controller/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/http/icon.png b/config/.cache/brands/integrations/http/icon.png deleted file mode 100644 index 3f9ab041..00000000 Binary files a/config/.cache/brands/integrations/http/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/hue/icon.png b/config/.cache/brands/integrations/hue/icon.png deleted file mode 100644 index 7826f433..00000000 Binary files a/config/.cache/brands/integrations/hue/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/humidifier/icon.png b/config/.cache/brands/integrations/humidifier/icon.png deleted file mode 100644 index 03c1a638..00000000 Binary files a/config/.cache/brands/integrations/humidifier/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/ifttt/icon.png b/config/.cache/brands/integrations/ifttt/icon.png deleted file mode 100644 index decf21e0..00000000 Binary files a/config/.cache/brands/integrations/ifttt/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/image_upload/icon.png b/config/.cache/brands/integrations/image_upload/icon.png deleted file mode 100644 index c10f0943..00000000 Binary files a/config/.cache/brands/integrations/image_upload/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/input_boolean/icon.png b/config/.cache/brands/integrations/input_boolean/icon.png deleted file mode 100644 index 17b61b1e..00000000 Binary files a/config/.cache/brands/integrations/input_boolean/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/input_button/icon.png b/config/.cache/brands/integrations/input_button/icon.png deleted file mode 100644 index 4e4db4f7..00000000 Binary files a/config/.cache/brands/integrations/input_button/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/input_datetime/icon.png b/config/.cache/brands/integrations/input_datetime/icon.png deleted file mode 100644 index f7fd095a..00000000 Binary files a/config/.cache/brands/integrations/input_datetime/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/input_number/icon.png b/config/.cache/brands/integrations/input_number/icon.png deleted file mode 100644 index 2db7955a..00000000 Binary files a/config/.cache/brands/integrations/input_number/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/input_select/icon.png b/config/.cache/brands/integrations/input_select/icon.png deleted file mode 100644 index 4e083623..00000000 Binary files a/config/.cache/brands/integrations/input_select/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/input_text/icon.png b/config/.cache/brands/integrations/input_text/icon.png deleted file mode 100644 index 20c8c66b..00000000 Binary files a/config/.cache/brands/integrations/input_text/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/integration/icon.png b/config/.cache/brands/integrations/integration/icon.png deleted file mode 100644 index 8b369866..00000000 Binary files a/config/.cache/brands/integrations/integration/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/intent/icon.png b/config/.cache/brands/integrations/intent/icon.png deleted file mode 100644 index 32ca5985..00000000 Binary files a/config/.cache/brands/integrations/intent/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/ios/icon.png b/config/.cache/brands/integrations/ios/icon.png deleted file mode 100644 index 2b653636..00000000 Binary files a/config/.cache/brands/integrations/ios/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/ipp/icon.png b/config/.cache/brands/integrations/ipp/icon.png deleted file mode 100644 index 27877b65..00000000 Binary files a/config/.cache/brands/integrations/ipp/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/labs/icon.png b/config/.cache/brands/integrations/labs/icon.png deleted file mode 100644 index 2b864c36..00000000 Binary files a/config/.cache/brands/integrations/labs/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/launch_library/icon.png b/config/.cache/brands/integrations/launch_library/icon.png deleted file mode 100644 index 3f7e912e..00000000 Binary files a/config/.cache/brands/integrations/launch_library/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/life360/icon.png b/config/.cache/brands/integrations/life360/icon.png deleted file mode 100644 index 5dbb9be2..00000000 Binary files a/config/.cache/brands/integrations/life360/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/light/icon.png b/config/.cache/brands/integrations/light/icon.png deleted file mode 100644 index 97965b92..00000000 Binary files a/config/.cache/brands/integrations/light/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/lock/icon.png b/config/.cache/brands/integrations/lock/icon.png deleted file mode 100644 index a2eb6e87..00000000 Binary files a/config/.cache/brands/integrations/lock/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/logbook/icon.png b/config/.cache/brands/integrations/logbook/icon.png deleted file mode 100644 index 6fd1bd1d..00000000 Binary files a/config/.cache/brands/integrations/logbook/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/logger/icon.png b/config/.cache/brands/integrations/logger/icon.png deleted file mode 100644 index 378293b3..00000000 Binary files a/config/.cache/brands/integrations/logger/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/lovelace/icon.png b/config/.cache/brands/integrations/lovelace/icon.png deleted file mode 100644 index e5267faf..00000000 Binary files a/config/.cache/brands/integrations/lovelace/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/matter/icon.png b/config/.cache/brands/integrations/matter/icon.png deleted file mode 100644 index d6b04319..00000000 Binary files a/config/.cache/brands/integrations/matter/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/mcp_server/icon.png b/config/.cache/brands/integrations/mcp_server/icon.png deleted file mode 100644 index 76aa2770..00000000 Binary files a/config/.cache/brands/integrations/mcp_server/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/media_player/icon.png b/config/.cache/brands/integrations/media_player/icon.png deleted file mode 100644 index 702b5a79..00000000 Binary files a/config/.cache/brands/integrations/media_player/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/media_source/icon.png b/config/.cache/brands/integrations/media_source/icon.png deleted file mode 100644 index a0f3dbe2..00000000 Binary files a/config/.cache/brands/integrations/media_source/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/mobile_app/icon.png b/config/.cache/brands/integrations/mobile_app/icon.png deleted file mode 100644 index b7f29918..00000000 Binary files a/config/.cache/brands/integrations/mobile_app/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/moon/icon.png b/config/.cache/brands/integrations/moon/icon.png deleted file mode 100644 index ee1a490f..00000000 Binary files a/config/.cache/brands/integrations/moon/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/mqtt/icon.png b/config/.cache/brands/integrations/mqtt/icon.png deleted file mode 100644 index fcc1ce25..00000000 Binary files a/config/.cache/brands/integrations/mqtt/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/my/icon.png b/config/.cache/brands/integrations/my/icon.png deleted file mode 100644 index 961b6604..00000000 Binary files a/config/.cache/brands/integrations/my/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/nest/icon.png b/config/.cache/brands/integrations/nest/icon.png deleted file mode 100644 index 3e00949b..00000000 Binary files a/config/.cache/brands/integrations/nest/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/network/icon.png b/config/.cache/brands/integrations/network/icon.png deleted file mode 100644 index 5176f180..00000000 Binary files a/config/.cache/brands/integrations/network/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/notify/icon.png b/config/.cache/brands/integrations/notify/icon.png deleted file mode 100644 index 10f77b21..00000000 Binary files a/config/.cache/brands/integrations/notify/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/number/icon.png b/config/.cache/brands/integrations/number/icon.png deleted file mode 100644 index 8f8fe6d5..00000000 Binary files a/config/.cache/brands/integrations/number/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/nut/icon.png b/config/.cache/brands/integrations/nut/icon.png deleted file mode 100644 index ec2ab02a..00000000 Binary files a/config/.cache/brands/integrations/nut/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/nws_alerts/icon.png b/config/.cache/brands/integrations/nws_alerts/icon.png deleted file mode 100644 index 0fc54d81..00000000 Binary files a/config/.cache/brands/integrations/nws_alerts/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/onboarding/icon.png b/config/.cache/brands/integrations/onboarding/icon.png deleted file mode 100644 index 94a9af7b..00000000 Binary files a/config/.cache/brands/integrations/onboarding/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/openai_conversation/icon.png b/config/.cache/brands/integrations/openai_conversation/icon.png deleted file mode 100644 index e020a2fe..00000000 Binary files a/config/.cache/brands/integrations/openai_conversation/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/persistent_notification/icon.png b/config/.cache/brands/integrations/persistent_notification/icon.png deleted file mode 100644 index 52d0905d..00000000 Binary files a/config/.cache/brands/integrations/persistent_notification/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/person/icon.png b/config/.cache/brands/integrations/person/icon.png deleted file mode 100644 index 04611f34..00000000 Binary files a/config/.cache/brands/integrations/person/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/phyn/icon.png b/config/.cache/brands/integrations/phyn/icon.png deleted file mode 100644 index 384d101b..00000000 Binary files a/config/.cache/brands/integrations/phyn/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/pi_hole_v6/icon.png b/config/.cache/brands/integrations/pi_hole_v6/icon.png deleted file mode 100644 index edcbfbbe..00000000 Binary files a/config/.cache/brands/integrations/pi_hole_v6/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/pirateweather/icon.png b/config/.cache/brands/integrations/pirateweather/icon.png deleted file mode 100644 index 0c9d6ae3..00000000 Binary files a/config/.cache/brands/integrations/pirateweather/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/places/icon.png b/config/.cache/brands/integrations/places/icon.png deleted file mode 100644 index 2f7efa07..00000000 Binary files a/config/.cache/brands/integrations/places/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/portainer/icon.png b/config/.cache/brands/integrations/portainer/icon.png deleted file mode 100644 index 09cc1a7f..00000000 Binary files a/config/.cache/brands/integrations/portainer/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/powerwall/icon.png b/config/.cache/brands/integrations/powerwall/icon.png deleted file mode 100644 index 88cca8c3..00000000 Binary files a/config/.cache/brands/integrations/powerwall/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/proxmoxve/icon.png b/config/.cache/brands/integrations/proxmoxve/icon.png deleted file mode 100644 index f03c68b0..00000000 Binary files a/config/.cache/brands/integrations/proxmoxve/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/rachio/icon.png b/config/.cache/brands/integrations/rachio/icon.png deleted file mode 100644 index 620296aa..00000000 Binary files a/config/.cache/brands/integrations/rachio/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/recorder/icon.png b/config/.cache/brands/integrations/recorder/icon.png deleted file mode 100644 index 378293b3..00000000 Binary files a/config/.cache/brands/integrations/recorder/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/remote/icon.png b/config/.cache/brands/integrations/remote/icon.png deleted file mode 100644 index 3f35ce57..00000000 Binary files a/config/.cache/brands/integrations/remote/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/repairs/icon.png b/config/.cache/brands/integrations/repairs/icon.png deleted file mode 100644 index 3534ac71..00000000 Binary files a/config/.cache/brands/integrations/repairs/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/rest/icon.png b/config/.cache/brands/integrations/rest/icon.png deleted file mode 100644 index deecdb86..00000000 Binary files a/config/.cache/brands/integrations/rest/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/rest_command/icon.png b/config/.cache/brands/integrations/rest_command/icon.png deleted file mode 100644 index deecdb86..00000000 Binary files a/config/.cache/brands/integrations/rest_command/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/roku/icon.png b/config/.cache/brands/integrations/roku/icon.png deleted file mode 100644 index 0107a3eb..00000000 Binary files a/config/.cache/brands/integrations/roku/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/scene/icon.png b/config/.cache/brands/integrations/scene/icon.png deleted file mode 100644 index 12358c4f..00000000 Binary files a/config/.cache/brands/integrations/scene/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/schedule/icon.png b/config/.cache/brands/integrations/schedule/icon.png deleted file mode 100644 index 944b8a44..00000000 Binary files a/config/.cache/brands/integrations/schedule/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/script/icon.png b/config/.cache/brands/integrations/script/icon.png deleted file mode 100644 index fe60b350..00000000 Binary files a/config/.cache/brands/integrations/script/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/search/icon.png b/config/.cache/brands/integrations/search/icon.png deleted file mode 100644 index e7f2fc41..00000000 Binary files a/config/.cache/brands/integrations/search/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/season/icon.png b/config/.cache/brands/integrations/season/icon.png deleted file mode 100644 index f9c8a7f7..00000000 Binary files a/config/.cache/brands/integrations/season/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/select/icon.png b/config/.cache/brands/integrations/select/icon.png deleted file mode 100644 index 1b8f76c8..00000000 Binary files a/config/.cache/brands/integrations/select/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/sensor/icon.png b/config/.cache/brands/integrations/sensor/icon.png deleted file mode 100644 index ac8047c8..00000000 Binary files a/config/.cache/brands/integrations/sensor/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/siren/icon.png b/config/.cache/brands/integrations/siren/icon.png deleted file mode 100644 index 8166b8d9..00000000 Binary files a/config/.cache/brands/integrations/siren/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/sleepiq/icon.png b/config/.cache/brands/integrations/sleepiq/icon.png deleted file mode 100644 index ab3d43a4..00000000 Binary files a/config/.cache/brands/integrations/sleepiq/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/smartthinq_sensors/icon.png b/config/.cache/brands/integrations/smartthinq_sensors/icon.png deleted file mode 100644 index 891d7496..00000000 Binary files a/config/.cache/brands/integrations/smartthinq_sensors/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/speedtestdotnet/icon.png b/config/.cache/brands/integrations/speedtestdotnet/icon.png deleted file mode 100644 index b5365efc..00000000 Binary files a/config/.cache/brands/integrations/speedtestdotnet/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/spook/icon.png b/config/.cache/brands/integrations/spook/icon.png deleted file mode 100644 index 3f3193d0..00000000 Binary files a/config/.cache/brands/integrations/spook/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/sql/icon.png b/config/.cache/brands/integrations/sql/icon.png deleted file mode 100644 index 378293b3..00000000 Binary files a/config/.cache/brands/integrations/sql/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/ssdp/icon.png b/config/.cache/brands/integrations/ssdp/icon.png deleted file mode 100644 index 8974c5ab..00000000 Binary files a/config/.cache/brands/integrations/ssdp/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/stream/icon.png b/config/.cache/brands/integrations/stream/icon.png deleted file mode 100644 index a0748c31..00000000 Binary files a/config/.cache/brands/integrations/stream/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/stt/icon.png b/config/.cache/brands/integrations/stt/icon.png deleted file mode 100644 index 50d7e301..00000000 Binary files a/config/.cache/brands/integrations/stt/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/sun/icon.png b/config/.cache/brands/integrations/sun/icon.png deleted file mode 100644 index 06ff4cb4..00000000 Binary files a/config/.cache/brands/integrations/sun/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/switch/icon.png b/config/.cache/brands/integrations/switch/icon.png deleted file mode 100644 index dc705641..00000000 Binary files a/config/.cache/brands/integrations/switch/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/synology_dsm/icon.png b/config/.cache/brands/integrations/synology_dsm/icon.png deleted file mode 100644 index f5867d98..00000000 Binary files a/config/.cache/brands/integrations/synology_dsm/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/system_health/icon.png b/config/.cache/brands/integrations/system_health/icon.png deleted file mode 100644 index 31f51fcc..00000000 Binary files a/config/.cache/brands/integrations/system_health/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/system_log/icon.png b/config/.cache/brands/integrations/system_log/icon.png deleted file mode 100644 index 6fd1bd1d..00000000 Binary files a/config/.cache/brands/integrations/system_log/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/systemmonitor/icon.png b/config/.cache/brands/integrations/systemmonitor/icon.png deleted file mode 100644 index 34a3524f..00000000 Binary files a/config/.cache/brands/integrations/systemmonitor/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/tag/icon.png b/config/.cache/brands/integrations/tag/icon.png deleted file mode 100644 index a080e91a..00000000 Binary files a/config/.cache/brands/integrations/tag/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/teamtracker/icon.png b/config/.cache/brands/integrations/teamtracker/icon.png deleted file mode 100644 index a9d9925b..00000000 Binary files a/config/.cache/brands/integrations/teamtracker/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/telegram_bot/icon.png b/config/.cache/brands/integrations/telegram_bot/icon.png deleted file mode 100644 index e74bc27b..00000000 Binary files a/config/.cache/brands/integrations/telegram_bot/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/template/icon.png b/config/.cache/brands/integrations/template/icon.png deleted file mode 100644 index 2c5bc81b..00000000 Binary files a/config/.cache/brands/integrations/template/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/time/icon.png b/config/.cache/brands/integrations/time/icon.png deleted file mode 100644 index 47d1e57e..00000000 Binary files a/config/.cache/brands/integrations/time/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/time_date/icon.png b/config/.cache/brands/integrations/time_date/icon.png deleted file mode 100644 index 944b8a44..00000000 Binary files a/config/.cache/brands/integrations/time_date/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/timer/icon.png b/config/.cache/brands/integrations/timer/icon.png deleted file mode 100644 index d651b549..00000000 Binary files a/config/.cache/brands/integrations/timer/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/tplink/icon.png b/config/.cache/brands/integrations/tplink/icon.png deleted file mode 100644 index 3a8961c5..00000000 Binary files a/config/.cache/brands/integrations/tplink/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/trace/icon.png b/config/.cache/brands/integrations/trace/icon.png deleted file mode 100644 index 39f0c14e..00000000 Binary files a/config/.cache/brands/integrations/trace/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/tuya/icon.png b/config/.cache/brands/integrations/tuya/icon.png deleted file mode 100644 index dc238dcc..00000000 Binary files a/config/.cache/brands/integrations/tuya/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/unifi/icon.png b/config/.cache/brands/integrations/unifi/icon.png deleted file mode 100644 index 4e1c9c6f..00000000 Binary files a/config/.cache/brands/integrations/unifi/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/update/icon.png b/config/.cache/brands/integrations/update/icon.png deleted file mode 100644 index c9b48888..00000000 Binary files a/config/.cache/brands/integrations/update/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/upnp/icon.png b/config/.cache/brands/integrations/upnp/icon.png deleted file mode 100644 index 95ba0c93..00000000 Binary files a/config/.cache/brands/integrations/upnp/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/uptime/icon.png b/config/.cache/brands/integrations/uptime/icon.png deleted file mode 100644 index d651b549..00000000 Binary files a/config/.cache/brands/integrations/uptime/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/uptimerobot/icon.png b/config/.cache/brands/integrations/uptimerobot/icon.png deleted file mode 100644 index 45876ba2..00000000 Binary files a/config/.cache/brands/integrations/uptimerobot/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/usage_prediction/icon.png b/config/.cache/brands/integrations/usage_prediction/icon.png deleted file mode 100644 index 7398d4d8..00000000 Binary files a/config/.cache/brands/integrations/usage_prediction/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/usb/icon.png b/config/.cache/brands/integrations/usb/icon.png deleted file mode 100644 index 8974c5ab..00000000 Binary files a/config/.cache/brands/integrations/usb/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/utility_meter/icon.png b/config/.cache/brands/integrations/utility_meter/icon.png deleted file mode 100644 index b37f4912..00000000 Binary files a/config/.cache/brands/integrations/utility_meter/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/vacuum/icon.png b/config/.cache/brands/integrations/vacuum/icon.png deleted file mode 100644 index 48805026..00000000 Binary files a/config/.cache/brands/integrations/vacuum/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/valve/icon.png b/config/.cache/brands/integrations/valve/icon.png deleted file mode 100644 index e69de29b..00000000 diff --git a/config/.cache/brands/integrations/version/icon.png b/config/.cache/brands/integrations/version/icon.png deleted file mode 100644 index 39f0c14e..00000000 Binary files a/config/.cache/brands/integrations/version/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/wake_word/icon.png b/config/.cache/brands/integrations/wake_word/icon.png deleted file mode 100644 index acb638a3..00000000 Binary files a/config/.cache/brands/integrations/wake_word/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/water_heater/icon.png b/config/.cache/brands/integrations/water_heater/icon.png deleted file mode 100644 index 9be1668b..00000000 Binary files a/config/.cache/brands/integrations/water_heater/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/weather/icon.png b/config/.cache/brands/integrations/weather/icon.png deleted file mode 100644 index b2fc81be..00000000 Binary files a/config/.cache/brands/integrations/weather/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/web_rtc/icon.png b/config/.cache/brands/integrations/web_rtc/icon.png deleted file mode 100644 index 3527d1e2..00000000 Binary files a/config/.cache/brands/integrations/web_rtc/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/webhook/icon.png b/config/.cache/brands/integrations/webhook/icon.png deleted file mode 100644 index 93a5df6e..00000000 Binary files a/config/.cache/brands/integrations/webhook/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/websocket_api/icon.png b/config/.cache/brands/integrations/websocket_api/icon.png deleted file mode 100644 index 226fb689..00000000 Binary files a/config/.cache/brands/integrations/websocket_api/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/whois/icon.png b/config/.cache/brands/integrations/whois/icon.png deleted file mode 100644 index a11eeaf0..00000000 Binary files a/config/.cache/brands/integrations/whois/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/wyzeapi/icon.png b/config/.cache/brands/integrations/wyzeapi/icon.png deleted file mode 100644 index b189b810..00000000 Binary files a/config/.cache/brands/integrations/wyzeapi/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/yahoofinance/icon.png b/config/.cache/brands/integrations/yahoofinance/icon.png deleted file mode 100644 index 4484faad..00000000 Binary files a/config/.cache/brands/integrations/yahoofinance/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/youtube/icon.png b/config/.cache/brands/integrations/youtube/icon.png deleted file mode 100644 index 774fbc56..00000000 Binary files a/config/.cache/brands/integrations/youtube/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/zeroconf/icon.png b/config/.cache/brands/integrations/zeroconf/icon.png deleted file mode 100644 index d21d6279..00000000 Binary files a/config/.cache/brands/integrations/zeroconf/icon.png and /dev/null differ diff --git a/config/.cache/brands/integrations/zone/icon.png b/config/.cache/brands/integrations/zone/icon.png deleted file mode 100644 index b938794e..00000000 Binary files a/config/.cache/brands/integrations/zone/icon.png and /dev/null differ diff --git a/config/automation/kitchen_sink_circadian.yaml b/config/automation/kitchen_sink_circadian.yaml index 275f7fab..927b28b0 100644 --- a/config/automation/kitchen_sink_circadian.yaml +++ b/config/automation/kitchen_sink_circadian.yaml @@ -9,6 +9,7 @@ # Related Issue: n/a # Notes: Uses color temperature only to avoid inherited color scenes. # TV playback keeps the sink off so media time does not re-light it. +# Notes: Office lamp switch off-gates the sink while active PC work is in progress. ###################################################################### - alias: "Kitchen Sink Circadian Daylight" @@ -35,6 +36,10 @@ entity_id: media_player.living_room_ultra to: "playing" id: tv_playing + - platform: state + entity_id: switch.office_lamp_switch + to: "on" + id: working - platform: sun event: sunrise offset: "-00:30:00" @@ -75,6 +80,8 @@ {% set stacey_awake = is_state('person.stacey', 'home') and is_state('binary_sensor.sleepnumber_carlo_stacey_is_in_bed', 'off') %} {{ carlo_awake or stacey_awake }} + working_active: >- + {{ is_state('switch.office_lamp_switch', 'on') }} target_kelvin: >- {% set elevation = state_attr('sun.sun', 'elevation') | float(-90) %} {% if elevation <= -2 %} @@ -102,12 +109,13 @@ {{ is_state('group.family', 'home') and is_state('input_boolean.guest_mode', 'off') and not (tv_active | bool) + and not (working_active | bool) and ((awake_home | bool) or is_state('light.sink', 'on')) and (is_state('light.sink', 'on') or now().hour >= 4) }} - choose: - conditions: - condition: template - value_template: "{{ tv_active | bool }}" + value_template: "{{ tv_active | bool or working_active | bool }}" sequence: - service: light.turn_off target: diff --git a/config/dashboards/README.md b/config/dashboards/README.md index a01c6946..18bece7d 100644 --- a/config/dashboards/README.md +++ b/config/dashboards/README.md @@ -22,6 +22,10 @@ This folder holds YAML-managed Home Assistant Lovelace dashboards and UI resources. +### Walkthrough +- Video: [Build Clean Home Assistant Dashboards with YAML Partials and Templates](https://youtu.be/aFis2YPeSuY) +- Companion post: [Home Assistant Dashboard Design System (Button-Card First)](https://www.vcloudinfo.com/2026/02/home-assistant-dashboard-design-system-button-card.html) + ### Why this exists - Home Assistant stores dashboards and resources in `config/.storage/` by default (runtime state). - YAML dashboards make the UI version-controllable and editable by automation tools (including Codex). diff --git a/config/dashboards/infrastructure/partials/docker_containers_sections.yaml b/config/dashboards/infrastructure/partials/docker_containers_sections.yaml index 9ceed273..39566f7a 100644 --- a/config/dashboards/infrastructure/partials/docker_containers_sections.yaml +++ b/config/dashboards/infrastructure/partials/docker_containers_sections.yaml @@ -4,12 +4,13 @@ # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Infrastructure Partial - Docker containers sections -# Related Issue: 1560, 1584 +# Related Issue: 1560, 1584, 1725 # Sections layout for the Docker containers view. # ------------------------------------------------------------------- # Notes: Auto-discovers Portainer container entities from `switch.*_container`. # Notes: Keeps cards visible when Portainer telemetry is unavailable (degraded mode). # Notes: Includes stack-level status tiles from Portainer `*_stack_status` entities. +# Notes: Portainer volume usage is visible; volume prune is confined to host maintenance popups with hold-confirm actions. ###################################################################### - type: grid @@ -103,6 +104,65 @@ action: navigate navigation_path: '#infra-docker-14' +- type: grid + column_span: 4 + columns: 1 + square: false + cards: + - type: custom:vertical-stack-in-card + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Docker Volumes + - type: custom:auto-entities + show_empty: false + grid_options: + columns: full + card: + type: custom:layout-card + layout_type: custom:grid-layout + layout: + grid-template-columns: repeat(4, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(2, minmax(0, 1fr)) + card_param: cards + filter: + include: + - entity_id: "/^sensor\\..*_volume_disk_usage_total_size$/" + options: + type: custom:button-card + template: bearstone_infra_list_row + icon: mdi:database + name: > + [[[ + const friendly = entity?.attributes?.friendly_name + ? String(entity.attributes.friendly_name) + : String(entity?.entity_id ?? 'Volume usage'); + return friendly.replace(/\s+Volume disk usage total size$/, ''); + ]]] + state_display: > + [[[ + const value = Number(entity?.state); + const unit = entity?.attributes?.unit_of_measurement ?? ''; + if (!Number.isFinite(value)) return entity?.state ?? 'unknown'; + if (unit === 'MiB' && value > 0 && value < 1) { + return `${(value * 1024).toFixed(1)} KiB`; + } + const precision = value >= 10 ? 1 : 2; + return `${value.toFixed(precision)} ${unit}`.trim(); + ]]] + sort: + method: name + - type: grid column_span: 4 columns: 1 @@ -150,6 +210,16 @@ sort: method: name +- type: grid + column_span: 4 + columns: 1 + square: false + cards: + - !include /config/dashboards/infrastructure/popups/docker_10_maintenance.yaml + - !include /config/dashboards/infrastructure/popups/docker_17_maintenance.yaml + - !include /config/dashboards/infrastructure/popups/docker_69_maintenance.yaml + - !include /config/dashboards/infrastructure/popups/docker_14_maintenance.yaml + - type: grid column_span: 4 columns: 1 diff --git a/config/dashboards/infrastructure/partials/proxmox_sections.yaml b/config/dashboards/infrastructure/partials/proxmox_sections.yaml index 7df52b13..4219aa79 100644 --- a/config/dashboards/infrastructure/partials/proxmox_sections.yaml +++ b/config/dashboards/infrastructure/partials/proxmox_sections.yaml @@ -120,10 +120,6 @@ name: HASS CPU - entity: sensor.qemu_carlo_hass_105_memory_used_percentage name: HASS MEM - - entity: sensor.qemu_wireguard_104_cpu_used - name: WireGuard CPU - - entity: sensor.qemu_wireguard_104_memory_used_percentage - name: WireGuard MEM - type: custom:vertical-stack-in-card card_mod: diff --git a/config/dashboards/infrastructure/popups/docker_10_maintenance.yaml b/config/dashboards/infrastructure/popups/docker_10_maintenance.yaml index 481942ad..44b4943d 100644 --- a/config/dashboards/infrastructure/popups/docker_10_maintenance.yaml +++ b/config/dashboards/infrastructure/popups/docker_10_maintenance.yaml @@ -6,8 +6,9 @@ # Infrastructure Popup - docker_10 maintenance # Bubble Card popup for host maintenance details and safe cleanup actions. # ------------------------------------------------------------------- -# Related Issue: 1560 +# Related Issue: 1560, 1725 # Notes: Reuses existing Infrastructure button-card templates inside the popup. +# Notes: Volume prune is destructive; keep it hold-to-act with an explicit confirmation. ###################################################################### type: vertical-stack @@ -35,6 +36,13 @@ cards: last_update_sensor: sensor.docker_10_apt_last_update prune_button: button.carlo_hass_prune_unused_images name: docker_10 + - type: custom:button-card + template: bearstone_infra_volume_prune_tile + name: docker_10 volumes + entity: button.carlo_hass_prune_unused_volumes + variables: + prune_button: button.carlo_hass_prune_unused_volumes + name: docker_10 - type: custom:button-card template: bearstone_infra_tile entity: sensor.docker_10_apt_last_update diff --git a/config/dashboards/infrastructure/popups/docker_14_maintenance.yaml b/config/dashboards/infrastructure/popups/docker_14_maintenance.yaml index ade276f0..73e0151e 100644 --- a/config/dashboards/infrastructure/popups/docker_14_maintenance.yaml +++ b/config/dashboards/infrastructure/popups/docker_14_maintenance.yaml @@ -6,8 +6,9 @@ # Infrastructure Popup - docker_14 maintenance # Bubble Card popup for host maintenance details and safe cleanup actions. # ------------------------------------------------------------------- -# Related Issue: 1560 +# Related Issue: 1560, 1725 # Notes: Reuses existing Infrastructure button-card templates inside the popup. +# Notes: Volume prune is destructive; keep it hold-to-act with an explicit confirmation. ###################################################################### type: vertical-stack @@ -35,6 +36,13 @@ cards: last_update_sensor: sensor.docker_14_apt_last_update prune_button: button.docker2_prune_unused_images name: docker_14 + - type: custom:button-card + template: bearstone_infra_volume_prune_tile + name: docker_14 volumes + entity: button.docker2_prune_unused_volumes + variables: + prune_button: button.docker2_prune_unused_volumes + name: docker_14 - type: custom:button-card template: bearstone_infra_tile entity: sensor.docker_14_apt_last_update diff --git a/config/dashboards/infrastructure/popups/docker_17_maintenance.yaml b/config/dashboards/infrastructure/popups/docker_17_maintenance.yaml index 38398d5b..53c92cf1 100644 --- a/config/dashboards/infrastructure/popups/docker_17_maintenance.yaml +++ b/config/dashboards/infrastructure/popups/docker_17_maintenance.yaml @@ -6,8 +6,9 @@ # Infrastructure Popup - docker_17 maintenance # Bubble Card popup for host maintenance details and safe cleanup actions. # ------------------------------------------------------------------- -# Related Issue: 1560 +# Related Issue: 1560, 1725 # Notes: Reuses existing Infrastructure button-card templates inside the popup. +# Notes: Volume prune is destructive; keep it hold-to-act with an explicit confirmation. ###################################################################### type: vertical-stack @@ -35,6 +36,13 @@ cards: last_update_sensor: sensor.docker_17_apt_last_update prune_button: button.docker17_prune_unused_images name: docker_17 + - type: custom:button-card + template: bearstone_infra_volume_prune_tile + name: docker_17 volumes + entity: button.docker17_prune_unused_volumes + variables: + prune_button: button.docker17_prune_unused_volumes + name: docker_17 - type: custom:button-card template: bearstone_infra_tile entity: sensor.docker_17_apt_last_update diff --git a/config/dashboards/infrastructure/popups/docker_69_maintenance.yaml b/config/dashboards/infrastructure/popups/docker_69_maintenance.yaml index d9eb670b..6cd330b2 100644 --- a/config/dashboards/infrastructure/popups/docker_69_maintenance.yaml +++ b/config/dashboards/infrastructure/popups/docker_69_maintenance.yaml @@ -6,8 +6,9 @@ # Infrastructure Popup - docker_69 maintenance # Bubble Card popup for host maintenance details and safe cleanup actions. # ------------------------------------------------------------------- -# Related Issue: 1560 +# Related Issue: 1560, 1725 # Notes: Reuses existing Infrastructure button-card templates inside the popup. +# Notes: Volume prune is destructive; keep it hold-to-act with an explicit confirmation. ###################################################################### type: vertical-stack @@ -35,6 +36,13 @@ cards: last_update_sensor: sensor.docker_69_apt_last_update prune_button: button.docker69_prune_unused_images name: docker_69 + - type: custom:button-card + template: bearstone_infra_volume_prune_tile + name: docker_69 volumes + entity: button.docker69_prune_unused_volumes + variables: + prune_button: button.docker69_prune_unused_volumes + name: docker_69 - type: custom:button-card template: bearstone_infra_tile entity: sensor.docker_69_apt_last_update diff --git a/config/dashboards/infrastructure/templates/button_card_templates.yaml b/config/dashboards/infrastructure/templates/button_card_templates.yaml index 48f936bf..51b71123 100644 --- a/config/dashboards/infrastructure/templates/button_card_templates.yaml +++ b/config/dashboards/infrastructure/templates/button_card_templates.yaml @@ -158,6 +158,27 @@ bearstone_infra_prune_tile: state: - font-weight: 700 +bearstone_infra_volume_prune_tile: + template: bearstone_infra_tile + show_state: true + icon: mdi:database-remove + label: >- + [[[ + return variables.subtitle ? variables.subtitle : "Hold to prune unused volumes"; + ]]] + hold_action: + action: call-service + service: button.press + service_data: + entity_id: '[[[ return variables.prune_button ]]]' + confirmation: + text: '[[[ return "Prune unused volumes on " + (variables.name ? variables.name : "host") + "?" ]]]' + styles: + icon: + - color: var(--secondary-text-color) + state: + - font-weight: 700 + bearstone_infra_apt_prune_tile: template: bearstone_infra_prune_tile show_state: true diff --git a/config/dashboards/infrastructure/views/03_docker_containers.yaml b/config/dashboards/infrastructure/views/03_docker_containers.yaml index e26df6a8..4184f20f 100644 --- a/config/dashboards/infrastructure/views/03_docker_containers.yaml +++ b/config/dashboards/infrastructure/views/03_docker_containers.yaml @@ -4,10 +4,11 @@ # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Infrastructure View - Docker containers -# Related Issue: 1560 +# Related Issue: 1560, 1725 # Container status + restart controls (Portainer integration). # ------------------------------------------------------------------- # Notes: Uses `binary_sensor.*_status` and `button.*_restart_container` entities (Portainer integration). +# Notes: 2026.5 Portainer volume prune controls are exposed through confirmed host maintenance popups; kill/recreate buttons are intentionally not exposed broadly. ###################################################################### title: Docker diff --git a/config/dashboards/kiosk/partials/locator_sections.yaml b/config/dashboards/kiosk/partials/locator_sections.yaml index f82a6db9..18dbfc3f 100644 --- a/config/dashboards/kiosk/partials/locator_sections.yaml +++ b/config/dashboards/kiosk/partials/locator_sections.yaml @@ -53,35 +53,28 @@ - entity: person.carlo state_not: home row: - type: attribute - entity: sensor.carlo_place + entity: sensor.carlo_location_display name: Carlo Location - type: conditional conditions: - entity: person.stacey state_not: home row: - type: attribute - entity: sensor.stacey_place - attribute: place_name + entity: sensor.stacey_location_display name: Stacey Location - type: conditional conditions: - entity: person.justin state_not: home row: - type: attribute - entity: sensor.justin_place - attribute: place_name + entity: sensor.justin_location_display name: Justin Location - type: conditional conditions: - entity: person.paige state_not: home row: - type: attribute - entity: sensor.paige_place - attribute: place_name + entity: sensor.paige_location_display name: Paige Location - show_state: true show_name: true diff --git a/config/dashboards/overview/partials/home_sections.yaml b/config/dashboards/overview/partials/home_sections.yaml index 396af704..b17c2850 100644 --- a/config/dashboards/overview/partials/home_sections.yaml +++ b/config/dashboards/overview/partials/home_sections.yaml @@ -66,36 +66,28 @@ - entity: person.carlo state_not: home row: - type: attribute - entity: sensor.carlo_place - attribute: formatted_place + entity: sensor.carlo_location_display name: Carlo Location - type: conditional conditions: - entity: person.stacey state_not: home row: - type: attribute - entity: sensor.stacey_place - attribute: formatted_place + entity: sensor.stacey_location_display name: Stacey Location - type: conditional conditions: - entity: person.justin state_not: home row: - type: attribute - entity: sensor.justin_place - attribute: formatted_place + entity: sensor.justin_location_display name: Justin Location - type: conditional conditions: - entity: person.paige state_not: home row: - type: attribute - entity: sensor.paige_place - attribute: formatted_place + entity: sensor.paige_location_display name: Paige Location state_color: false - type: custom:mushroom-template-card diff --git a/config/packages/README.md b/config/packages/README.md index 977c5990..99a99c72 100755 --- a/config/packages/README.md +++ b/config/packages/README.md @@ -46,11 +46,12 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this | [lightning.yaml](lightning.yaml) | Blitzortung lightning counter monitoring with snoozeable push actions. | `sensor.blitzortung_lightning_counter`, `input_boolean.snooze_lightning`, notify engine actions | | [logbook_activity_feed.yaml](logbook_activity_feed.yaml) | Dummy `sensor.activity_feed` + helper to write clean Activity entries (Issue #1550). | `sensor.activity_feed`, `script.send_to_logbook` | | [mariadb_monitoring.yaml](mariadb_monitoring.yaml) | MariaDB health sensors and Lovelace dashboard snippet for recorder stats. | `sensor.mariadb_status`, `sensor.database_size` | +| [llmvision.yaml](llmvision.yaml) | Vision-backed garage-can and front-door package checks with rate-limited, downscaled OpenAI calls for package detection. | `input_button.llmvision_*`, `binary_sensor.front_door_packages_present`, `llmvision.stream_analyzer` | | [docker_infrastructure.yaml](docker_infrastructure.yaml) | Docker host patching telemetry, container/stack Repairs automation, 20-minute Joanna escalation for persistent container outages using stable configured monitor membership, and weekly scheduled prune actions across docker_10/14/17/69. | `sensor.docker_*_apt_status`, `binary_sensor.*_stack_status`, `sensor.docker_stacks_down_count`, `repairs.create`, `script.joanna_dispatch` | | [proxmox.yaml](proxmox.yaml) | Proxmox runtime and disk pressure monitoring with Repairs + Joanna dispatch for sustained node degradations, plus nightly Frigate reboot. | `binary_sensor.proxmox*_runtime_healthy`, `sensor.proxmox*_disk_used_percentage`, `repairs.create`, `script.joanna_dispatch`, `button.qemu_docker2_101_reboot` | -| [synology_dsm.yaml](synology_dsm.yaml) | Synology DSM integration health normalization for Carlo-NAS01 and Carlo-NVR, with Repairs + Joanna dispatch on sustained integration, security, or storage problems. | `binary_sensor.carlo_*_synology_problem`, `sensor.carlo_*_synology_problem_summary`, `repairs.create`, `script.joanna_dispatch` | -| [infrastructure.yaml](infrastructure.yaml) | Normalized WAN/DNS/backup/domain/cert health, Glances-backed Docker host disk pressure with bounded safe Joanna cleanup, and website uptime/latency SLO signals for Infrastructure dashboards, plus nightly backup verification and monthly Joanna HA log hygiene review with GitHub issue follow-up. | `sensor.docker_*_disk_used_percentage`, `automation.docker_host_disk_pressure_monitor`, `binary_sensor.infra_website_uptime_slo_breach`, `binary_sensor.infra_website_latency_degraded`, `automation.infra_backup_nightly_verification`, `script.joanna_dispatch` | -| [onenote_indexer.yaml](onenote_indexer.yaml) | OneNote indexer health/status monitoring for Joanna, failure-repair automation, and a daily duplicate-delete maintenance request. | `sensor.onenote_indexer_last_job_status`, `binary_sensor.onenote_indexer_last_job_successful` | +| [synology_dsm.yaml](synology_dsm.yaml) | Synology DSM integration health normalization for Carlo-NAS01 and Carlo-NVR, with outage-aware Joanna-first handling for lone post-outage volume warnings and Repairs escalation for persistent or non-outage problems. | `binary_sensor.carlo_*_synology_problem`, `sensor.carlo_*_synology_problem_summary`, `binary_sensor.powerwall_grid_status`, `repairs.create`, `script.joanna_dispatch` | +| [infrastructure.yaml](infrastructure.yaml) | Normalized WAN/DNS/backup/domain/cert health, Nebula Sync primary/backup Pi-hole consistency monitoring with Joanna dispatch, Glances-backed Docker host disk pressure with Joanna-only warning cleanup and critical Repairs, and website uptime/latency SLO signals for Infrastructure dashboards, plus nightly backup verification and monthly Joanna HA log hygiene review with GitHub issue follow-up. | `sensor.infra_nebula_sync_dns_consistency`, `binary_sensor.infra_nebula_sync_degraded`, `sensor.docker_*_disk_used_percentage`, `automation.infra_nebula_sync_health_dispatch`, `automation.docker_host_disk_pressure_monitor`, `binary_sensor.infra_website_uptime_slo_breach`, `binary_sensor.infra_website_latency_degraded`, `automation.infra_backup_nightly_verification`, `script.joanna_dispatch` | +| [onenote_indexer.yaml](onenote_indexer.yaml) | OneNote indexer health/status monitoring for Joanna, explicit index-health confirmation, failure-repair automation, and a daily duplicate-delete maintenance request. | `sensor.onenote_indexer_last_job_status`, `binary_sensor.onenote_indexer_last_job_successful`, `binary_sensor.onenote_indexer_index_healthy` | | [mqtt_status.yaml](mqtt_status.yaml) | Command-line MQTT broker reachability probe with Spook Repairs escalation and Joanna troubleshooting dispatch on outage. | `binary_sensor.mqtt_status_raw`, `binary_sensor.mqtt_broker_problem`, `repairs.create`, `rest_command.bearclaw_command` | | [mariadb.yaml](mariadb.yaml) | MariaDB recorder health and capacity snapshots with hourly live metrics, weekly admin/recorder polling, and stats-ready numeric sensors. | `sensor.mariadb_status`, `sensor.database_size` | | [processmonitor.yaml](processmonitor.yaml) | Root filesystem disk-pressure monitoring with immediate digest/logbook notes at 80%, Joanna review after 10 minutes above 80%, and delayed phone alerts only if the issue stays unresolved after dispatch. | `sensor.disk_use_percent`, `repairs.create`, `script.joanna_dispatch`, `tts.clear_cache` | @@ -59,7 +60,7 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this | [telegram_bot.yaml](telegram_bot.yaml) | Legacy Telegram transport marker for BearClaw; the shared `joanna_send_telegram` helper now forwards through the codex_appliance direct Telegram API. | `rest_command.bearclaw_telegram_send`, `script.joanna_send_telegram` | | [phynplus.yaml](phynplus.yaml) | Phyn shutoff automations with push + Activity feed + Repairs issues for leak events. | `valve.phyn_shutoff_valve`, `binary_sensor.phyn_leak_test_running`, `repairs.create` | | [water_delivery.yaml](water_delivery.yaml) | ReadyRefresh delivery date helper with night-before + garage door Alexa reminders, plus helper-change audit logging and Telegram confirmations. | `input_datetime.water_delivery_date`, `script.send_to_logbook`, `script.joanna_send_telegram`, `notify.alexa_media_garage` | -| [vacation_mode.yaml](vacation_mode.yaml) | Auto-enable vacation mode after 24 hours away or no bed use, track sitter analytics/secure-house checks, and deliver Chromecast-first vacation briefings with a garage Alexa welcome. | `input_boolean.vacation_mode`, `input_boolean.house_sitter_present`, `sensor.vacation_house_sitter_*`, `group.garage_doors`, `lock.front_door`, `script.notify_engine`, `script.joanna_send_telegram` | +| [vacation_mode.yaml](vacation_mode.yaml) | Auto-enable vacation mode after 24 hours away or no bed use, track sitter analytics/secure-house checks, and deliver exact sitter-facing briefings with garage Alexa support. | `input_boolean.vacation_mode`, `input_boolean.house_sitter_present`, `input_datetime.vacation_house_sitter_*`, `sensor.vacation_house_sitter_*`, `group.garage_doors`, `lock.front_door`, `script.notify_engine`, `script.joanna_send_telegram` | | [maintenance_log.yaml](maintenance_log.yaml) | Joanna maintenance webhook ingest for water softener salt with idempotent event handling, Activity feed logging, and recorder-backed helper history for long-term graphing. | `automation.maintenance_log_joanna_webhook_ingest`, `input_number.water_softener_salt_total_added_lb`, `counter.water_softener_salt_event_count`, `sensor.water_softener_salt_days_since_last_add` | | [powerwall.yaml](powerwall.yaml) | Track Tesla Powerwall grid status, push live outage tracking to mobile targets, and shed loads automatically when off-grid (alerts include Activity feed + Repairs). | `binary_sensor.powerwall_grid_status`, `sensor.powerwall_*`, `script.notify_live_activity`, `repairs.create` | | [tesla_model_y.yaml](tesla_model_y.yaml) | Remind the garage and parents to plug in the Model Y after low-battery arrivals and after 8 PM when it is home but not charging. | `sensor.spaceship_battery_level`, `switch.spaceship_charge`, `notify.alexa_media_garage`, `script.notify_engine` | diff --git a/config/packages/bearclaw.yaml b/config/packages/bearclaw.yaml index 2923438c..237ff4c8 100644 --- a/config/packages/bearclaw.yaml +++ b/config/packages/bearclaw.yaml @@ -16,6 +16,7 @@ # Notes: v2 intake is the primary HA contract; legacy command/ingest routes remain appliance-side shims. # Notes: Command payload supports async_only for automation-first queueing when immediate inline handling is not required. # Notes: Command payload supports optional metadata for HA dispatch context snapshots. +# Notes: HA automation dispatches default to BearClaw's ops domain so wording like NAS "health" cannot route to the health coach. # Notes: Blog: https://www.vcloudinfo.com/2026/03/joanna-dispatch-telemetry-home-assistant-infrastructure-dashboard/ ###################################################################### @@ -44,6 +45,10 @@ rest_command: "context": {{ context | default(none) | tojson }}, "callback": {{ callback | default(none) | tojson }} }, + "routing": { + "domainHint": {{ domain_hint | default('ops', true) | tojson }}, + "laneHint": {{ lane_hint | default('joanna.ops', true) | tojson }} + }, "replyTargets": [ { "type": "ha", diff --git a/config/packages/docker_infrastructure.yaml b/config/packages/docker_infrastructure.yaml index 66dea236..25269f4f 100644 --- a/config/packages/docker_infrastructure.yaml +++ b/config/packages/docker_infrastructure.yaml @@ -4,18 +4,20 @@ # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Docker Infrastructure - Host patching and container alerts -# Related Issue: 1632, 1584 +# Related Issue: 1632, 1584, 1739 # APT results and container down repairs. # ------------------------------------------------------------------- -# Notes: Hosts run weekly Wed 12:00 APT job and POST JSON to webhooks. +# Notes: Hosts run daily read-only APT pending checks plus Mon/Thu 12:00 APT jobs. # Notes: Reboots are handled directly on each host by apt_weekly.sh. -# Notes: Reboot staggering: docker_14 first, docker_69 second, docker_10 third. +# Notes: Reboot staggering: docker_14, docker_69, docker_17, docker_10. # Notes: Container monitoring is dynamic with binary_sensor status preferred over switch state. # Notes: Weekly Joanna reconcile checks discovered container switches vs configured group members. # Notes: Includes Portainer stack status repairs, 20-minute Joanna dispatch for persistent container outages, and scheduled image prune. # Notes: Outage escalation keys off the configured monitored group so host-wide telemetry drops do not fall out of scope before the delayed Joanna dispatch runs. # Notes: Weekly reconcile should replace retired container-name switches with the current container-ID-prefixed discovery set. # Notes: Tapple is now served by `games_hub` on `/tapple/`; do not keep a standalone `tapple` container switch in the monitored group. +# Notes: Teslamate and crystalsoftwashsolutions are live services and should remain in the monitored group when their discovery switches are present. +# Notes: Infra Info was removed; BearClaw Admin is the planning snapshot surface. ###################################################################### input_datetime: @@ -85,6 +87,7 @@ switch: - switch.college_budget_app_container_2 - switch.cruise_tracker_container - switch.cruise_tracker_container_2 + - switch.crystalsoftwashsolutions_container - switch.dashy_container - switch.dashy_container_2 - switch.docker_socket_proxy_container @@ -111,8 +114,6 @@ switch: - switch.home_assistant_container_2 - switch.imposter_container - switch.imposter_container_2 - - switch.infra_info_container - - switch.infra_info_container_2 - switch.kingcrafthomes_container - switch.kingcrafthomes_container_2 - switch.lmediaservices_container @@ -137,14 +138,23 @@ switch: - switch.pihole_secondary_container_2 - switch.poker_tracker_container - switch.poker_tracker_container_2 + - switch.portainer_agent_container + - switch.portainer_agent_container_2 - switch.portainer_container - switch.portainer_container_2 + - switch.postgres_webhooks_backup_container - switch.postgres_webhooks_engine_container - switch.postgres_webhooks_engine_container_2 + - switch.rc_price_checker_container + - switch.rc_price_checker_container_2 - switch.redis_webhooks_engine_container - switch.redis_webhooks_engine_container_2 - switch.rvtools_ppt_web_container - switch.rvtools_ppt_web_container_2 + - switch.teslamate_backup_container + - switch.teslamate_container + - switch.teslamate_database_container + - switch.teslamate_grafana_container - switch.tugtainer_agent_container - switch.tugtainer_agent_container_2 - switch.tugtainer_container @@ -155,6 +165,7 @@ switch: - switch.unifi_container_2 - switch.webhooks_engine_container - switch.webhooks_engine_container_2 + - switch.wordpress_db_backup_container - switch.wordpress_db_container - switch.wordpress_db_container_2 - switch.wordpress_wp_container @@ -893,6 +904,7 @@ automation: updated: "{{ payload.get('updated', false) | bool }}" reboot_required: "{{ payload.get('reboot_required', false) | bool }}" packages: "{{ payload.get('packages', 0) | int(0) }}" + security_packages: "{{ payload.get('security_packages', 0) | int(0) }}" message: "{{ payload.get('message', '') | string }}" helpers: docker_10: @@ -913,17 +925,20 @@ automation: last_result: input_text.apt_docker_69_last_result host_helpers: "{{ helpers[host_id] if host_id in helpers else none }}" result: >- + {% set security = ' (' ~ security_packages ~ ' SEC)' if security_packages > 0 else '' %} {% if not success %} ERROR{% if (message | trim) != '' %}: {{ message | trim }}{% endif %} {% elif updated %} - UPDATED {{ packages }} PKGS{% if reboot_required %} (REBOOT REQ){% endif %} + UPDATED {{ packages }} PKGS{{ security }}{% if reboot_required %} (REBOOT REQ){% endif %} + {% elif packages > 0 %} + PENDING {{ packages }} PKGS{{ security }}{% if reboot_required %} (REBOOT REQ){% endif %} {% elif reboot_required %} NO UPDATES (REBOOT REQ) {% else %} NO UPDATES {% endif %} log_message: >- - {{ host_id }} updated {{ packages }} package{% if packages != 1 %}s{% endif %}{% if reboot_required %}; reboot required{% endif %}. + {{ host_id }} updated {{ packages }} package{% if packages != 1 %}s{% endif %}{% if security_packages > 0 %} ({{ security_packages }} security){% endif %}{% if reboot_required %}; reboot required{% endif %}. condition: - condition: template value_template: "{{ host_helpers is not none }}" diff --git a/config/packages/infrastructure.yaml b/config/packages/infrastructure.yaml index 563cd22f..b1074585 100644 --- a/config/packages/infrastructure.yaml +++ b/config/packages/infrastructure.yaml @@ -9,11 +9,15 @@ # Related Issue: 1584 # Notes: Home dashboard consumes `infra_*` entities for exceptions-only alerts. # Notes: Domain warning threshold is <30 days; critical threshold is <14 days. -# Notes: Nightly Duplicati verification is performed by codex_appliance after the Duplicati retry window because HA backup entities are not available. +# Notes: Nightly Duplicati verification runs at 08:00 after the 05:30 Duplicati job and docker_14 reboot window. +# Notes: Duplicati transport/API errors are logged only; repairs are reserved for proven failed or stale backups. +# Notes: Duplicati failure Repairs enable a recovery poll that clears the Repair after a later successful run. # Notes: Monthly HA log hygiene review requests Telegram + GitHub issue follow-up only; Joanna must wait for approval before any changes. # Notes: Numeric WAN telemetry exposes state_class so recorder can keep long-term statistics. # Notes: Docker host root disk usage uses Glances-backed normalized sensors; raw Glances sensors are recorder/logbook-filtered. # Notes: Disk-pressure dispatch allows bounded safe cleanup of disposable caches and old generated backup artifacts, but not live data or restarts. +# Notes: Warning-level Docker host disk pressure is Joanna-only; Repairs are reserved for critical pressure. +# Notes: Nebula Sync DNS consistency compares primary/backup Pi-hole answers and dispatches Joanna on sustained drift or container loss. ###################################################################### input_text: @@ -26,6 +30,13 @@ input_text: docker_69_disk_pressure_band: name: "docker_69 disk pressure band" max: 20 + infra_nebula_sync_health_band: + name: "Nebula Sync health band" + max: 20 + +input_boolean: + infra_duplicati_backup_repair_active: + name: "Duplicati backup repair active" command_line: - sensor: @@ -58,6 +69,23 @@ command_line: command: "curl -fsS https://api.ipify.org || echo unknown" scan_interval: 900 + - sensor: + name: Infra Nebula Sync DNS Consistency + unique_id: infra_nebula_sync_dns_consistency + command: >- + /bin/bash -c 'primary=192.168.10.10; secondary=192.168.10.14; host=GTG-PF45FK6F; fqdn=GTG-PF45FK6F.fordst.com; ip=192.168.10.117; q(){ dig +time=2 +tries=1 +short @"$1" "$2" A 2>/dev/null | tr -d "\r" | sort | tr "\n" "," | sed "s/,$//"; }; r(){ dig +time=2 +tries=1 +short @"$1" -x "$2" 2>/dev/null | tr -d "\r" | sed "s/\.$//" | sort | tr "\n" "," | sed "s/,$//"; }; p_short=$(q "$primary" "$host"); s_short=$(q "$secondary" "$host"); p_fqdn=$(q "$primary" "$fqdn"); s_fqdn=$(q "$secondary" "$fqdn"); p_rev=$(r "$primary" "$ip"); s_rev=$(r "$secondary" "$ip"); status=mismatch; if [ "$p_short" = "$ip" ] && [ "$s_short" = "$ip" ] && [ "$p_fqdn" = "$ip" ] && [ "$s_fqdn" = "$ip" ] && [ -n "$p_rev" ] && [ "$p_rev" = "$s_rev" ]; then status=ok; fi; printf "{\"status\":\"%s\",\"host\":\"%s\",\"expected_ip\":\"%s\",\"primary_short\":\"%s\",\"secondary_short\":\"%s\",\"primary_fqdn\":\"%s\",\"secondary_fqdn\":\"%s\",\"primary_reverse\":\"%s\",\"secondary_reverse\":\"%s\"}\n" "$status" "$host" "$ip" "$p_short" "$s_short" "$p_fqdn" "$s_fqdn" "$p_rev" "$s_rev"' + scan_interval: 300 + value_template: "{{ value_json.status | default('unknown') }}" + json_attributes: + - host + - expected_ip + - primary_short + - secondary_short + - primary_fqdn + - secondary_fqdn + - primary_reverse + - secondary_reverse + template: - sensor: - name: "Infra External IP" @@ -203,6 +231,45 @@ template: {% set service_state = states('binary_sensor.pihole_status') %} {{ switch_state != 'on' or service_state in ['off', 'unavailable', 'unknown'] }} + - name: "Infra Nebula Sync Degraded" + unique_id: infra_nebula_sync_degraded + device_class: problem + state: >- + {% set dns_state = states('sensor.infra_nebula_sync_dns_consistency') | lower %} + {% set portainer_known = [ + expand('binary_sensor.nebula_sync_status') | count > 0, + expand('binary_sensor.nebula_sync_status_2') | count > 0, + expand('sensor.nebula_sync_state') | count > 0, + expand('sensor.nebula_sync_state_2') | count > 0, + expand('switch.nebula_sync_container') | count > 0, + expand('switch.nebula_sync_container_2') | count > 0 + ] | select('equalto', true) | list | count > 0 %} + {% set portainer_ok = [ + is_state('binary_sensor.nebula_sync_status', 'on'), + is_state('binary_sensor.nebula_sync_status_2', 'on'), + (states('sensor.nebula_sync_state') | lower) == 'running', + (states('sensor.nebula_sync_state_2') | lower) == 'running', + is_state('switch.nebula_sync_container', 'on'), + is_state('switch.nebula_sync_container_2', 'on') + ] | select('equalto', true) | list | count > 0 %} + {{ dns_state != 'ok' or (portainer_known and not portainer_ok) }} + attributes: + dns_consistency: "{{ states('sensor.infra_nebula_sync_dns_consistency') }}" + host: "{{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'host') }}" + expected_ip: "{{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'expected_ip') }}" + primary_short: "{{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'primary_short') }}" + secondary_short: "{{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'secondary_short') }}" + primary_fqdn: "{{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'primary_fqdn') }}" + secondary_fqdn: "{{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'secondary_fqdn') }}" + primary_reverse: "{{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'primary_reverse') }}" + secondary_reverse: "{{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'secondary_reverse') }}" + nebula_status: "{{ states('binary_sensor.nebula_sync_status') }}" + nebula_status_2: "{{ states('binary_sensor.nebula_sync_status_2') }}" + nebula_state: "{{ states('sensor.nebula_sync_state') }}" + nebula_state_2: "{{ states('sensor.nebula_sync_state_2') }}" + pihole_secondary_status: "{{ states('binary_sensor.pihole_secondary_status') }}" + pihole_secondary_status_2: "{{ states('binary_sensor.pihole_secondary_status_2') }}" + - name: "Infra UPS On Battery" unique_id: infra_ups_on_battery device_class: problem @@ -371,6 +438,121 @@ automation: data: issue_id: infra_website_latency_degraded + - alias: "Infrastructure - Nebula Sync Health Dispatch" + id: infra_nebula_sync_health_dispatch + description: "Dispatch Joanna when Nebula Sync DNS consistency or container telemetry stays degraded." + mode: queued + trigger: + - platform: state + entity_id: binary_sensor.infra_nebula_sync_degraded + to: "on" + for: "00:10:00" + id: degraded + - platform: state + entity_id: binary_sensor.infra_nebula_sync_degraded + to: "off" + for: "00:02:00" + id: recovered + - platform: homeassistant + event: start + id: reconcile + - platform: time_pattern + minutes: "/30" + id: reconcile + variables: + issue_id: infra_nebula_sync_degraded + dns_state: "{{ states('sensor.infra_nebula_sync_dns_consistency') }}" + previous_band: "{{ states('input_text.infra_nebula_sync_health_band') | lower }}" + degraded: "{{ is_state('binary_sensor.infra_nebula_sync_degraded', 'on') }}" + nebula_status: "{{ states('binary_sensor.nebula_sync_status') }}" + nebula_status_alt: "{{ states('binary_sensor.nebula_sync_status_2') }}" + nebula_state: "{{ states('sensor.nebula_sync_state') }}" + nebula_state_alt: "{{ states('sensor.nebula_sync_state_2') }}" + pihole_secondary_status: "{{ states('binary_sensor.pihole_secondary_status') }}" + pihole_secondary_status_alt: "{{ states('binary_sensor.pihole_secondary_status_2') }}" + action: + - choose: + - conditions: "{{ degraded and previous_band != 'warning' }}" + sequence: + - service: repairs.remove + continue_on_error: true + data: + issue_id: "{{ issue_id }}" + - service: script.joanna_dispatch + data: + trigger_context: "HA automation infra_nebula_sync_health_dispatch (Infrastructure - Nebula Sync Health Dispatch)" + source: "home_assistant_automation.infra_nebula_sync_health_dispatch.warning" + summary: "Nebula Sync DNS consistency or container health is degraded" + entity_ids: + - sensor.infra_nebula_sync_dns_consistency + - binary_sensor.infra_nebula_sync_degraded + - binary_sensor.nebula_sync_status + - binary_sensor.nebula_sync_status_2 + - sensor.nebula_sync_state + - sensor.nebula_sync_state_2 + - binary_sensor.pihole_secondary_status + - binary_sensor.pihole_secondary_status_2 + diagnostics: >- + issue_id={{ issue_id }}, + dns_consistency={{ dns_state }}, + host={{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'host') }}, + expected_ip={{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'expected_ip') }}, + primary_short={{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'primary_short') }}, + secondary_short={{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'secondary_short') }}, + primary_fqdn={{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'primary_fqdn') }}, + secondary_fqdn={{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'secondary_fqdn') }}, + primary_reverse={{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'primary_reverse') }}, + secondary_reverse={{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'secondary_reverse') }}, + nebula_status={{ nebula_status }}, + nebula_status_2={{ nebula_status_alt }}, + nebula_state={{ nebula_state }}, + nebula_state_2={{ nebula_state_alt }}, + pihole_secondary_status={{ pihole_secondary_status }}, + pihole_secondary_status_2={{ pihole_secondary_status_alt }}, + primary_dns=192.168.10.10, + backup_dns=192.168.10.14 + request: >- + Investigate Nebula Sync on docker_14 and the backup Pi-hole sync path. + Verify both Pi-holes answer the GTG-PF45FK6F short name, FQDN, and reverse lookup consistently. + Check nebula_sync container status, Docker health, recent sync logs, and primary/replica Pi-hole API reachability. + If confidence is high, perform safe remediation such as a one-time Nebula Sync run or restarting only the nebula_sync container. + Do not restart Pi-hole or change DHCP/custom DNS records unless diagnostics prove data drift and the action is safe. + Reply with resolved=true/false, root_cause, action_taken, verification, and next_action_required=true/false. + domain_hint: ops + lane_hint: joanna.ops + - service: script.send_to_logbook + data: + topic: "DNS" + message: >- + Nebula Sync DNS consistency is degraded ({{ dns_state }}); Joanna investigation requested without opening a Repair. + - service: input_text.set_value + target: + entity_id: input_text.infra_nebula_sync_health_band + data: + value: warning + - conditions: "{{ not degraded and previous_band in ['warning', 'unavailable'] }}" + sequence: + - service: repairs.remove + continue_on_error: true + data: + issue_id: "{{ issue_id }}" + - service: script.send_to_logbook + data: + topic: "DNS" + message: "Nebula Sync DNS consistency recovered; Joanna-only warning state cleared." + - service: input_text.set_value + target: + entity_id: input_text.infra_nebula_sync_health_band + data: + value: normal + - conditions: "{{ not degraded and previous_band not in ['normal', 'warning', 'unavailable'] }}" + sequence: + - service: input_text.set_value + target: + entity_id: input_text.infra_nebula_sync_health_band + data: + value: normal + - alias: "Docker Host Disk Pressure Monitor" id: docker_host_disk_pressure_monitor description: "Track Docker host root disk pressure from normalized Glances sensors and dispatch Joanna on band changes." @@ -480,15 +662,10 @@ automation: value: "critical" - conditions: "{{ current_band == 'warning' and previous_band not in ['warning', 'critical'] }}" sequence: - - service: repairs.create + - service: repairs.remove + continue_on_error: true data: issue_id: "{{ issue_id }}" - severity: warning - persistent: true - title: "{{ host_name }} disk pressure warning ({{ disk_pct | round(1) }}%)" - description: >- - {{ host_name }} root disk usage is elevated. - Plan cleanup before capacity reaches critical levels. - service: script.joanna_dispatch data: trigger_context: "HA automation docker_host_disk_pressure_monitor (Docker Host Disk Pressure Monitor - Warning)" @@ -519,7 +696,7 @@ automation: topic: "DOCKER" message: >- {{ host_name }} disk usage warning at {{ disk_pct | round(1) }}%. - Repair {{ issue_id }} opened and Joanna investigation requested. + Joanna investigation requested without opening a warning Repair. - service: input_text.set_value target: entity_id: "{{ band_entity }}" @@ -527,19 +704,14 @@ automation: value: "warning" - conditions: "{{ current_band == 'warning' and previous_band == 'critical' }}" sequence: - - service: repairs.create + - service: repairs.remove + continue_on_error: true data: issue_id: "{{ issue_id }}" - severity: warning - persistent: true - title: "{{ host_name }} disk pressure warning ({{ disk_pct | round(1) }}%)" - description: >- - {{ host_name }} root disk usage is elevated but no longer critical. - Continue cleanup before capacity reaches critical levels again. - service: script.send_to_logbook data: topic: "DOCKER" - message: "{{ host_name }} disk usage dropped from critical to warning at {{ disk_pct | round(1) }}%." + message: "{{ host_name }} disk usage dropped from critical to warning at {{ disk_pct | round(1) }}%. Critical Repair cleared; Joanna continues handling warning-level cleanup." - service: input_text.set_value target: entity_id: "{{ band_entity }}" @@ -578,14 +750,28 @@ automation: mode: single trigger: - platform: time - at: "06:45:00" + at: "08:00:00" + id: nightly + - platform: time_pattern + minutes: "15" + id: recovery_poll + - platform: time_pattern + minutes: "45" + id: recovery_poll + condition: + - condition: template + value_template: >- + {{ trigger is not defined or trigger.id != 'recovery_poll' + or is_state('input_boolean.infra_duplicati_backup_repair_active', 'on') }} action: - variables: + trigger_source: "{{ trigger.id if trigger is defined and trigger.id is defined else 'manual' }}" + verifier_reason: "{{ 'ha_failure_followup' if trigger_source == 'recovery_poll' else 'ha_nightly' }}" trigger_context: "HA automation infra_backup_nightly_verification (Infrastructure - Backup Nightly Verification)" duplicati_state: "{{ states('switch.duplicati_container') }}" - action: rest_command.bearclaw_duplicati_verify data: - reason: "ha_nightly" + reason: "{{ verifier_reason }}" response_variable: duplicati_verify - service: script.send_to_logbook data: @@ -605,6 +791,7 @@ automation: verify_backup_name: "{{ verify_detail.get('backupName', 'Docker_Configs') }}" verify_latest_result: "{{ verify_detail.get('latestResult', {}) if verify_detail is mapping else {} }}" verify_last_success: "{{ verify_detail.get('lastSuccessfulRun', {}) if verify_detail is mapping else {} }}" + verify_transport_issue: "{{ verify_status in ['api_error', 'unknown'] }}" - choose: - conditions: "{{ verify_healthy }}" sequence: @@ -612,47 +799,77 @@ automation: continue_on_error: true data: issue_id: infra_duplicati_backup_failure + - service: repairs.remove + continue_on_error: true + data: + issue_id: user_infra_duplicati_backup_failure + - service: input_boolean.turn_off + target: + entity_id: input_boolean.infra_duplicati_backup_repair_active + - conditions: "{{ verify_transport_issue }}" + sequence: + - service: script.send_to_logbook + data: + topic: "BACKUP" + message: >- + Duplicati verifier could not prove backup health because the verification service returned + status {{ verify_status }} with issue {{ verify_issue }}. No repair card was opened because + this is verifier transport state, not a confirmed backup failure. default: - - service: repairs.create - data: - issue_id: infra_duplicati_backup_failure - title: "Duplicati nightly backup verification failed" - description: >- - {{ verify_summary }} - Backup={{ verify_backup_name }}; - status={{ verify_status }}; - last_result={{ verify_latest_result.get('endedAt', 'n/a') }}; - last_success={{ verify_last_success.get('endedAt', 'n/a') }}. - severity: error - persistent: true - - service: script.joanna_dispatch - data: - trigger_context: "{{ trigger_context }}" - source: "home_assistant_automation.infra_backup_nightly_verification" - summary: "Nightly Duplicati backup verification failed" - entity_ids: - - "switch.duplicati_container" - diagnostics: >- - scheduled_time=06:45:00, - duplicati_container={{ duplicati_state }}, - verifier_http_status={{ verify_http_status }}, - verifier_status={{ verify_status }}, - verifier_issue={{ verify_issue }}, - backup_name={{ verify_backup_name }}, - latest_result={{ verify_latest_result.get('endedAt', 'n/a') }}, - last_success={{ verify_last_success.get('endedAt', 'n/a') }} - request: >- - Investigate the Duplicati backup job {{ verify_backup_name }}. - The codex_appliance verifier reported status {{ verify_status }} with issue {{ verify_issue }}. - Use the Duplicati API or UI directly, resolve the failure if possible, and verify a successful run before closing out. - Reply with explicit status fields: - resolved=true/false, - backup_status, - last_success_time, - root_cause, - action_taken, - verification, - next_action_required=true/false. + - service: input_boolean.turn_on + target: + entity_id: input_boolean.infra_duplicati_backup_repair_active + - choose: + - conditions: "{{ trigger_source != 'recovery_poll' }}" + sequence: + - service: repairs.create + data: + issue_id: infra_duplicati_backup_failure + title: "Duplicati nightly backup verification failed" + description: >- + {{ verify_summary }} + Backup={{ verify_backup_name }}; + status={{ verify_status }}; + last_result={{ verify_latest_result.get('endedAt', 'n/a') }}; + last_success={{ verify_last_success.get('endedAt', 'n/a') }}. + severity: error + persistent: true + - service: script.joanna_dispatch + data: + trigger_context: "{{ trigger_context }}" + source: "home_assistant_automation.infra_backup_nightly_verification" + summary: "Nightly Duplicati backup verification failed" + entity_ids: + - "switch.duplicati_container" + diagnostics: >- + scheduled_time=08:00:00, + duplicati_container={{ duplicati_state }}, + verifier_http_status={{ verify_http_status }}, + verifier_status={{ verify_status }}, + verifier_issue={{ verify_issue }}, + backup_name={{ verify_backup_name }}, + latest_result={{ verify_latest_result.get('endedAt', 'n/a') }}, + last_success={{ verify_last_success.get('endedAt', 'n/a') }} + request: >- + Investigate the Duplicati backup job {{ verify_backup_name }}. + The codex_appliance verifier reported status {{ verify_status }} with issue {{ verify_issue }}. + Use the Duplicati API or UI directly, resolve the failure if possible, and verify a successful run before closing out. + Home Assistant will re-check this verifier every 30 minutes after dispatch and clear the Repair automatically once the backup is healthy. + Reply with explicit status fields: + resolved=true/false, + backup_status, + last_success_time, + root_cause, + action_taken, + verification, + next_action_required=true/false. + default: + - service: script.send_to_logbook + data: + topic: "BACKUP" + message: >- + Duplicati recovery follow-up still reports {{ verify_status }} for {{ verify_backup_name }}: + {{ verify_issue }}. Existing Repair remains open; Joanna was not dispatched again. - alias: "Infrastructure - Monthly HA Log Hygiene Review" id: infra_monthly_log_hygiene_review diff --git a/config/packages/llmvision.yaml b/config/packages/llmvision.yaml index ae1db883..ab31557b 100644 --- a/config/packages/llmvision.yaml +++ b/config/packages/llmvision.yaml @@ -7,7 +7,9 @@ # Trigger with input_button.llmvision_garbage_check, input_button.llmvision_front_door_package_check, or front door person activity to update vision-backed package sensors. # ------------------------------------------------------------------- # Notes: LLMVision analyzes camera.garagecam; expects strict "on"/"off" output. -# Notes: Front-door package detection waits 3 minutes after person activity or 20 seconds after the front door relocks, then analyzes a short live stream for better package accuracy. +# Notes: Front-door package detection waits 3 minutes after person +# activity, skips routine relocks unless a package is already present, +# and rate-limits/downscales OpenAI vision calls for cost control. # Docs: https://llmvision.gitbook.io/getting-started/usage/image-analyzer ###################################################################### @@ -148,11 +150,12 @@ automation: front door person activity {% endif %} prompt_text: >- - Examine these front door camera frames for delivery packages. Focus on the porch and doorstep area - near the wall and doormat in the lower-right part of the image. Treat cardboard boxes, padded mailers, - poly bags, and shopping bags as packages. Ignore the street, cars, landscaping, and anything not resting - on the porch or doorstep. If any package is clearly visible, respond exactly: on. If no package is - clearly visible, respond exactly: off. No other words. + Examine these front door camera frames for delivery packages. Focus on the porch and doorstep area, + especially the lower-right and bottom-right edge near the door. Treat cardboard boxes, padded mailers, + poly bags, shopping bags, and white shipping bags as packages. A package can be partially cut off by + the edge of the image or timestamp overlay and should still count as visible if part of the bag, box, + label, or mailer is visible on the porch. Ignore the street, cars, landscaping, and anything not + resting on the porch or doorstep. Respond exactly: on or off. No other words. action: - choose: - conditions: @@ -165,6 +168,15 @@ automation: value_template: "{{ trigger.id == 'front_door_locked' }}" sequence: - delay: "00:00:20" + - condition: template + value_template: >- + {% set last = states('input_datetime.llmvision_front_door_last_run') %} + {% set minutes_since_last = 999 if last in ['unknown', 'unavailable', 'none', ''] else ((as_timestamp(now()) - as_timestamp(last)) / 60) | int(999) %} + {{ trigger.id == 'manual_check' + or (trigger.id == 'person_count' and minutes_since_last >= 15) + or (trigger.id == 'front_door_locked' + and is_state('input_boolean.front_door_packages_present', 'on') + and minutes_since_last >= 5) }} - service: llmvision.stream_analyzer response_variable: llmvision_result data: @@ -173,11 +185,11 @@ automation: message: "{{ prompt_text }}" image_entity: - camera.frontdoorbell - duration: 6 - max_frames: 5 + duration: 4 + max_frames: 3 include_filename: false - target_width: 1920 - max_tokens: 16 + target_width: 1280 + max_tokens: 32 expose_images: true - variables: normalized_response: "{{ llmvision_result.response_text | default('') | trim | lower }}" diff --git a/config/packages/onenote_indexer.yaml b/config/packages/onenote_indexer.yaml index febe3775..2e05f449 100644 --- a/config/packages/onenote_indexer.yaml +++ b/config/packages/onenote_indexer.yaml @@ -7,10 +7,12 @@ # Polls codex_appliance OneNote status and exposes trigger-ready health entities. # ------------------------------------------------------------------- # Notes: Keep onenote indexer monitoring in this package (separate from bearclaw transport). -# Notes: last_status='never' is treated as success to avoid false alerts after restarts. +# Notes: last_status='never' is treated as success only when index health is confirmed. # Notes: Only explicit last_status='error' is treated as failure; unknown/unavailable are neutral. # Notes: HA->Joanna request includes trigger context so Telegram progress messages can identify origin. # Notes: Creates/clears a Spook Repair issue and requests Joanna remediation on failures. +# Notes: Index health requires pages, chunks, no pending embeddings, and a healthy embedding worker. +# Notes: Recovery clear is polled so stale Repairs do not linger after the indexer recovers. # Notes: Daily Joanna recap should be plain-English; only surface detailed index metrics when something materially changes or fails. ###################################################################### @@ -43,13 +45,23 @@ template: state: >- {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} {% set sync = payload.get('sync', {}) if payload is mapping else {} %} + {% set index = payload.get('index', {}) if payload is mapping else {} %} + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} {% set raw = (sync.get('last_status', '') | string | lower) %} - {% if raw in ['ok', 'success', 'never'] %} + {% set pages = index.get('pages') | int(0) %} + {% set chunks = index.get('chunks') | int(0) %} + {% set pending = index.get('pending_embeddings') | int(999999) %} + {% set worker_status = worker.get('lastStatus', '') | string | lower %} + {% set worker_running = worker.get('running', false) | bool %} + {% set index_healthy = pages > 0 and chunks > 0 and pending == 0 and worker_status == 'ok' and not worker_running %} + {% if raw in ['ok', 'success'] or (raw == 'never' and index_healthy) %} success {% elif raw == 'running' %} running {% elif raw == 'error' %} error + {% elif raw == 'never' %} + unknown {% else %} unknown {% endif %} @@ -86,6 +98,12 @@ template: {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} {% set index = payload.get('index', {}) if payload is mapping else {} %} {{ index.get('chunks') }} + embedding_worker_status: >- + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} + {{ worker.get('lastStatus') }} + embedding_worker_last_run_at: >- + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} + {{ worker.get('lastRunAt') }} last_metrics: >- {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} {% set sync = payload.get('sync', {}) if payload is mapping else {} %} @@ -103,6 +121,44 @@ template: mdi:alert-circle {% endif %} + - name: OneNote Indexer Index Healthy + unique_id: onenote_indexer_index_healthy + state: >- + {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} + {% set index = payload.get('index', {}) if payload is mapping else {} %} + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} + {% set pages = index.get('pages') | int(0) %} + {% set chunks = index.get('chunks') | int(0) %} + {% set pending = index.get('pending_embeddings') | int(999999) %} + {% set worker_status = worker.get('lastStatus', '') | string | lower %} + {% set worker_running = worker.get('running', false) | bool %} + {{ pages > 0 and chunks > 0 and pending == 0 and worker_status == 'ok' and not worker_running }} + icon: >- + {% if is_state('binary_sensor.onenote_indexer_index_healthy', 'on') %} + mdi:notebook-check + {% else %} + mdi:notebook-remove + {% endif %} + attributes: + pages: >- + {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} + {% set index = payload.get('index', {}) if payload is mapping else {} %} + {{ index.get('pages') }} + chunks: >- + {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} + {% set index = payload.get('index', {}) if payload is mapping else {} %} + {{ index.get('chunks') }} + pending_embeddings: >- + {% set payload = state_attr('sensor.onenote_indexer_status_payload', 'indexer') or {} %} + {% set index = payload.get('index', {}) if payload is mapping else {} %} + {{ index.get('pending_embeddings') }} + embedding_worker_status: >- + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} + {{ worker.get('lastStatus') }} + embedding_worker_last_run_at: >- + {% set worker = state_attr('sensor.onenote_indexer_status_payload', 'embeddingWorker') or {} %} + {{ worker.get('lastRunAt') }} + - name: OneNote Indexer Job Failed unique_id: onenote_indexer_job_failed device_class: problem @@ -199,13 +255,28 @@ automation: - id: onenote_indexer_failure_clear_repair alias: OneNote Indexer - Clear Repair On Recovery - description: Clear the Spook Repair issue when OneNote indexer is healthy again. + description: Clear the Spook Repair issue when OneNote indexer and index health are confirmed healthy again. mode: single trigger: - platform: state entity_id: binary_sensor.onenote_indexer_job_failed to: "off" for: "00:02:00" + - platform: state + entity_id: binary_sensor.onenote_indexer_index_healthy + to: "on" + for: "00:02:00" + - platform: time_pattern + minutes: "20" + - platform: time_pattern + minutes: "50" + condition: + - condition: state + entity_id: binary_sensor.onenote_indexer_job_failed + state: "off" + - condition: state + entity_id: binary_sensor.onenote_indexer_index_healthy + state: "on" action: - service: repairs.remove continue_on_error: true @@ -214,4 +285,4 @@ automation: - service: script.send_to_logbook data: topic: "ONENOTE" - message: "OneNote indexer recovered. Spook repair cleared." + message: "OneNote indexer and index health are confirmed healthy. Spook repair cleared." diff --git a/config/packages/phynplus.yaml b/config/packages/phynplus.yaml index 3fe286f6..0929c32b 100755 --- a/config/packages/phynplus.yaml +++ b/config/packages/phynplus.yaml @@ -5,13 +5,36 @@ # ------------------------------------------------------------------- # Phyn Plus Water Shutoff - Leak detection and auto shutoff # Related Issue: 1550 -# Push + Activity feed + Repairs issue while valve is closed. +# Push action buttons + Activity feed + Repairs issue while valve is closed. # ------------------------------------------------------------------- # Info: https://www.vcloudinfo.com/2020/05/phyn-plus-smart-water-shutoff-device.html # HACS: https://github.com/MizterB/homeassistant-phyn # Product: https://amzn.to/2Zy3sbJ ###################################################################### +script: + phyn_send_actionable_leak_notification: + alias: "Phyn Send Actionable Leak Notification" + mode: parallel + fields: + source: + description: "Caller context for trace/debug visibility." + example: "initial" + sequence: + - service: script.notify_engine_two_button + data: + title: "Phyn Leak Detection" + value1: "Phyn is sensing a leak and turned off the water. Please verify." + who: "parents" + group: "Phyn" + title1: "Turn Water On" + action1: "PHYN_WATER_ON" + title2: "Snooze 30m" + action2: "SNOOZE_PHYN_NOTIFICATION" + icon1: "sfsymbols:arrow.up.circle" + icon2: "sfsymbols:clock" + level: "critical" + automation: - alias: 'Phyn ShutOff Notification' id: 78bbd270-ee1e-4b3d-80fd-44ce7c66dab5 @@ -44,18 +67,10 @@ automation: Verify there are no active leaks before restoring water service. - - service: script.notify_engine_two_button + - service: script.phyn_send_actionable_leak_notification data: - title: 'Phyn Leak Detection' - value1: 'Phyn is sensing a leak and turned off the water. Please verify.' - who: 'parents' - group: 'Phyn' - action1: 'PHYN_WATER_ON' - action2: 'SNOOZE_PHYN_NOTIFICATION' - icon1: 'sfsymbols:arrow.up.circle' - icon2: 'sfsymbols:clock' - level: 'critical' - + source: "initial" + - service: persistent_notification.create data: title: '🚨 Phyn Leak Detection Alert' @@ -75,12 +90,9 @@ automation: event_data: action: PHYN_WATER_ON action: - - service: homeassistant.turn_on - entity_id: valve.phyn_shutoff_valve - - - service: persistent_notification.dismiss - data: - notification_id: 'phyn_leak_detection' + - service: valve.open_valve + target: + entity_id: valve.phyn_shutoff_valve - service: script.notify_engine data: @@ -105,17 +117,9 @@ automation: action: SNOOZE_PHYN_NOTIFICATION action: - delay: '00:30:00' - - service: script.notify_engine_two_button + - service: script.phyn_send_actionable_leak_notification data: - title: 'Phyn Leak Detection' - value1: 'Phyn is sensing a leak and turned off the water. Please verify.' - who: 'parents' - group: 'Phyn' - action1: 'PHYN_WATER_ON' - action2: 'SNOOZE_PHYN_NOTIFICATION' - icon1: 'sfsymbols:arrow.up.circle' - icon2: 'sfsymbols:clock' - level: 'critical' + source: "snooze" - alias: Dismiss Phyn Notification When Valve Opens id: 3f295bb8-8925-4b22-PHYN-9fe079b295a8 diff --git a/config/packages/powerwall.yaml b/config/packages/powerwall.yaml index dc99cadc..45f8d69d 100755 --- a/config/packages/powerwall.yaml +++ b/config/packages/powerwall.yaml @@ -12,6 +12,7 @@ # Notes: Read more https://www.vcloudinfo.com/2018/01/going-green-to-save-some-green-in-2018.html | Existing Issue #272 # Tesla Powerwall added via UI Integration # Notes: Live outage tracking derives its chronometer anchor from binary_sensor.powerwall_grid_status.last_changed. +# Notes: Camera PoE restore requires grid online and Powerwall charge at least 50%. ###################################################################### # Binary Sensors: # - binary_sensor.powerwall_charging ............. battery_charging (on=charging) @@ -375,7 +376,7 @@ automation: - alias: "Restore PoE ports when grid returns" id: 1ae8b5c5-8627-4a44-8c8a-5bf8ca5e1bf5 - description: "Turn camera PoE ports back on after grid has been stable" + description: "Turn camera PoE ports back on after grid has returned and battery is at least 50%" mode: single trigger: - platform: state @@ -386,7 +387,7 @@ automation: minutes: 60 - platform: numeric_state entity_id: sensor.powerwall_charge - above: 90 + above: 49.9 condition: - condition: or @@ -403,9 +404,12 @@ automation: - condition: state entity_id: switch.poe_garage_port_6_poe state: 'off' + - condition: state + entity_id: binary_sensor.powerwall_grid_status + state: 'on' - condition: numeric_state entity_id: sensor.powerwall_charge - above: 90 + above: 49.9 action: - service: switch.turn_on target: @@ -417,7 +421,7 @@ automation: - service: script.notify_engine data: title: "Grid restored - PoE ports re-enabled" - value1: "Power is stable. Camera PoE ports 3-6 were turned back on automatically." + value1: "Grid is online and Powerwall charge is at least 50%. Camera PoE ports 3-6 were turned back on automatically." who: 'family' group: 'information' diff --git a/config/packages/stats.yaml b/config/packages/stats.yaml index bbaea690..6f2b753f 100755 --- a/config/packages/stats.yaml +++ b/config/packages/stats.yaml @@ -4,10 +4,11 @@ # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Stats - Historical metrics and AI notifications. +# Related Issue: 1279 # Build historical stats for AI/alerting. # ------------------------------------------------------------------- -# Contact: @CCOSTAN # Notes: Numeric repo and home counters expose state_class for long-term trend rollups. +# Notes: Tesla charging time now normalizes Tesla Fleet charging state instead of the retired JuiceBox API. ###################################################################### ### Building out some Historical stats for AI. ##################### @@ -69,8 +70,8 @@ sensor: - platform: history_stats name: Tesla Charging time - entity_id: sensor.carlojuice_charging_status - state: 'charging' + entity_id: binary_sensor.tesla_charging_active + state: 'on' type: time end: '{{ now() }}' duration: @@ -79,6 +80,17 @@ sensor: ### Building some interesting stats for tweeting. ### template: + - binary_sensor: + - name: "Tesla Charging Active" + unique_id: stats_tesla_charging_active + icon: mdi:ev-station + availability: >- + {{ states('sensor.spaceship_charging') not in ['unknown', 'unavailable', 'none', ''] or + states('switch.spaceship_charge') not in ['unknown', 'unavailable', 'none', ''] }} + state: >- + {{ states('sensor.spaceship_charging') | lower == 'charging' or + is_state('switch.spaceship_charge', 'on') }} + - sensor: - name: "Number of Sensors" unique_id: stats_number_of_sensors diff --git a/config/packages/synology_dsm.yaml b/config/packages/synology_dsm.yaml index ba4e5598..9704836d 100644 --- a/config/packages/synology_dsm.yaml +++ b/config/packages/synology_dsm.yaml @@ -9,6 +9,7 @@ # Notes: Uses native `synology_dsm` entities for Carlo-NAS01 and Carlo-NVR. # Notes: Joanna dispatches are reserved for integration/security/storage problems, not routine reboot/shutdown controls. # Notes: DSM update availability stays diagnostic context only; it does not trigger remediation by itself. +# Notes: Recent Powerwall outages route lone volume warnings to Joanna first; Repairs open after the recovery grace window if still active. ###################################################################### template: @@ -281,16 +282,47 @@ template: automation: - id: synology_dsm_open_repair_and_dispatch alias: "Synology DSM - Open Repair And Dispatch" - description: "Open a Repairs issue and dispatch Joanna when a Synology problem stays active." + description: "Dispatch Joanna when a Synology problem stays active, and open Repairs after outage-aware grace checks." mode: queued trigger: - - platform: state + - id: initial_dispatch + platform: state entity_id: - binary_sensor.carlo_nas01_synology_problem - binary_sensor.carlo_nvr_synology_problem to: "on" for: "00:10:00" + - id: repair_escalation + platform: state + entity_id: + - binary_sensor.carlo_nas01_synology_problem + - binary_sensor.carlo_nvr_synology_problem + to: "on" + for: "01:00:00" variables: + outage_grace_minutes: 60 + trigger_phase: "{{ trigger.id | default('initial_dispatch', true) }}" + is_repair_escalation: "{{ trigger_phase == 'repair_escalation' }}" + grid_state: "{{ states('binary_sensor.powerwall_grid_status') }}" + grid_changed_minutes: >- + {% if states.binary_sensor.powerwall_grid_status is defined %} + {{ ((as_timestamp(now(), 0) - as_timestamp(states.binary_sensor.powerwall_grid_status.last_changed, 0)) / 60) | round(1) }} + {% else %} + 9999 + {% endif %} + outage_grace_active: >- + {{ grid_state == 'off' or + (grid_state == 'on' and (grid_changed_minutes | float(9999)) <= outage_grace_minutes) }} + outage_context: >- + {% if states.binary_sensor.powerwall_grid_status is not defined %} + Powerwall grid status entity is unavailable to this automation. + {% elif grid_state == 'off' %} + Powerwall grid is currently down; outage began {{ states.binary_sensor.powerwall_grid_status.last_changed }}. + {% elif (grid_changed_minutes | float(9999)) <= outage_grace_minutes %} + Powerwall grid recovered {{ grid_changed_minutes }} minutes ago. + {% else %} + No recent Powerwall outage recovery within {{ outage_grace_minutes }} minutes. + {% endif %} host_name: >- {% if trigger.entity_id == 'binary_sensor.carlo_nas01_synology_problem' %} Carlo-NAS01 @@ -402,32 +434,49 @@ automation: volume_status: "{{ states(volume_status_entity) }}" volume_used: "{{ states(volume_used_entity) }}" dsm_update_state: "{{ states(update_entity) }}" + lone_volume_warning: >- + {{ problem_summary | lower | trim == 'volume status=warning' and + volume_status | lower | trim == 'warning' and + security_state == 'off' }} + joanna_only_outage_grace: >- + {{ not (is_repair_escalation | bool(false)) and + (outage_grace_active | bool(false)) and + (lone_volume_warning | bool(false)) }} + should_create_repair: "{{ not (joanna_only_outage_grace | bool(false)) }}" trigger_context: "HA automation synology_dsm_open_repair_and_dispatch (Synology DSM - Open Repair And Dispatch)" action: - - service: repairs.create - data: - issue_id: "{{ issue_id }}" - title: "{{ host_name }} Synology health issue" - severity: "{{ 'error' if problem_severity == 'error' else 'warning' }}" - persistent: true - description: >- - Home Assistant detected a sustained Synology DSM issue for {{ host_name }}. + - choose: + - conditions: + - condition: template + value_template: "{{ should_create_repair | bool(false) }}" + sequence: + - service: repairs.create + data: + issue_id: "{{ issue_id }}" + title: "{{ host_name }} Synology health issue" + severity: "{{ 'error' if problem_severity == 'error' else 'warning' }}" + persistent: true + description: >- + Home Assistant detected a sustained Synology DSM issue for {{ host_name }}. - summary: {{ problem_summary }} - security_state: {{ security_state }} - volume_status: {{ volume_status }} - volume_used: {{ volume_used }} - dsm_update: {{ dsm_update_state }} - ssh_alias: {{ ssh_alias }} - dsm_url: {{ dsm_url }} + summary: {{ problem_summary }} + security_state: {{ security_state }} + volume_status: {{ volume_status }} + volume_used: {{ volume_used }} + dsm_update: {{ dsm_update_state }} + outage_context: {{ outage_context }} + ssh_alias: {{ ssh_alias }} + dsm_url: {{ dsm_url }} - service: script.joanna_dispatch data: trigger_context: "{{ trigger_context }}" source: "{{ source }}" - summary: "{{ host_name }} Synology DSM problem detected" + summary: >- + {{ host_name }} Synology DSM problem detected{{ ' after recent Powerwall outage' if joanna_only_outage_grace | bool(false) else '' }} entity_ids: "{{ entity_ids }}" diagnostics: >- issue_id={{ issue_id }}, + trigger_phase={{ trigger_phase }}, severity={{ problem_severity }}, problem_sensor={{ trigger.entity_id }}, problem_summary={{ problem_summary }}, @@ -435,18 +484,27 @@ automation: volume_status={{ volume_status }}, volume_used={{ volume_used }}, dsm_update={{ dsm_update_state }}, + outage_grace_active={{ outage_grace_active }}, + outage_context={{ outage_context }}, + joanna_only_outage_grace={{ joanna_only_outage_grace }}, + repair_created={{ should_create_repair }}, ssh_alias={{ ssh_alias }}, dsm_url={{ dsm_url }} request: >- Investigate {{ host_name }} using the Home Assistant Synology DSM entities first, then DSM or SSH if needed. - Review security status, drive health, volume health, and integration availability. + Review security state, drive condition, volume condition, and integration availability. + If this is a recent Powerwall outage and the only symptom is a volume warning, treat it as post-outage recovery first and monitor before escalating. Do not reboot or shut down the NAS unless explicitly requested. - service: script.send_to_logbook data: topic: "SYNOLOGY" message: >- {{ host_name }} reported a Synology DSM problem for 10 minutes. - Repair {{ issue_id }} opened and Joanna investigation requested. + {% if should_create_repair | bool(false) %} + Repair {{ issue_id }} opened and Joanna investigation requested. + {% else %} + Joanna investigation requested without opening a Repair during the post-outage recovery grace window. + {% endif %} Summary: {{ problem_summary }}. - id: synology_dsm_clear_repair_on_recovery diff --git a/config/packages/vacation_mode.yaml b/config/packages/vacation_mode.yaml index dcfb1b60..b64a03a1 100644 --- a/config/packages/vacation_mode.yaml +++ b/config/packages/vacation_mode.yaml @@ -9,7 +9,8 @@ # Related Issue: 793 # Notes: Vacation mode auto-enables after 24 hours of family absence or 24 hours without bed use while the family is away. # Notes: General vacation speech uses Chromecast only; the garage Alexa welcome is the one local-device exception. -# Notes: Visit analytics come from native entity attributes plus recorder-backed sensors instead of extra helper entities. +# Notes: Visit analytics use recorder-backed visit counts plus arrival/departure helpers for accurate durations. +# Notes: Dorm zones are reported as away locations, not Bear Stone home. ###################################################################### input_boolean: @@ -22,6 +23,25 @@ input_boolean: icon: mdi:account-key initial: off +input_datetime: + vacation_house_sitter_last_arrival: + name: Vacation House Sitter Last Arrival + has_date: true + has_time: true + vacation_house_sitter_last_departure: + name: Vacation House Sitter Last Departure + has_date: true + has_time: true + +input_number: + vacation_house_sitter_last_visit_minutes: + name: Vacation House Sitter Last Visit Minutes + min: 0 + max: 1440 + step: 0.1 + mode: box + unit_of_measurement: min + sensor: - platform: history_stats name: Vacation House Sitter Visit Count @@ -51,38 +71,35 @@ template: {% endif %} last_arrival_at: >- {% set vacation_start = as_timestamp(states.input_boolean.vacation_mode.last_changed, 0) %} - {% set raw = state_attr('automation.vacation_mode_house_sitter_arrival', 'last_triggered') %} + {% set raw = states('input_datetime.vacation_house_sitter_last_arrival') %} {% set ts = as_timestamp(raw, 0) %} - {% if ts >= vacation_start and ts > 0 %} - {{ as_local(raw).isoformat() }} + {% if is_state('input_boolean.vacation_mode', 'on') and ts >= vacation_start and ts > 0 %} + {{ as_local(as_datetime(raw)).isoformat() }} {% else %} none {% endif %} last_departure_at: >- {% set vacation_start = as_timestamp(states.input_boolean.vacation_mode.last_changed, 0) %} - {% set raw = state_attr('automation.vacation_mode_house_sitter_departure', 'last_triggered') %} + {% set raw = states('input_datetime.vacation_house_sitter_last_departure') %} {% set ts = as_timestamp(raw, 0) %} - {% if ts >= vacation_start and ts > 0 %} - {{ as_local(raw).isoformat() }} + {% if is_state('input_boolean.vacation_mode', 'on') and ts >= vacation_start and ts > 0 %} + {{ as_local(as_datetime(raw)).isoformat() }} {% else %} none {% endif %} last_visit_minutes: >- {% set vacation_start = as_timestamp(states.input_boolean.vacation_mode.last_changed, 0) %} - {% set arrival = state_attr('automation.vacation_mode_house_sitter_arrival', 'last_triggered') %} - {% set departure = state_attr('automation.vacation_mode_house_sitter_departure', 'last_triggered') %} - {% set arrival_ts = as_timestamp(arrival, 0) %} - {% set departure_ts = as_timestamp(departure, 0) %} - {% if arrival_ts >= vacation_start and departure_ts >= arrival_ts %} - {{ ((departure_ts - arrival_ts) / 60) | round(1) }} + {% set departure_ts = as_timestamp(states('input_datetime.vacation_house_sitter_last_departure'), 0) %} + {% if is_state('input_boolean.vacation_mode', 'on') and departure_ts >= vacation_start %} + {{ states('input_number.vacation_house_sitter_last_visit_minutes') | float(0) }} {% else %} 0 {% endif %} hours_since_last_visit: >- {% set vacation_start = as_timestamp(states.input_boolean.vacation_mode.last_changed, 0) %} - {% set departure = state_attr('automation.vacation_mode_house_sitter_departure', 'last_triggered') %} + {% set departure = states('input_datetime.vacation_house_sitter_last_departure') %} {% set departure_ts = as_timestamp(departure, 0) %} - {% if departure_ts >= vacation_start %} + {% if is_state('input_boolean.vacation_mode', 'on') and departure_ts >= vacation_start %} {{ ((as_timestamp(now()) - departure_ts) / 3600) | round(1) }} {% else %} 0 @@ -160,6 +177,7 @@ automation: - platform: state entity_id: group.family to: 'home' + for: "00:02:00" condition: - condition: template @@ -255,7 +273,7 @@ automation: entry_delay: >- {{ '00:05:00' if trigger.id == 'garage' else '00:02:00' }} vacation_start_ts: "{{ as_timestamp(states.input_boolean.vacation_mode.last_changed, 0) }}" - last_departure_raw: "{{ state_attr('automation.vacation_mode_house_sitter_departure', 'last_triggered') }}" + last_departure_raw: "{{ states('input_datetime.vacation_house_sitter_last_departure') }}" last_departure_ts: "{{ as_timestamp(last_departure_raw, 0) }}" visit_number: "{{ (states('sensor.vacation_house_sitter_visit_count') | int(0)) + 1 }}" away_duration: >- @@ -286,17 +304,57 @@ automation: This is the first house-sitter visit for this vacation. {% endif %} carlo_location: >- + {% set person_state = states('person.carlo') %} {% set place = states('sensor.carlo_place') %} - {{ place if place not in ['unknown', 'unavailable', 'none', ''] else states('person.carlo') }} + {% set label = place if place not in ['unknown', 'unavailable', 'none', ''] else person_state %} + {% if person_state == 'driving' or label.startswith('Driving') %} + driving near {{ label | replace('Driving near ', '') | replace('Driving', '') | trim }} + {% elif person_state == 'home' %} + at Bear Stone home + {% elif 'Dorm' in label %} + away at {{ label }} + {% else %} + at {{ label }} + {% endif %} stacey_location: >- + {% set person_state = states('person.stacey') %} {% set place = states('sensor.stacey_place') %} - {{ place if place not in ['unknown', 'unavailable', 'none', ''] else states('person.stacey') }} + {% set label = place if place not in ['unknown', 'unavailable', 'none', ''] else person_state %} + {% if person_state == 'driving' or label.startswith('Driving') %} + driving near {{ label | replace('Driving near ', '') | replace('Driving', '') | trim }} + {% elif person_state == 'home' %} + at Bear Stone home + {% elif 'Dorm' in label %} + away at {{ label }} + {% else %} + at {{ label }} + {% endif %} justin_location: >- + {% set person_state = states('person.justin') %} {% set place = states('sensor.justin_place') %} - {{ place if place not in ['unknown', 'unavailable', 'none', ''] else states('person.justin') }} + {% set label = place if place not in ['unknown', 'unavailable', 'none', ''] else person_state %} + {% if person_state == 'driving' or label.startswith('Driving') %} + driving near {{ label | replace('Driving near ', '') | replace('Driving', '') | trim }} + {% elif person_state == 'home' %} + at Bear Stone home + {% elif 'Dorm' in label %} + away at {{ label }} + {% else %} + at {{ label }} + {% endif %} paige_location: >- + {% set person_state = states('person.paige') %} {% set place = states('sensor.paige_place') %} - {{ place if place not in ['unknown', 'unavailable', 'none', ''] else states('person.paige') }} + {% set label = place if place not in ['unknown', 'unavailable', 'none', ''] else person_state %} + {% if person_state == 'driving' or label.startswith('Driving') %} + driving near {{ label | replace('Driving near ', '') | replace('Driving', '') | trim }} + {% elif person_state == 'home' %} + at Bear Stone home + {% elif 'Dorm' in label %} + away at {{ label }} + {% else %} + at {{ label }} + {% endif %} garbage_message: >- {% set day = now().strftime('%a') %} {% if day in ['Tue', 'Sat'] %} @@ -316,8 +374,8 @@ automation: House sitter arrival detected through the {{ entry_label }}. The family was last home {{ away_duration }} ago. {{ visit_context }} - Carlo is at {{ carlo_location }}. Stacey is at {{ stacey_location }}. - Justin is at {{ justin_location }}. Paige is at {{ paige_location }}. + Carlo is {{ carlo_location }}. Stacey is {{ stacey_location }}. + Justin is {{ justin_location }}. Paige is {{ paige_location }}. Please check Molly's food, water, and litter box. {{ package_message }} {{ garbage_message }} @@ -341,6 +399,12 @@ automation: entity_id: - input_boolean.house_sitter_present + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.vacation_house_sitter_last_arrival + data: + timestamp: "{{ now().timestamp() }}" + - service: automation.trigger target: entity_id: automation.late_night_helper @@ -376,6 +440,7 @@ automation: - service: script.speech_processing data: media_player: media_player.livingroomcc + speech_direct: true speech_message: "{{ arrival_message }}" - alias: 'Vacation Mode Sitter Checklist Follow-Up' @@ -411,6 +476,7 @@ automation: - service: script.speech_processing data: media_player: media_player.livingroomcc + speech_direct: true speech_message: "{{ checklist_message }}" - alias: 'Vacation Mode House Sitter Departure' @@ -442,12 +508,27 @@ automation: exit_label: >- {{ 'garage door' if trigger.id == 'garage' else 'front door' }} visit_count: "{{ states('sensor.vacation_house_sitter_visit_count') | int(0) }}" + visit_minutes_numeric: >- + {% set seconds = (as_timestamp(now()) - as_timestamp(states.input_boolean.house_sitter_present.last_changed, 0)) | int(0) %} + {{ (seconds / 60) | round(1) }} visit_duration: >- {% set seconds = (as_timestamp(now()) - as_timestamp(states.input_boolean.house_sitter_present.last_changed, 0)) | int(0) %} {% set minutes = (seconds // 60) | int(0) %} {% set remainder = (seconds % 60) | int(0) %} {{ minutes }} minute{{ 's' if minutes != 1 else '' }}{% if remainder > 0 %} and {{ remainder }} second{{ 's' if remainder != 1 else '' }}{% endif %} + - service: input_number.set_value + target: + entity_id: input_number.vacation_house_sitter_last_visit_minutes + data: + value: "{{ visit_minutes_numeric }}" + + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.vacation_house_sitter_last_departure + data: + timestamp: "{{ now().timestamp() }}" + - service: homeassistant.turn_off target: entity_id: input_boolean.house_sitter_present @@ -553,7 +634,7 @@ automation: - condition: template value_template: >- {% set vacation_start = as_timestamp(states.input_boolean.vacation_mode.last_changed, 0) %} - {% set last_departure = as_timestamp(state_attr('automation.vacation_mode_house_sitter_departure', 'last_triggered'), 0) %} + {% set last_departure = as_timestamp(states('input_datetime.vacation_house_sitter_last_departure'), 0) %} {% set baseline = last_departure if last_departure >= vacation_start else vacation_start %} {{ is_state('input_boolean.vacation_mode', 'on') and is_state('input_boolean.house_sitter_present', 'off') @@ -567,7 +648,7 @@ automation: - variables: visit_count: "{{ states('sensor.vacation_house_sitter_visit_count') | int(0) }}" vacation_start: "{{ as_timestamp(states.input_boolean.vacation_mode.last_changed, 0) }}" - last_departure: "{{ as_timestamp(state_attr('automation.vacation_mode_house_sitter_departure', 'last_triggered'), 0) }}" + last_departure: "{{ as_timestamp(states('input_datetime.vacation_house_sitter_last_departure'), 0) }}" baseline: >- {{ last_departure if (last_departure | float(0)) >= (vacation_start | float(0)) else vacation_start }} hours_since: "{{ ((as_timestamp(now()) - (baseline | float(0))) / 3600) | round(1) }}" @@ -677,6 +758,7 @@ automation: - service: script.speech_processing data: media_player: media_player.livingroomcc + speech_direct: true speech_message: "{{ hint_message }}" - delay: "00:20:00" diff --git a/config/recorder.yaml b/config/recorder.yaml index c5b8d687..0b57292c 100755 --- a/config/recorder.yaml +++ b/config/recorder.yaml @@ -6,7 +6,7 @@ # Recorder Configuration - database retention and exclusions # Stores HA history while purging noise and controlling DB size. # ------------------------------------------------------------------- -# Notes: Keeps 180 days (1/2 year); excludes vcloudinfo pings, noisy connectivity telemetry, countdown-style alarm helpers, MariaDB snapshot helpers, raw Glances host telemetry, and other high-churn entities; MariaDB via recorder_db_url. +# Notes: Keeps 180 days (1/2 year); excludes vcloudinfo pings, noisy connectivity telemetry, countdown-style alarm helpers, low-value script/button/scene history, MariaDB snapshot helpers, raw Docker/Proxmox/Glances host telemetry, PoE live power, and other high-churn entities; MariaDB via recorder_db_url. ###################################################################### db_url: !secret recorder_db_url purge_keep_days: 180 @@ -14,6 +14,7 @@ auto_purge: true commit_interval: 30 exclude: domains: + - button - camera - device_tracker - event @@ -21,6 +22,8 @@ exclude: - image - media_player - persistent_notification + - scene + - script - sun - update - zone @@ -46,7 +49,6 @@ exclude: - sensor.*_since - sensor.*uptime* - sensor.sun_next_* - - sensor.vpn_client_* - sensor.*_linkquality - sensor.*_link_quality - sensor.*_lqi @@ -57,12 +59,68 @@ exclude: - sensor.*_wifi_signal - sensor.*_wifi_signal_strength - sensor.*_wake_alarm_minutes + - sensor.*_cpu_usage_total* + - sensor.*_memory_usage* - sensor.*_temperature_state - sensor.*_humidity_state - sensor.*_last_seen* - sensor.192_168_10_17_* - sensor.docker14_* - sensor.docker69_* + - sensor.alarm_panel_*_free_memory + - sensor.alarm_panel_*_internal_storage_free_space + - sensor.joanna_minutes_since_* + - sensor.node_proxmox*_containers_running + - sensor.node_proxmox*_cpu_used + - sensor.node_proxmox*_disk_used_percentage + - sensor.node_proxmox*_memory_free + - sensor.node_proxmox*_memory_used + - sensor.node_proxmox*_memory_used_percentage + - sensor.node_proxmox*_total_updates + - sensor.node_proxmox*_virtual_machines_running + - sensor.poe_*_poe_power + - sensor.proxmox1_cpu_load + - sensor.proxmox1_cpu_usage + - sensor.proxmox1_disk_free + - sensor.proxmox1_disk_usage + - sensor.proxmox1_disk_used + - sensor.proxmox1_*_disk_read + - sensor.proxmox1_*_disk_write + - sensor.proxmox1_*_rx + - sensor.proxmox1_*_tx + - sensor.proxmox1_memory_free + - sensor.proxmox1_memory_usage + - sensor.proxmox1_memory_use + - sensor.proxmox1_processor_fan_* + - sensor.proxmox1_running + - sensor.proxmox1_sleeping + - sensor.proxmox1_swap_* + - sensor.proxmox1_threads + - sensor.proxmox1_total + - sensor.proxmox2_cpu_load + - sensor.proxmox2_cpu_usage + - sensor.proxmox2_disk_free + - sensor.proxmox2_disk_usage + - sensor.proxmox2_disk_used + - sensor.proxmox2_*_disk_read + - sensor.proxmox2_*_disk_write + - sensor.proxmox2_*_rx + - sensor.proxmox2_*_tx + - sensor.proxmox2_memory_free + - sensor.proxmox2_memory_usage + - sensor.proxmox2_memory_use + - sensor.proxmox2_processor_fan_* + - sensor.proxmox2_running + - sensor.proxmox2_sleeping + - sensor.proxmox2_swap_* + - sensor.proxmox2_threads + - sensor.proxmox2_total + - sensor.qemu_*_cpu_used + - sensor.qemu_*_disk_used_percentage + - sensor.qemu_*_memory_free + - sensor.qemu_*_memory_used + - sensor.qemu_*_memory_used_percentage + - sensor.storage_proxmox*_*_disk_used_percentage - switch.*_do_not_disturb_* - switch.*_repeat_switch - input_text.l10s_vacuum_* diff --git a/config/script/README.md b/config/script/README.md index 34ffb504..361bc7d4 100755 --- a/config/script/README.md +++ b/config/script/README.md @@ -31,7 +31,7 @@ Reusable scripts that other automations call for notifications, lighting, safety | [notify_live_activity.yaml](notify_live_activity.yaml) | Shared helper for tagged live activity/live update pushes and clear commands. | | [send_to_logbook.yaml](send_to_logbook.yaml) | Generic `logbook.log` helper for Activity feed entries (Issue #1550). | | [joanna_dispatch.yaml](joanna_dispatch.yaml) | Shared AGENT engineer dispatch contract that routes HA-detected issues into Joanna/BearClaw remediation. | -| [speech_engine.yaml](speech_engine.yaml) | TTS/announcement orchestration with templated speech; speech processing also routes garage Echo announcements and office Echo announcements when the office lamp switch indicates active PC work. | +| [speech_engine.yaml](speech_engine.yaml) | TTS/announcement orchestration with templated speech; speech processing can bypass LLM rewriting for exact messages and also routes garage/office Echo announcements. | | [monthly_color_scene.yaml](monthly_color_scene.yaml) | Seasonal lighting scenes used across automations. | | [interior_off.yaml](interior_off.yaml) | One-call "all interior lights off" helper. | @@ -39,7 +39,7 @@ Reusable scripts that other automations call for notifications, lighting, safety `script.joanna_dispatch` is the shared handoff contract from Home Assistant automations into Joanna/BearClaw when Home Assistant detects something worth investigating or fixing. Why we use it: -- Keeps one message schema for remediation context (`trigger_context`, `source`, `summary`, `entity_ids`, `diagnostics`, `request`). +- Keeps one message schema for remediation context (`trigger_context`, `source`, `summary`, `entity_ids`, `diagnostics`, `request`, plus optional routing hints). - Avoids repeating direct `rest_command.bearclaw_command` payload formatting in multiple packages. - Lets Home Assistant stay focused on detection, timing, and routing while Joanna acts as the AGENT engineer for infrastructure triage and recommended remediation. - Makes resolution-trigger automations easier to review, update, and audit. @@ -49,6 +49,7 @@ What the helper normalizes before the BearClaw intake call: - `entity_ids` from either a YAML list or a comma-delimited string. - `diagnostics` from either free text or structured mappings/sequences. - `request` guardrails so Joanna defaults to investigation/recommendation, not blind resets or power-cycles. +- `domain_hint`/`lane_hint` default to BearClaw ops routing so HA infrastructure text does not drift into another domain parser. Current automations that kick off automated resolutions (via `script.joanna_dispatch`): | Automation ID | Alias | File | @@ -58,6 +59,7 @@ Current automations that kick off automated resolutions (via `script.joanna_disp | `onenote_indexer_failure_open_repair` | OneNote Indexer - Open Repair On Failure | [../packages/onenote_indexer.yaml](../packages/onenote_indexer.yaml) | | `infra_backup_nightly_verification` | Infrastructure - Backup Nightly Verification | [../packages/infrastructure.yaml](../packages/infrastructure.yaml) | | `infra_monthly_log_hygiene_review` | Infrastructure - Monthly HA Log Hygiene Review | [../packages/infrastructure.yaml](../packages/infrastructure.yaml) | +| `infra_nebula_sync_health_dispatch` | Infrastructure - Nebula Sync Health Dispatch | [../packages/infrastructure.yaml](../packages/infrastructure.yaml) | | `docker_state_sync_repairs_dynamic` | Docker State Sync - Repairs (Dynamic) | [../packages/docker_infrastructure.yaml](../packages/docker_infrastructure.yaml) | | `docker_group_reconcile_weekly_joanna_review` | Docker Group Reconcile - Weekly Joanna Review | [../packages/docker_infrastructure.yaml](../packages/docker_infrastructure.yaml) | | `docker_host_disk_pressure_monitor` | Docker Host Disk Pressure Monitor | [../packages/infrastructure.yaml](../packages/infrastructure.yaml) | diff --git a/config/script/joanna_dispatch.yaml b/config/script/joanna_dispatch.yaml index 68ff485d..eccc7028 100644 --- a/config/script/joanna_dispatch.yaml +++ b/config/script/joanna_dispatch.yaml @@ -9,6 +9,7 @@ # Notes: Keep this helper generic so package automations can reuse one schema. # Notes: Source defaults to home_assistant_automation.unknown when omitted. # Notes: Automation dispatches are async_only by default so HA calls return quickly while BearClaw works in queue. +# Notes: Automation dispatches default to domain_hint=ops and lane_hint=joanna.ops. # Notes: HA is a dispatcher/integration here; Telegram transport ownership lives in docker_17/codex_appliance. ###################################################################### @@ -31,6 +32,10 @@ joanna_dispatch: description: Extra troubleshooting context. user: description: BearClaw user identity. + domain_hint: + description: BearClaw domain hint. + lane_hint: + description: BearClaw lane hint. sequence: - variables: normalized_context: "{{ trigger_context | default('HA automation', true) }}" @@ -39,6 +44,8 @@ joanna_dispatch: normalized_request: >- {{ request | default('Investigate and recommend remediation. Do not run automated resets or power-cycles unless explicitly requested.', true) }} normalized_user: "{{ user | default('carlo', true) }}" + normalized_domain_hint: "{{ domain_hint | default('ops', true) }}" + normalized_lane_hint: "{{ lane_hint | default('joanna.ops', true) }}" normalized_entity_ids: >- {% if entity_ids is sequence and entity_ids is not string %} {{ entity_ids | map('string') | join(', ') }} @@ -66,4 +73,6 @@ joanna_dispatch: user: "{{ normalized_user }}" source: "{{ normalized_source }}" context: "{{ normalized_context }}" + domain_hint: "{{ normalized_domain_hint }}" + lane_hint: "{{ normalized_lane_hint }}" async_only: true diff --git a/config/script/speech_processing.yaml b/config/script/speech_processing.yaml index 7f7bc405..03b26604 100755 --- a/config/script/speech_processing.yaml +++ b/config/script/speech_processing.yaml @@ -9,6 +9,7 @@ # Related Issue: 798 # Notes: Operates only when family/guest/vacation and speech notification # guards allow it, with time-aware volume. +# Notes: Set `speech_direct: true` for exact visitor-facing announcements that should not be rewritten by the LLM. # Notes: Garage Echo is an always-on announcement target. Office Echo is # added when switch.office_lamp_switch is on, which tracks active PC work. ###################################################################### @@ -20,6 +21,11 @@ speech_processing: - event: openai_instructions_sent event_data: instructions: "{{ speech_message | striptags }}" + + - variables: + direct_speech: "{{ speech_direct | default(false) | bool }}" + routed_media_players: "{{ media_player | string | lower }}" + speech_response: "{{ speech_message | striptags | trim }}" - condition: and conditions: @@ -67,16 +73,20 @@ speech_processing: 0.2 {% endif %} - - service: conversation.process - data: - agent_id: conversation.openai_conversation - text: >- - {{ speech_message }} - response_variable: agent + - choose: + - conditions: + - condition: template + value_template: "{{ not direct_speech }}" + sequence: + - service: conversation.process + data: + agent_id: conversation.openai_conversation + text: >- + {{ speech_message }} + response_variable: agent - - variables: - speech_response: "{{ agent.response.speech.plain.speech }}" - routed_media_players: "{{ media_player | string | lower }}" + - variables: + speech_response: "{{ agent.response.speech.plain.speech }}" - service: tts.cloud_say data: diff --git a/config/shell_scripts/README.md b/config/shell_scripts/README.md index 96994ee7..e7e3a628 100755 --- a/config/shell_scripts/README.md +++ b/config/shell_scripts/README.md @@ -27,7 +27,8 @@ Longer-running shell helpers referenced by automations, packages, or cron. Anyth | File | Why it matters | | --- | --- | | [HAUpdate.sh](HAUpdate.sh) | One-command Home Assistant update helper. | -| [apt_weekly.sh](apt_weekly.sh) | Weekly APT updater that posts webhook status and can schedule reboot when needed. | +| [apt_pending_check.sh](apt_pending_check.sh) | Read-only APT pending-count reporter for the Docker host maintenance dashboard. | +| [apt_weekly.sh](apt_weekly.sh) | Twice-weekly APT updater that posts webhook status and can schedule reboot when needed. | | [apt_reboot_report.sh](apt_reboot_report.sh) | Boot-time webhook status reporter that clears/keeps reboot-required state in HA. | | [gitupdate.sh](gitupdate.sh) | Pull the latest config changes on demand. | | [basketball.yaml](basketball.yaml) | ESPN stat scraping helper used by sensors. | diff --git a/config/shell_scripts/apt_pending_check.service.sample b/config/shell_scripts/apt_pending_check.service.sample new file mode 100644 index 00000000..b245f7a2 --- /dev/null +++ b/config/shell_scripts/apt_pending_check.service.sample @@ -0,0 +1,8 @@ +[Unit] +Description=Daily APT pending check (Home Assistant webhook) +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/apt_pending_check.sh "http://YOUR_HOME_ASSISTANT:8123/api/webhook/YOUR_APT_WEBHOOK" "docker_10" diff --git a/config/shell_scripts/apt_pending_check.sh b/config/shell_scripts/apt_pending_check.sh new file mode 100644 index 00000000..b477bfab --- /dev/null +++ b/config/shell_scripts/apt_pending_check.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Read-only APT pending check for Docker hosts. +# Posts current package counts to the same Home Assistant webhook as apt_weekly.sh. + +WEBHOOK_URL="$1" +HOST_NAME="${2:-$(hostname -s)}" + +if [[ -z "$WEBHOOK_URL" ]]; then + echo "Usage: $0 [host_name]" >&2 + exit 1 +fi + +post_result() { + local success="$1" + local packages="$2" + local security_packages="$3" + local reboot_required="$4" + local message="${5:-}" + + payload=$(cat </dev/null | tail -n +2 | wc -l) +UPGRADABLE="$(apt list --upgradable 2>/dev/null | tail -n +2 || true)" +PACKAGES=0 +SECURITY_PACKAGES=0 +if [[ -n "$UPGRADABLE" ]]; then + PACKAGES="$(printf '%s\n' "$UPGRADABLE" | sed '/^[[:space:]]*$/d' | wc -l)" + SECURITY_PACKAGES="$(printf '%s\n' "$UPGRADABLE" | grep -Ec '(^|,|-)(security|esm-apps-security|esm-infra-security)(,|/|[[:space:]]|$)' || true)" +fi if [[ "$PACKAGES" -gt 0 ]]; then log "Applying upgrades ($PACKAGES pending)" - if apt-get -y upgrade --with-new-pkgs; then + if apt-get "${APT_OPTS[@]}" -y upgrade --with-new-pkgs; then UPDATED=true else MESSAGE="apt-get upgrade failed" @@ -61,6 +68,7 @@ payload=$(cat </dev/null 2>&1 || true if [[ "$REBOOT_DELAY_MINUTES" -eq 0 ]]; then log "Reboot required; rebooting immediately." - shutdown -r now "APT weekly maintenance reboot" + shutdown -r now "APT maintenance reboot" else log "Reboot required; scheduling reboot in ${REBOOT_DELAY_MINUTES} minute(s)." - shutdown -r +"$REBOOT_DELAY_MINUTES" "APT weekly maintenance reboot" + shutdown -r +"$REBOOT_DELAY_MINUTES" "APT maintenance reboot" fi fi diff --git a/config/shell_scripts/apt_weekly.timer.sample b/config/shell_scripts/apt_weekly.timer.sample new file mode 100644 index 00000000..bff12068 --- /dev/null +++ b/config/shell_scripts/apt_weekly.timer.sample @@ -0,0 +1,10 @@ +[Unit] +Description=Run APT maintenance twice weekly + +[Timer] +OnCalendar=Mon,Thu *-*-* 12:00:00 +Persistent=true +Unit=apt_weekly.service + +[Install] +WantedBy=timers.target diff --git a/config/templates/speech/briefing.yaml b/config/templates/speech/briefing.yaml index ce241a6c..af398f39 100755 --- a/config/templates/speech/briefing.yaml +++ b/config/templates/speech/briefing.yaml @@ -7,6 +7,10 @@ # Generates macro prompts for weather, reminders, and AI-driven speech routines. # ------------------------------------------------------------------- # Weather, responsibilities, holidays, air quality, and fact prompts parsed by speech_processing/speech_engine. +# Notes: Dorm zones are away from Bear Stone; only person state `home` +# means someone is physically home at this house. +# Notes: Previous broadcast text is stale context only; current sensor data +# stays authoritative for entry point and action wording. ###################################################################### @@ -86,19 +90,15 @@ {%- endmacro -%} {%- macro window_check() -%} - {% if states.group.entry_points.state != 'off' -%} - {% set comma = joiner(', ') %} - The - {% for state in states.binary_sensor if state.state == 'on' and state.attributes.device_class == 'opening' -%} - {%- endfor %} - {% for group in states.binary_sensor|groupby('state') -%} - {%- for entity in group.list if entity.state == 'on' and entity.attributes.device_class == 'opening' -%} - {{ ' and' if loop.last and not loop.first else comma() }} - {{ entity.attributes.friendly_name }} - {%- endfor -%} - {% endfor %} - need to be closed. - {%- endif -%} + {% set open_entries = states.binary_sensor + | selectattr('state', 'eq', 'on') + | selectattr('attributes.device_class', 'eq', 'opening') + | map(attribute='attributes.friendly_name') + | list %} + {% set entry_count = open_entries | length %} + {% if entry_count > 0 -%} + [Current entry point state: {{ open_entries | join(', ') }} {{ 'is' if entry_count == 1 else 'are' }} still open and {{ 'needs' if entry_count == 1 else 'need' }} to be closed manually. Do not say any physical window or door was closed unless current sensor data says it is closed.] + {%- endif -%} {%- endmacro -%} {%- macro lock_check() -%} @@ -201,6 +201,8 @@ {% set place_state = states(place_sensor_id) if place_sensor_id in states else 'unknown' %} {% set location_label = place_state if place_state not in ['unknown','unavailable','','none'] else person_state %} {% set location_label = 'Away' if location_label in ['unknown','unavailable','','none'] else location_label %} + {% set at_bear_stone = person_state == 'home' %} + {% set at_dorm = 'Dorm' in location_label %} {% set last_changed = as_timestamp(person.last_changed) %} {% set seconds = (as_timestamp(now()) - last_changed) | int(0) if last_changed else 0 %} {% set hours = (seconds // 3600) | int %} @@ -208,9 +210,13 @@ {% set duration = (hours ~ ' hours') if hours >= 1 else (minutes ~ ' minutes') %} {% if person_state == 'driving' %} {% set driving_label = location_label if location_label != 'Away' else '' %} - {{ name }}: Driving{% if driving_label %} near {{ driving_label }}{% endif %}{% if seconds >= 60 %} for {{ duration }}{% endif %} + {{ name }}: driving{% if driving_label %} near {{ driving_label }}{% endif %}{% if seconds >= 60 %} for {{ duration }}{% endif %} + {% elif at_bear_stone %} + {{ name }}: at Bear Stone home{% if seconds >= 60 %} for {{ duration }}{% endif %} + {% elif at_dorm %} + {{ name }}: away at {{ location_label }}{% if seconds >= 60 %} for {{ duration }}{% endif %} {% else %} - {{ name }}: {{ location_label }}{% if seconds >= 60 %} for {{ duration }}{% endif %} + {{ name }}: away at {{ location_label }}{% if seconds >= 60 %} for {{ duration }}{% endif %} {% endif %} {% endif %} {% endmacro %} @@ -239,6 +245,10 @@ {% set time = current_date.strftime('%I:%M %p') %} [Current date time: {{ month }} {{ day }}, {{ year }} {{ time }}] + [Presence rule: Bear Stone home means a resident's person state is exactly `home`. + Justin Dorm and Paige Dorm are away-from-Bear-Stone locations, not home for this house. + Do not welcome or address residents at dorm zones as being home here.] + [Resident: Location: - {{ friendly_location('person.carlo', 'sensor.carlo_place', 'Carlo') }} - {{ friendly_location('person.stacey', 'sensor.stacey_place', 'Stacey') }} @@ -324,6 +334,7 @@ {# call a Random fact about the house or inspiration quote #} {{ ([moon, holiday, days_until ]|random)() }} ] + [Previous broadcast rule: The previous broadcast is stale context only and must not override current Sensor Data. Use it only to avoid repetitive wording. Do not repeat prior claims that a window, door, lock, garage door, or light was changed unless current Sensor Data supports that claim. If current Sensor Data says an entry point is still open and needs closure, say it is still open or needs attention, not that it is closed.] [Previous broadcast for context: "{{ state_attr('sensor.openai_response', 'response') }}" ] {%- endmacro -%} diff --git a/docs/diagrams/bear-stone-proxmox-docker-topology.md b/docs/diagrams/bear-stone-proxmox-docker-topology.md index 30ed3cc8..8815b655 100644 --- a/docs/diagrams/bear-stone-proxmox-docker-topology.md +++ b/docs/diagrams/bear-stone-proxmox-docker-topology.md @@ -4,4 +4,5 @@ - `docker10` is pinned to ProxMox1 (`qemu/105`) based on the current workspace inventory. - `docker14`, `docker17`, and `docker69` are shown as cluster-managed Docker VMs on shared storage because the current AGENTS inventory does not pin them to a single Proxmox node. - The diagram is intentionally hierarchy-first. It shows hosts and containers, not every runtime network edge between services. +- Infra Info is omitted because it was removed; BearClaw Admin owns the replacement infrastructure planning view. - Use the Mermaid source as the editable system-of-record, then import it into Excalidraw for spacing and visual cleanup when a polished graphic is needed. diff --git a/docs/diagrams/bear-stone-proxmox-docker-topology.mmd b/docs/diagrams/bear-stone-proxmox-docker-topology.mmd index c5b8809b..fe30e524 100644 --- a/docs/diagrams/bear-stone-proxmox-docker-topology.mmd +++ b/docs/diagrams/bear-stone-proxmox-docker-topology.mmd @@ -60,7 +60,6 @@ flowchart TD end subgraph D69[docker69 - public edge and utility apps] - D69_INFO[infra_info] D69_CF_WP[cloudflared_wp] D69_CF_KCH[cloudflared_kch] D69_WP_DB[wordpress_db] diff --git a/ha-version-badge.svg b/ha-version-badge.svg index 19c3ffef..0d32d370 100644 --- a/ha-version-badge.svg +++ b/ha-version-badge.svg @@ -1,16 +1,22 @@ 2026.5.3 + + 2026.5.2 + - - + + - + \ No newline at end of file