Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/homeassistant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,24 @@ To enable this, add the following to your config:
| `permissionResponseEntity` | `input_text.opencode_permission_response` | Entity the plugin polls for the user's response |
| `permissionTimeout` | `120` | Seconds to wait for a response before giving up |

> [!IMPORTANT]
> `${ENV_VAR}` expansion reads the **opencode process environment**, not your interactive shell. Exporting the variable in `.zshrc` is not enough when opencode is launched from a GUI app, a launch agent, or any non-login context, and opencode does not read `.env` files. If the variable is missing, remote replies are silently skipped: the notification still arrives, but tapping it does nothing.
>
> A reliable option is a mise `[env]` entry, which does reach opencode:
>
> ```toml
> # ~/.config/mise/config.local.toml (machine-local, untracked)
> [env]
> HA_LONG_LIVED_TOKEN = "eyJhbGciOi..."
> ```
>
> To check what opencode actually sees, run it with `--log-level DEBUG` and answer a prompt. The plugin names the exact missing variable:
>
> ```text
> not polling because haToken references HA_LONG_LIVED_TOKEN,
> which is not set in the opencode process environment
> ```

**How it works:**

1. Plugin sends the `waiting` webhook (with `waiting.id` set to the permission ID or question request ID)
Expand Down
33 changes: 32 additions & 1 deletion packages/homeassistant/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,29 @@ export const HomeAssistantPlugin: Plugin = async ({ client, directory }) => {
);
staleSessionSweep.unref();

function unsetTokenVars(): string[] {
if (!config.haToken) return [];
return [...config.haToken.matchAll(/\$\{([^}]+)}/g)]
.map((match) => match[1] as string)
.filter((name) => !process.env[name]);
}

function describeUnsetTokenVars(names: string[]): string {
return `haToken references ${names.join(', ')}, which ${
names.length > 1 ? 'are' : 'is'
} not set in the opencode process environment`;
}

function describeMissingHaConfig(): string | undefined {
if (!config.haApiUrl) return 'haApiUrl is not set';
if (!config.haToken) return 'haToken is not set';
if (resolveEnvVars(config.haToken)) return undefined;

const unset = unsetTokenVars();
if (unset.length > 0) return describeUnsetTokenVars(unset);
return 'haToken resolved to an empty value';
}

function resolveHaConfig(): { apiUrl: string; token: string; entity: string } | undefined {
if (!config.haApiUrl || !config.haToken) return undefined;
const token = resolveEnvVars(config.haToken);
Expand All @@ -226,12 +249,20 @@ export const HomeAssistantPlugin: Plugin = async ({ client, directory }) => {
const ha = resolveHaConfig();
if (!ha) {
await report(
`${requestId}: Home Assistant config unavailable (haApiUrl, haToken or ${'${ENV_VAR}'} expansion missing), not polling`,
`${requestId}: not polling because ${describeMissingHaConfig() ?? 'the Home Assistant config is unavailable'}`,
undefined,
);
return { kind: 'no-config' };
}

const partiallyUnset = unsetTokenVars();
if (partiallyUnset.length > 0) {
await report(
`${requestId}: ${describeUnsetTokenVars(partiallyUnset)}, so the token is incomplete`,
undefined,
);
}

const timeoutMs = (config.permissionTimeout ?? DEFAULT_PERMISSION_TIMEOUT) * 1000;
const deadline = Date.now() + timeoutMs;
activePermissionPolls.add(requestId);
Expand Down
Loading