ha-opencode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +315 -0
- package/blueprints/opencode_permission_response.yaml +81 -0
- package/blueprints/opencode_state_notifications.yaml +191 -0
- package/dist/cleanup.d.ts +30 -0
- package/dist/cleanup.d.ts.map +1 -0
- package/dist/cleanup.js +125 -0
- package/dist/cleanup.js.map +1 -0
- package/dist/commands.d.ts +24 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +309 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -0
- package/dist/discovery.d.ts +129 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +257 -0
- package/dist/discovery.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/dist/mqtt.d.ts +16 -0
- package/dist/mqtt.d.ts.map +1 -0
- package/dist/mqtt.js +141 -0
- package/dist/mqtt.js.map +1 -0
- package/dist/notify.d.ts +11 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +20 -0
- package/dist/notify.js.map +1 -0
- package/dist/state.d.ts +42 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +282 -0
- package/dist/state.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Stephen Golub
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# ha-opencode
|
|
2
|
+
|
|
3
|
+
OpenCode plugin for Home Assistant integration via MQTT Discovery.
|
|
4
|
+
|
|
5
|
+
Exposes OpenCode session state as Home Assistant entities and allows bidirectional control (responding to permission requests, sending prompts, viewing history).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Session-Based Identity**: Each OpenCode session gets its own HA device and entities
|
|
10
|
+
- **Real-time State Tracking**: Session state, model, current tool, tokens, and cost
|
|
11
|
+
- **Permission Handling**: Approve/reject permission requests from HA or mobile notifications
|
|
12
|
+
- **Send Prompts**: Send prompts to OpenCode sessions via MQTT
|
|
13
|
+
- **Session History**: Retrieve conversation history on demand
|
|
14
|
+
- **Agent Tracking**: Track primary agent and sub-agents being used
|
|
15
|
+
- **Hostname Display**: Identify which machine the session is running on
|
|
16
|
+
- **Automatic Cleanup**: Stale sessions (7+ days inactive) are automatically removed
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### 1. Install the plugin
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install --prefix ~/.config/opencode /path/to/ha-opencode
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Add to OpenCode config
|
|
27
|
+
|
|
28
|
+
Add `"ha-opencode"` to the `plugins` array in `~/.config/opencode/opencode.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"plugins": ["ha-opencode"]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 3. Configure MQTT connection
|
|
37
|
+
|
|
38
|
+
Via environment variables:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export MQTT_HOST=your-mqtt-broker.local
|
|
42
|
+
export MQTT_PORT=1883
|
|
43
|
+
export MQTT_USERNAME=optional-username
|
|
44
|
+
export MQTT_PASSWORD=optional-password
|
|
45
|
+
export HA_DISCOVERY_PREFIX=homeassistant # default
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or via `opencode.json`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"plugins": ["ha-opencode"],
|
|
53
|
+
"ha-opencode": {
|
|
54
|
+
"mqtt": {
|
|
55
|
+
"host": "your-mqtt-broker.local",
|
|
56
|
+
"port": 1883,
|
|
57
|
+
"username": "optional-username",
|
|
58
|
+
"password": "optional-password"
|
|
59
|
+
},
|
|
60
|
+
"ha": {
|
|
61
|
+
"discoveryPrefix": "homeassistant"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Environment variables take precedence over JSON config.
|
|
68
|
+
|
|
69
|
+
## Session-Based Identity
|
|
70
|
+
|
|
71
|
+
Each OpenCode session creates a unique Home Assistant device:
|
|
72
|
+
|
|
73
|
+
- **Session ID**: `ses_46b09b89bffevq6HeMNIkuvk4B` (generated by OpenCode)
|
|
74
|
+
- **Device ID**: `opencode_46b09b89bffevq6HeMNIkuvk4B` (strips `ses_` prefix)
|
|
75
|
+
- **Initial Name**: `OpenCode - {projectName} - Untitled`
|
|
76
|
+
- **After Title**: `OpenCode - {projectName} - {sessionTitle}`
|
|
77
|
+
|
|
78
|
+
This allows running multiple concurrent OpenCode sessions in the same directory, each with their own entities.
|
|
79
|
+
|
|
80
|
+
## Entities Created
|
|
81
|
+
|
|
82
|
+
Per OpenCode session (device), the following entities are created:
|
|
83
|
+
|
|
84
|
+
| Entity | Type | Description |
|
|
85
|
+
|--------|------|-------------|
|
|
86
|
+
| `sensor.opencode_*_state` | Sensor | Session state (idle, working, waiting_permission, error) |
|
|
87
|
+
| `sensor.opencode_*_session_title` | Sensor | Current session/conversation title |
|
|
88
|
+
| `sensor.opencode_*_model` | Sensor | AI model being used (provider/model) |
|
|
89
|
+
| `sensor.opencode_*_current_tool` | Sensor | Currently executing tool |
|
|
90
|
+
| `sensor.opencode_*_tokens_input` | Sensor | Input token count |
|
|
91
|
+
| `sensor.opencode_*_tokens_output` | Sensor | Output token count |
|
|
92
|
+
| `sensor.opencode_*_cost` | Sensor | Session cost in USD |
|
|
93
|
+
| `sensor.opencode_*_last_activity` | Sensor | Timestamp of last activity |
|
|
94
|
+
| `sensor.opencode_*_permission` | Sensor | Permission request status |
|
|
95
|
+
| `sensor.opencode_*_device_id` | Sensor | Device identifier with command/response topics |
|
|
96
|
+
|
|
97
|
+
**Note**: The `*` in entity IDs represents the session ID (e.g., `sensor.opencode_46b09b89bffevq6HeMNIkuvk4B_state`).
|
|
98
|
+
|
|
99
|
+
### State Entity Attributes
|
|
100
|
+
|
|
101
|
+
The `state` entity includes these attributes:
|
|
102
|
+
|
|
103
|
+
- `previous_state`: The state before the current one (for automation conditions)
|
|
104
|
+
- `agent`: Primary agent selected by the user
|
|
105
|
+
- `current_agent`: Sub-agent currently being used (if any)
|
|
106
|
+
- `hostname`: Machine hostname where OpenCode is running
|
|
107
|
+
- `error_message`: Error details when in error state
|
|
108
|
+
|
|
109
|
+
### Device ID Entity Attributes
|
|
110
|
+
|
|
111
|
+
The `device_id` entity includes:
|
|
112
|
+
|
|
113
|
+
- `command_topic`: MQTT topic for sending commands
|
|
114
|
+
- `response_topic`: MQTT topic for receiving responses
|
|
115
|
+
- `state_topic_base`: Base topic for all state updates
|
|
116
|
+
- `device_name`: Friendly device name
|
|
117
|
+
- `session_id`: Full session ID (e.g., `ses_46b09b89bffevq6HeMNIkuvk4B`)
|
|
118
|
+
- `project_name`: Project directory name
|
|
119
|
+
|
|
120
|
+
## MQTT Topics
|
|
121
|
+
|
|
122
|
+
For a session with ID `ses_46b09b89bffevq6HeMNIkuvk4B`:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
# State topics
|
|
126
|
+
opencode/opencode_46b09b89bffevq6HeMNIkuvk4B/state
|
|
127
|
+
opencode/opencode_46b09b89bffevq6HeMNIkuvk4B/state/attributes
|
|
128
|
+
opencode/opencode_46b09b89bffevq6HeMNIkuvk4B/session_title
|
|
129
|
+
opencode/opencode_46b09b89bffevq6HeMNIkuvk4B/model
|
|
130
|
+
opencode/opencode_46b09b89bffevq6HeMNIkuvk4B/availability
|
|
131
|
+
|
|
132
|
+
# Commands & responses
|
|
133
|
+
opencode/opencode_46b09b89bffevq6HeMNIkuvk4B/command
|
|
134
|
+
opencode/opencode_46b09b89bffevq6HeMNIkuvk4B/response
|
|
135
|
+
|
|
136
|
+
# HA Discovery
|
|
137
|
+
homeassistant/sensor/opencode_46b09b89bffevq6HeMNIkuvk4B/state/config
|
|
138
|
+
|
|
139
|
+
# Global cleanup response
|
|
140
|
+
opencode/cleanup/response
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## MQTT Commands
|
|
144
|
+
|
|
145
|
+
Send commands to the `command_topic` (e.g., `opencode/opencode_46b09b89bffevq6HeMNIkuvk4B/command`):
|
|
146
|
+
|
|
147
|
+
### Permission Response
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"command": "permission_response",
|
|
152
|
+
"permission_id": "perm-123",
|
|
153
|
+
"response": "once"
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Response options: `"once"`, `"always"`, or `"reject"`
|
|
158
|
+
|
|
159
|
+
### Send Prompt
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"command": "prompt",
|
|
164
|
+
"text": "Your prompt text here",
|
|
165
|
+
"session_id": "optional-session-id"
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Get History
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"command": "get_history",
|
|
174
|
+
"session_id": "optional-session-id",
|
|
175
|
+
"request_id": "optional-correlation-id"
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Get History Since
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"command": "get_history_since",
|
|
184
|
+
"since": "2024-01-01T00:00:00Z",
|
|
185
|
+
"session_id": "optional-session-id",
|
|
186
|
+
"request_id": "optional-correlation-id"
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Cleanup Stale Sessions
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"command": "cleanup_stale_sessions",
|
|
195
|
+
"max_age_days": 7
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Response published to `opencode/cleanup/response`:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"type": "cleanup_result",
|
|
204
|
+
"sessions_removed": 3,
|
|
205
|
+
"session_ids": ["opencode_abc123...", "opencode_def456..."],
|
|
206
|
+
"max_age_days": 7,
|
|
207
|
+
"timestamp": "2025-01-07T12:00:00Z"
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Stale Session Cleanup
|
|
212
|
+
|
|
213
|
+
Sessions that haven't been active for 7 days are automatically cleaned up:
|
|
214
|
+
|
|
215
|
+
- **Automatic**: Runs on every plugin startup (async, non-blocking)
|
|
216
|
+
- **Manual**: Send `cleanup_stale_sessions` command via MQTT
|
|
217
|
+
|
|
218
|
+
Cleanup removes all HA entities for stale sessions by publishing empty configs to the discovery topics.
|
|
219
|
+
|
|
220
|
+
## Home Assistant Blueprints
|
|
221
|
+
|
|
222
|
+
Pre-built automation blueprints are available in the `blueprints/` directory:
|
|
223
|
+
|
|
224
|
+
| Blueprint | Description |
|
|
225
|
+
|-----------|-------------|
|
|
226
|
+
| `opencode_state_notifications.yaml` | Mobile notifications for task complete, permission required, and errors |
|
|
227
|
+
| `opencode_permission_response.yaml` | Handle approve/reject button taps from notifications |
|
|
228
|
+
|
|
229
|
+
### Installing Blueprints
|
|
230
|
+
|
|
231
|
+
1. Copy blueprints to your HA config:
|
|
232
|
+
```bash
|
|
233
|
+
mkdir -p /path/to/ha/config/blueprints/automation/opencode
|
|
234
|
+
cp blueprints/*.yaml /path/to/ha/config/blueprints/automation/opencode/
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
2. Reload automations in HA (Developer Tools > YAML > Reload Automations)
|
|
238
|
+
|
|
239
|
+
3. Create automations from blueprints:
|
|
240
|
+
- Settings > Automations > Create Automation > Use Blueprint
|
|
241
|
+
- Select the OpenCode blueprint
|
|
242
|
+
- Configure your mobile device and preferences
|
|
243
|
+
|
|
244
|
+
See [ha-card/README.md](ha-card/README.md) for detailed blueprint configuration and automation examples.
|
|
245
|
+
|
|
246
|
+
## Home Assistant Card
|
|
247
|
+
|
|
248
|
+
A custom Lovelace card is included for displaying OpenCode sessions in Home Assistant.
|
|
249
|
+
|
|
250
|
+
See [ha-card/README.md](ha-card/README.md) for installation and usage instructions.
|
|
251
|
+
|
|
252
|
+
> **Note**: The card will be moved to a separate repository ([opencode-card](https://gitlab.com/opencode-home-assistant/opencode-card)) for HACS compatibility. The `ha-card/` directory is structured for easy extraction when that happens.
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Install dependencies
|
|
258
|
+
npm install
|
|
259
|
+
|
|
260
|
+
# Build TypeScript
|
|
261
|
+
npm run build
|
|
262
|
+
|
|
263
|
+
# Watch mode
|
|
264
|
+
npm run dev
|
|
265
|
+
|
|
266
|
+
# Run tests
|
|
267
|
+
npm test
|
|
268
|
+
|
|
269
|
+
# Run tests with coverage
|
|
270
|
+
npm run test:coverage
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Project Structure
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
src/
|
|
277
|
+
index.ts Plugin entry point, defers setup until first session event
|
|
278
|
+
config.ts Configuration loading (env vars + JSON)
|
|
279
|
+
mqtt.ts MQTT client wrapper with wildcard support
|
|
280
|
+
discovery.ts Session-based HA MQTT Discovery
|
|
281
|
+
state.ts State tracking and publishing
|
|
282
|
+
commands.ts MQTT command handler
|
|
283
|
+
cleanup.ts Stale session cleanup
|
|
284
|
+
notify.ts Terminal notification utilities (Kitty OSC 99)
|
|
285
|
+
|
|
286
|
+
blueprints/
|
|
287
|
+
opencode_state_notifications.yaml Mobile notification blueprint
|
|
288
|
+
opencode_permission_response.yaml Permission action handler
|
|
289
|
+
|
|
290
|
+
ha-card/
|
|
291
|
+
src/ Custom Lovelace card source
|
|
292
|
+
dist/ Built card JS
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Troubleshooting
|
|
296
|
+
|
|
297
|
+
### Too many entities in HA
|
|
298
|
+
|
|
299
|
+
Each session creates a new set of entities. Use the cleanup command or rely on automatic cleanup to remove old sessions. Sessions inactive for 7+ days are automatically removed on plugin startup.
|
|
300
|
+
|
|
301
|
+
### Entity naming shows "Untitled"
|
|
302
|
+
|
|
303
|
+
This is the initial name before session title is available. Once the session gets a proper title, the device name updates automatically.
|
|
304
|
+
|
|
305
|
+
### Automation not triggering
|
|
306
|
+
|
|
307
|
+
Check that `previous_state` attribute is available when state changes. Attributes are published BEFORE the state value to ensure they're available when automations trigger.
|
|
308
|
+
|
|
309
|
+
### Kitty notifications not working
|
|
310
|
+
|
|
311
|
+
Ensure your terminal supports OSC 99 (Kitty, iTerm2) and that stdout is a TTY.
|
|
312
|
+
|
|
313
|
+
## License
|
|
314
|
+
|
|
315
|
+
MIT
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
blueprint:
|
|
2
|
+
name: OpenCode Permission Response Handler
|
|
3
|
+
description: >
|
|
4
|
+
Handles approve/reject actions from OpenCode permission notifications.
|
|
5
|
+
|
|
6
|
+
This blueprint listens for notification actions (button taps) and sends
|
|
7
|
+
the appropriate permission response via MQTT.
|
|
8
|
+
|
|
9
|
+
Works with both iOS and Android via the Home Assistant Companion app.
|
|
10
|
+
|
|
11
|
+
Use this together with the OpenCode State Notifications blueprint.
|
|
12
|
+
domain: automation
|
|
13
|
+
author: ha-opencode
|
|
14
|
+
source_url: https://github.com/your-repo/ha-opencode/blob/main/blueprints/opencode_permission_response.yaml
|
|
15
|
+
input: {}
|
|
16
|
+
|
|
17
|
+
mode: parallel
|
|
18
|
+
max_exceeded: silent
|
|
19
|
+
|
|
20
|
+
trigger:
|
|
21
|
+
- platform: event
|
|
22
|
+
event_type: mobile_app_notification_action
|
|
23
|
+
|
|
24
|
+
variables:
|
|
25
|
+
# Get the action string
|
|
26
|
+
action: "{{ trigger.event.data.action | default('') }}"
|
|
27
|
+
|
|
28
|
+
# Check if this is an OpenCode action
|
|
29
|
+
is_opencode_action: >
|
|
30
|
+
{{ action.startswith('OPENCODE_APPROVE_') or action.startswith('OPENCODE_REJECT_') }}
|
|
31
|
+
|
|
32
|
+
is_approve: "{{ action.startswith('OPENCODE_APPROVE_') }}"
|
|
33
|
+
response: "{{ 'once' if is_approve else 'reject' }}"
|
|
34
|
+
|
|
35
|
+
# Get data passed from the notification
|
|
36
|
+
# iOS uses action_data, Android might use data directly
|
|
37
|
+
action_data: >
|
|
38
|
+
{% if trigger.event.data.action_data is defined %}
|
|
39
|
+
{{ trigger.event.data.action_data }}
|
|
40
|
+
{% elif trigger.event.data.data is defined %}
|
|
41
|
+
{{ trigger.event.data.data }}
|
|
42
|
+
{% else %}
|
|
43
|
+
{}
|
|
44
|
+
{% endif %}
|
|
45
|
+
|
|
46
|
+
command_topic: "{{ action_data.command_topic | default('') }}"
|
|
47
|
+
permission_id: "{{ action_data.permission_id | default('') }}"
|
|
48
|
+
device_id: "{{ action_data.device_id | default('') }}"
|
|
49
|
+
|
|
50
|
+
condition:
|
|
51
|
+
- condition: template
|
|
52
|
+
value_template: "{{ is_opencode_action }}"
|
|
53
|
+
|
|
54
|
+
action:
|
|
55
|
+
# Log for debugging
|
|
56
|
+
- service: system_log.write
|
|
57
|
+
data:
|
|
58
|
+
message: >
|
|
59
|
+
OpenCode permission response: action={{ action }}, is_approve={{ is_approve }},
|
|
60
|
+
response={{ response }}, command_topic={{ command_topic }},
|
|
61
|
+
permission_id={{ permission_id }}, device_id={{ device_id }},
|
|
62
|
+
action_data={{ action_data }}
|
|
63
|
+
level: info
|
|
64
|
+
logger: ha-opencode.permission
|
|
65
|
+
|
|
66
|
+
# Check we have required data before publishing
|
|
67
|
+
- condition: template
|
|
68
|
+
value_template: "{{ command_topic != '' and permission_id != '' }}"
|
|
69
|
+
|
|
70
|
+
- service: mqtt.publish
|
|
71
|
+
data:
|
|
72
|
+
topic: "{{ command_topic }}"
|
|
73
|
+
payload: >
|
|
74
|
+
{"command": "permission_response", "permission_id": "{{ permission_id }}", "response": "{{ response }}"}
|
|
75
|
+
|
|
76
|
+
# Log success
|
|
77
|
+
- service: system_log.write
|
|
78
|
+
data:
|
|
79
|
+
message: "OpenCode permission {{ response }} sent for {{ permission_id }}"
|
|
80
|
+
level: info
|
|
81
|
+
logger: ha-opencode.permission
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
blueprint:
|
|
2
|
+
name: OpenCode State Notifications
|
|
3
|
+
description: >
|
|
4
|
+
Send notifications to your mobile device when OpenCode sessions need attention.
|
|
5
|
+
Notifies when:
|
|
6
|
+
- A task completes (idle after working)
|
|
7
|
+
- Permission is required (with approve/reject buttons)
|
|
8
|
+
- An error occurs
|
|
9
|
+
|
|
10
|
+
Works with both iOS and Android via the Home Assistant Companion app.
|
|
11
|
+
|
|
12
|
+
Requires the Permission Response Handler blueprint to handle approve/reject actions.
|
|
13
|
+
domain: automation
|
|
14
|
+
author: ha-opencode
|
|
15
|
+
source_url: https://github.com/your-repo/ha-opencode/blob/main/blueprints/opencode_state_notifications.yaml
|
|
16
|
+
input:
|
|
17
|
+
notify_service:
|
|
18
|
+
name: Notification Service
|
|
19
|
+
description: >
|
|
20
|
+
The notification service to use (e.g., notify.mobile_app_your_phone).
|
|
21
|
+
selector:
|
|
22
|
+
text:
|
|
23
|
+
default: "notify.mobile_app_phone"
|
|
24
|
+
|
|
25
|
+
notify_on_complete:
|
|
26
|
+
name: Notify on Task Complete
|
|
27
|
+
description: Send a notification when a task completes (idle after working).
|
|
28
|
+
default: true
|
|
29
|
+
selector:
|
|
30
|
+
boolean:
|
|
31
|
+
|
|
32
|
+
notify_on_permission:
|
|
33
|
+
name: Notify on Permission Required
|
|
34
|
+
description: Send a notification with approve/reject buttons when permission is needed.
|
|
35
|
+
default: true
|
|
36
|
+
selector:
|
|
37
|
+
boolean:
|
|
38
|
+
|
|
39
|
+
notify_on_error:
|
|
40
|
+
name: Notify on Error
|
|
41
|
+
description: Send a notification when an error occurs.
|
|
42
|
+
default: true
|
|
43
|
+
selector:
|
|
44
|
+
boolean:
|
|
45
|
+
|
|
46
|
+
notification_channel:
|
|
47
|
+
name: Notification Channel (Android)
|
|
48
|
+
description: >
|
|
49
|
+
Android notification channel name.
|
|
50
|
+
You can create channels in the Companion app settings.
|
|
51
|
+
default: "OpenCode"
|
|
52
|
+
selector:
|
|
53
|
+
text:
|
|
54
|
+
|
|
55
|
+
dashboard_path:
|
|
56
|
+
name: Dashboard Path
|
|
57
|
+
description: >
|
|
58
|
+
Path to your OpenCode dashboard for click action (e.g., /lovelace/opencode).
|
|
59
|
+
Leave empty to use default behavior.
|
|
60
|
+
default: "/lovelace/opencode"
|
|
61
|
+
selector:
|
|
62
|
+
text:
|
|
63
|
+
|
|
64
|
+
mode: parallel
|
|
65
|
+
max_exceeded: silent
|
|
66
|
+
|
|
67
|
+
trigger:
|
|
68
|
+
- platform: mqtt
|
|
69
|
+
topic: "opencode/+/state"
|
|
70
|
+
id: state_change
|
|
71
|
+
|
|
72
|
+
variables:
|
|
73
|
+
# The new state from the MQTT payload
|
|
74
|
+
new_state: "{{ trigger.payload }}"
|
|
75
|
+
|
|
76
|
+
# Extract device_id from topic: opencode/{device_id}/state
|
|
77
|
+
device_id: "{{ trigger.topic.split('/')[1] }}"
|
|
78
|
+
|
|
79
|
+
# Build the state_topic_base for entity lookup
|
|
80
|
+
state_topic_base: "opencode/{{ device_id }}"
|
|
81
|
+
|
|
82
|
+
# Find the device_id entity that has this state_topic_base in its attributes
|
|
83
|
+
device_id_entity: >
|
|
84
|
+
{% set ns = namespace(found='') %}
|
|
85
|
+
{% for entity in states.sensor %}
|
|
86
|
+
{% if entity.attributes.get('state_topic_base', '') == state_topic_base %}
|
|
87
|
+
{% set ns.found = entity.entity_id %}
|
|
88
|
+
{% endif %}
|
|
89
|
+
{% endfor %}
|
|
90
|
+
{{ ns.found }}
|
|
91
|
+
|
|
92
|
+
# Check if we found the entity
|
|
93
|
+
entity_found: "{{ device_id_entity != '' }}"
|
|
94
|
+
|
|
95
|
+
# Derive other entity IDs from the device_id entity (they share the same base)
|
|
96
|
+
entity_base: "{{ device_id_entity | replace('_device_id', '') if entity_found else '' }}"
|
|
97
|
+
state_entity: "{{ (entity_base ~ '_state') if entity_found else '' }}"
|
|
98
|
+
session_entity: "{{ (entity_base ~ '_session_title') if entity_found else '' }}"
|
|
99
|
+
|
|
100
|
+
# Get values from entities (with defaults)
|
|
101
|
+
previous_state: "{{ state_attr(state_entity, 'previous_state') | default('unknown') if entity_found else 'unknown' }}"
|
|
102
|
+
project_name: "{{ state_attr(device_id_entity, 'device_name') | default('OpenCode') if entity_found else 'OpenCode' }}"
|
|
103
|
+
command_topic: "{{ state_attr(device_id_entity, 'command_topic') | default('') if entity_found else '' }}"
|
|
104
|
+
permission_id: "{{ state_attr(state_entity, 'permission_id') | default('') if entity_found else '' }}"
|
|
105
|
+
permission_title: "{{ state_attr(state_entity, 'permission_title') | default('Permission needed') if entity_found else 'Permission needed' }}"
|
|
106
|
+
error_message: "{{ state_attr(state_entity, 'error_message') | default('An error occurred') if entity_found else 'An error occurred' }}"
|
|
107
|
+
session_title: "{{ states(session_entity) | default('Task finished') if entity_found else 'Task finished' }}"
|
|
108
|
+
|
|
109
|
+
# Determine if this is a transition we care about
|
|
110
|
+
was_working: "{{ previous_state == 'working' }}"
|
|
111
|
+
|
|
112
|
+
action:
|
|
113
|
+
# Log for debugging (can be removed later)
|
|
114
|
+
- service: system_log.write
|
|
115
|
+
data:
|
|
116
|
+
message: >
|
|
117
|
+
OpenCode notification trigger: new_state={{ new_state }}, previous_state={{ previous_state }},
|
|
118
|
+
device_id={{ device_id }}, entity_found={{ entity_found }}, was_working={{ was_working }}
|
|
119
|
+
level: info
|
|
120
|
+
logger: ha-opencode.blueprint
|
|
121
|
+
|
|
122
|
+
- choose:
|
|
123
|
+
# Permission Required (always notify, regardless of previous state)
|
|
124
|
+
- conditions:
|
|
125
|
+
- condition: template
|
|
126
|
+
value_template: "{{ new_state == 'waiting_permission' }}"
|
|
127
|
+
- condition: template
|
|
128
|
+
value_template: !input notify_on_permission
|
|
129
|
+
sequence:
|
|
130
|
+
# Small delay to let permission attributes arrive
|
|
131
|
+
- delay:
|
|
132
|
+
milliseconds: 500
|
|
133
|
+
- service: !input notify_service
|
|
134
|
+
data:
|
|
135
|
+
title: "OpenCode: Permission Required"
|
|
136
|
+
message: "{{ project_name }}: {{ permission_title }}"
|
|
137
|
+
data:
|
|
138
|
+
tag: "opencode-permission-{{ device_id }}"
|
|
139
|
+
channel: !input notification_channel
|
|
140
|
+
importance: high
|
|
141
|
+
priority: high
|
|
142
|
+
ttl: 0
|
|
143
|
+
clickAction: !input dashboard_path
|
|
144
|
+
url: !input dashboard_path
|
|
145
|
+
actions:
|
|
146
|
+
- action: "OPENCODE_APPROVE_{{ device_id }}"
|
|
147
|
+
title: "Approve"
|
|
148
|
+
- action: "OPENCODE_REJECT_{{ device_id }}"
|
|
149
|
+
title: "Reject"
|
|
150
|
+
# Pass data needed for permission response
|
|
151
|
+
action_data:
|
|
152
|
+
device_id: "{{ device_id }}"
|
|
153
|
+
permission_id: "{{ permission_id }}"
|
|
154
|
+
command_topic: "{{ command_topic }}"
|
|
155
|
+
|
|
156
|
+
# Error Occurred (only after working)
|
|
157
|
+
- conditions:
|
|
158
|
+
- condition: template
|
|
159
|
+
value_template: "{{ new_state == 'error' and was_working }}"
|
|
160
|
+
- condition: template
|
|
161
|
+
value_template: !input notify_on_error
|
|
162
|
+
sequence:
|
|
163
|
+
- service: !input notify_service
|
|
164
|
+
data:
|
|
165
|
+
title: "OpenCode: Error"
|
|
166
|
+
message: "{{ project_name }}: {{ error_message }}"
|
|
167
|
+
data:
|
|
168
|
+
tag: "opencode-error-{{ device_id }}"
|
|
169
|
+
channel: !input notification_channel
|
|
170
|
+
importance: high
|
|
171
|
+
priority: high
|
|
172
|
+
ttl: 0
|
|
173
|
+
clickAction: !input dashboard_path
|
|
174
|
+
url: !input dashboard_path
|
|
175
|
+
|
|
176
|
+
# Work Complete (idle after working)
|
|
177
|
+
- conditions:
|
|
178
|
+
- condition: template
|
|
179
|
+
value_template: "{{ new_state == 'idle' and was_working }}"
|
|
180
|
+
- condition: template
|
|
181
|
+
value_template: !input notify_on_complete
|
|
182
|
+
sequence:
|
|
183
|
+
- service: !input notify_service
|
|
184
|
+
data:
|
|
185
|
+
title: "OpenCode: Task Complete"
|
|
186
|
+
message: "{{ project_name }}: {{ session_title }}"
|
|
187
|
+
data:
|
|
188
|
+
tag: "opencode-complete-{{ device_id }}"
|
|
189
|
+
channel: !input notification_channel
|
|
190
|
+
clickAction: !input dashboard_path
|
|
191
|
+
url: !input dashboard_path
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MqttWrapper } from "./mqtt.js";
|
|
2
|
+
import type { HaConfig } from "./config.js";
|
|
3
|
+
export interface CleanupConfig {
|
|
4
|
+
maxAgeDays: number;
|
|
5
|
+
haConfig: HaConfig;
|
|
6
|
+
}
|
|
7
|
+
export interface CleanupResult {
|
|
8
|
+
sessionsRemoved: number;
|
|
9
|
+
sessionIds: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Clean up stale OpenCode sessions from Home Assistant.
|
|
13
|
+
* Removes entities for sessions that haven't been active in `maxAgeDays` days.
|
|
14
|
+
*
|
|
15
|
+
* @param mqtt - MQTT client wrapper
|
|
16
|
+
* @param config - Cleanup configuration
|
|
17
|
+
* @returns Result with count and IDs of removed sessions
|
|
18
|
+
*/
|
|
19
|
+
export declare function cleanupStaleSessions(mqtt: MqttWrapper, config: CleanupConfig): Promise<CleanupResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Manually trigger cleanup and publish results to MQTT.
|
|
22
|
+
* Called via the cleanup_stale_sessions command.
|
|
23
|
+
*/
|
|
24
|
+
export declare function cleanupStaleSessionsManual(mqtt: MqttWrapper, config: CleanupConfig): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Run cleanup in the background (non-blocking).
|
|
27
|
+
* Logs results but doesn't throw on error.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runCleanupInBackground(mqtt: MqttWrapper, config: CleanupConfig): void;
|
|
30
|
+
//# sourceMappingURL=cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AA+ED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,aAAa,CAAC,CA8BxB;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,aAAa,GACpB,IAAI,CAYN"}
|