homebridge-adaptive-home 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2026-05-17
4
+
5
+ ### Added
6
+ - Initial release
7
+ - Automatic behavior pattern learning from all HomeKit accessories
8
+ - Statistical pattern analysis with configurable confidence threshold
9
+ - Suggestion dashboard with approve/reject workflow
10
+ - 5 virtual HomeKit accessories: Adaptive Learning switch, Morning/Evening/Sleep routine contact sensors, Away Mode occupancy sensor
11
+ - 7-day activity heatmap in the Homebridge UI dashboard
12
+ - Rolling 30-day event log with configurable retention
13
+ - Correlated device detection (finds devices that change together)
14
+ - Configurable polling interval, analysis schedule, and confidence threshold
15
+ - No extra hardware required — works with all existing HomeKit devices
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # homebridge-adaptive-home
2
+
3
+ > **Your home learns from you. Not the other way around.**
4
+
5
+ [![npm](https://img.shields.io/npm/v/homebridge-adaptive-home?color=5c6bc0)](https://www.npmjs.com/package/homebridge-adaptive-home)
6
+ [![npm](https://img.shields.io/npm/dt/homebridge-adaptive-home?color=26a69a)](https://www.npmjs.com/package/homebridge-adaptive-home)
7
+ [![Homebridge](https://img.shields.io/badge/homebridge-%3E%3D1.6.0-blueviolet)](https://homebridge.io)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+
10
+ ---
11
+
12
+ ## The Problem
13
+
14
+ Every smart home platform — HomeKit included — requires you to manually program every automation. You decide the rules. You set the schedules. You maintain everything when your life changes.
15
+
16
+ That's not smart. That's just remote control.
17
+
18
+ ## The Solution
19
+
20
+ **homebridge-adaptive-home** silently watches how you use your home. After a few days, it starts recognising your patterns — when you wake up, when you wind down, when you leave. Then it suggests automations for you to approve with a single click.
21
+
22
+ **No new hardware. No cloud. 100% local. 100% private.**
23
+
24
+ ---
25
+
26
+ ## How It Works
27
+
28
+ ```
29
+ Week 1: 🔍 Plugin observes all your HomeKit device activity
30
+ Week 2: 💡 "Every morning at 7:15, your kitchen lights turn on. Automate it?"
31
+ Week 3: ✅ Your home runs the routines you approved — and keeps learning
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Features
37
+
38
+ - **Zero hardware requirements** — works with every HomeKit device you already own
39
+ - **Fully local** — no cloud, no AI subscription, no data leaves your home
40
+ - **Statistical pattern detection** — finds recurring time-based routines with configurable confidence thresholds
41
+ - **Correlated device detection** — discovers which devices you use together (TV on + lights dim = movie routine)
42
+ - **5 virtual HomeKit accessories** — use detected routines directly in your HomeKit automations
43
+ - **Interactive dashboard** — approve or dismiss suggestions from the Homebridge UI
44
+ - **Activity heatmap** — visualise your weekly device usage patterns
45
+ - **Adaptive** — adjusts as your behaviour changes over time
46
+
47
+ ---
48
+
49
+ ## Virtual Accessories
50
+
51
+ The plugin creates 5 accessories in HomeKit that you can use in any automation:
52
+
53
+ | Accessory | Type | Active when |
54
+ |---|---|---|
55
+ | **Adaptive Learning** | Switch | ON = learning enabled |
56
+ | **Morning Routine** | Contact Sensor | Open during your detected morning window |
57
+ | **Evening Routine** | Contact Sensor | Open during your detected evening window |
58
+ | **Sleep Time** | Contact Sensor | Open during your detected sleep window |
59
+ | **Away Mode** | Occupancy Sensor | Occupied = away pattern detected |
60
+
61
+ ---
62
+
63
+ ## Installation
64
+
65
+ ### Via Homebridge UI (recommended)
66
+ 1. Open Homebridge → **Plugins** tab
67
+ 2. Search for `homebridge-adaptive-home`
68
+ 3. Click **Install**
69
+
70
+ ### Via CLI
71
+ ```bash
72
+ npm install -g homebridge-adaptive-home
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Configuration
78
+
79
+ Add to your `config.json` under `platforms`:
80
+
81
+ ```json
82
+ {
83
+ "platform": "AdaptiveHome",
84
+ "name": "Adaptive Home"
85
+ }
86
+ ```
87
+
88
+ ### Full options
89
+
90
+ ```json
91
+ {
92
+ "platform": "AdaptiveHome",
93
+ "name": "Adaptive Home",
94
+ "uiPort": 8581,
95
+ "uiToken": "",
96
+ "analysisIntervalHours": 24,
97
+ "minConfidence": 60,
98
+ "retentionDays": 30,
99
+ "pollIntervalSeconds": 60
100
+ }
101
+ ```
102
+
103
+ | Field | Default | Description |
104
+ |---|---|---|
105
+ | `uiPort` | `8581` | Homebridge Config UI X port |
106
+ | `uiToken` | auto | Auth token — leave blank to auto-discover |
107
+ | `analysisIntervalHours` | `24` | How often to run pattern analysis |
108
+ | `minConfidence` | `60` | Minimum confidence (%) to show a suggestion |
109
+ | `retentionDays` | `30` | Days of event history to keep |
110
+ | `pollIntervalSeconds` | `60` | How often to check for device changes |
111
+
112
+ ---
113
+
114
+ ## Dashboard
115
+
116
+ Open the plugin in Homebridge UI to access the dashboard:
117
+
118
+ - **Status bar** — learning on/off, total events, days observed, routines found
119
+ - **Suggestions** — approve or dismiss detected routine suggestions
120
+ - **Detected Routines** — all routines with confidence scores and peak times
121
+ - **Activity Heatmap** — 7-day × 48-bucket visualisation of your device usage
122
+
123
+ ---
124
+
125
+ ## Requirements
126
+
127
+ - Homebridge ≥ 1.6.0
128
+ - Node.js ≥ 18.0.0
129
+ - Homebridge Config UI X (for the dashboard and accessory polling)
130
+
131
+ ---
132
+
133
+ ## Privacy
134
+
135
+ All data is stored locally in your Homebridge storage directory (`~/.homebridge/adaptive-home/`). Nothing is sent to any server. The plugin only communicates with your own Homebridge instance on localhost.
136
+
137
+ ---
138
+
139
+ ## Support
140
+
141
+ - **Issues:** [github.com/azadaydinli/homebridge-adaptive-home/issues](https://github.com/azadaydinli/homebridge-adaptive-home/issues)
142
+ - **Buy me a coffee:** [ko-fi.com/azadaydinli](https://ko-fi.com/azadaydinli)
143
+
144
+ ---
145
+
146
+ ## License
147
+
148
+ MIT © [Azad Aydınlı](https://github.com/azadaydinli)
@@ -0,0 +1,62 @@
1
+ {
2
+ "pluginAlias": "AdaptiveHome",
3
+ "pluginType": "platform",
4
+ "customUi": true,
5
+ "headerDisplay": "Adaptive Home — Your home learns from you.",
6
+ "schema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "name": {
10
+ "title": "Platform Name",
11
+ "type": "string",
12
+ "default": "Adaptive Home"
13
+ },
14
+ "uiPort": {
15
+ "title": "Homebridge UI Port",
16
+ "description": "Port of the Homebridge Config UI X. Default is 8581.",
17
+ "type": "integer",
18
+ "default": 8581,
19
+ "minimum": 1,
20
+ "maximum": 65535
21
+ },
22
+ "uiToken": {
23
+ "title": "Homebridge UI Auth Token",
24
+ "description": "Optional. Leave blank to auto-discover from Homebridge storage.",
25
+ "type": "string"
26
+ },
27
+ "analysisIntervalHours": {
28
+ "title": "Analysis Interval (hours)",
29
+ "description": "How often to run pattern analysis. Default is every 24 hours.",
30
+ "type": "integer",
31
+ "default": 24,
32
+ "minimum": 1,
33
+ "maximum": 168
34
+ },
35
+ "minConfidence": {
36
+ "title": "Minimum Confidence (%)",
37
+ "description": "Only show suggestions above this confidence threshold. Default is 60%.",
38
+ "type": "integer",
39
+ "default": 60,
40
+ "minimum": 10,
41
+ "maximum": 100
42
+ },
43
+ "retentionDays": {
44
+ "title": "Event Retention (days)",
45
+ "description": "How many days of events to keep for analysis. Default is 30 days.",
46
+ "type": "integer",
47
+ "default": 30,
48
+ "minimum": 7,
49
+ "maximum": 365
50
+ },
51
+ "pollIntervalSeconds": {
52
+ "title": "Poll Interval (seconds)",
53
+ "description": "How often to poll Homebridge for accessory state changes. Default is 60 seconds.",
54
+ "type": "integer",
55
+ "default": 60,
56
+ "minimum": 10,
57
+ "maximum": 300
58
+ }
59
+ },
60
+ "required": ["name"]
61
+ }
62
+ }
@@ -0,0 +1,340 @@
1
+ <style>
2
+ :root {
3
+ --bg: #fff;
4
+ --text: #1a1a2e;
5
+ --muted: #6c757d;
6
+ --border: #e0e0e0;
7
+ --card: #f8f9fa;
8
+ --input-bg: #fff;
9
+ --accent: #5c6bc0;
10
+ --accent2: #26a69a;
11
+ --success: #43a047;
12
+ --danger: #e53935;
13
+ --warn: #fb8c00;
14
+ --morning: #ff9800;
15
+ --evening: #5c6bc0;
16
+ --sleep: #7c4dff;
17
+ --away: #26a69a;
18
+ --radius: 12px;
19
+ }
20
+ @media (prefers-color-scheme: dark) {
21
+ :root {
22
+ --bg: transparent;
23
+ --text: #e8eaf6;
24
+ --muted: #9e9e9e;
25
+ --border: #333;
26
+ --card: rgba(255,255,255,0.06);
27
+ --input-bg: rgba(255,255,255,0.08);
28
+ --accent: #7986cb;
29
+ }
30
+ }
31
+ * { box-sizing: border-box; margin: 0; padding: 0; }
32
+ body {
33
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
34
+ color: var(--text); background: var(--bg); font-size: 14px; line-height: 1.5;
35
+ }
36
+ .page { padding: 0 0 32px; }
37
+
38
+ /* Hero */
39
+ .hero {
40
+ background: linear-gradient(135deg, #3f51b5 0%, #7c4dff 100%);
41
+ color: #fff; padding: 24px 20px 20px; border-radius: 0 0 var(--radius) var(--radius);
42
+ margin-bottom: 20px;
43
+ }
44
+ .hero h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
45
+ .hero p { font-size: 13px; opacity: 0.85; }
46
+ .stats-row {
47
+ display: flex; gap: 12px; margin-top: 16px; flex-wrap: wrap;
48
+ }
49
+ .stat-chip {
50
+ background: rgba(255,255,255,0.18); border-radius: 20px;
51
+ padding: 4px 14px; font-size: 12px; font-weight: 600;
52
+ }
53
+
54
+ /* Sections */
55
+ .section { margin: 0 16px 20px; }
56
+ .section-title {
57
+ font-size: 13px; font-weight: 700; text-transform: uppercase;
58
+ letter-spacing: .5px; color: var(--muted); margin-bottom: 10px;
59
+ display: flex; align-items: center; gap: 6px;
60
+ }
61
+ .badge {
62
+ background: var(--accent); color: #fff;
63
+ border-radius: 10px; padding: 1px 8px; font-size: 11px;
64
+ }
65
+
66
+ /* Cards */
67
+ .card {
68
+ background: var(--card); border: 1px solid var(--border);
69
+ border-radius: var(--radius); padding: 14px 16px; margin-bottom: 10px;
70
+ }
71
+ .card-header {
72
+ display: flex; justify-content: space-between; align-items: flex-start;
73
+ margin-bottom: 6px;
74
+ }
75
+ .card-title { font-weight: 600; font-size: 14px; }
76
+ .card-body { font-size: 13px; color: var(--muted); line-height: 1.6; }
77
+
78
+ /* Confidence bar */
79
+ .conf-wrap { margin-top: 10px; }
80
+ .conf-label { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
81
+ .conf-bar { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
82
+ .conf-fill { height: 100%; border-radius: 3px; transition: width .4s; }
83
+
84
+ /* Type badges */
85
+ .type-pill {
86
+ font-size: 11px; font-weight: 700; padding: 2px 10px;
87
+ border-radius: 12px; text-transform: uppercase; letter-spacing: .3px;
88
+ }
89
+ .type-morning { background: rgba(255,152,0,.15); color: var(--morning); }
90
+ .type-evening { background: rgba(92,107,192,.15); color: var(--evening); }
91
+ .type-sleep { background: rgba(124,77,255,.15); color: var(--sleep); }
92
+ .type-away { background: rgba(38,166,154,.15); color: var(--away); }
93
+
94
+ /* Suggestion actions */
95
+ .actions { display: flex; gap: 8px; margin-top: 12px; }
96
+ .btn {
97
+ padding: 7px 16px; border: none; border-radius: 8px;
98
+ font-size: 13px; font-weight: 600; cursor: pointer; transition: opacity .15s;
99
+ }
100
+ .btn:hover { opacity: .82; }
101
+ .btn-approve { background: var(--success); color: #fff; }
102
+ .btn-reject { background: transparent; color: var(--danger); border: 1px solid var(--danger); }
103
+
104
+ /* Devices row */
105
+ .devices-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
106
+ .device-chip {
107
+ background: var(--border); border-radius: 10px;
108
+ padding: 2px 10px; font-size: 11px; color: var(--muted);
109
+ }
110
+
111
+ /* Heatmap */
112
+ .heatmap { display: grid; grid-template-columns: 36px repeat(48, 1fr); gap: 2px; margin-top: 8px; }
113
+ .heatmap-label { font-size: 10px; color: var(--muted); display: flex; align-items: center; justify-content: flex-end; padding-right: 4px; }
114
+ .heatmap-cell { height: 14px; border-radius: 2px; background: var(--border); }
115
+ .heatmap-hours { display: grid; grid-template-columns: 36px repeat(12, 4fr); gap: 2px; margin-top: 2px; }
116
+ .heatmap-hour-label { font-size: 10px; color: var(--muted); text-align: center; }
117
+
118
+ /* Empty state */
119
+ .empty {
120
+ text-align: center; padding: 32px 20px; color: var(--muted);
121
+ }
122
+ .empty .icon { font-size: 40px; margin-bottom: 12px; }
123
+ .empty p { font-size: 13px; line-height: 1.7; }
124
+
125
+ /* Status pill */
126
+ .status-dot {
127
+ display: inline-block; width: 8px; height: 8px;
128
+ border-radius: 50%; margin-right: 6px;
129
+ }
130
+ .dot-green { background: var(--success); }
131
+ .dot-grey { background: var(--muted); }
132
+
133
+ /* Loading */
134
+ .loading { text-align: center; padding: 40px; color: var(--muted); }
135
+
136
+ /* Meta row */
137
+ .meta-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
138
+ .meta-item { font-size: 12px; color: var(--muted); }
139
+ .meta-item strong { color: var(--text); }
140
+ </style>
141
+
142
+ <div class="page" id="app">
143
+ <div class="loading">Loading Adaptive Home...</div>
144
+ </div>
145
+
146
+ <script>
147
+ (async () => {
148
+ const app = document.getElementById('app');
149
+ let status = null;
150
+
151
+ function confColor(c) {
152
+ if (c >= 0.8) return '#43a047';
153
+ if (c >= 0.6) return '#fb8c00';
154
+ return '#e53935';
155
+ }
156
+
157
+ function fmtTime(h, m) {
158
+ return `${String(h).padStart(2,'0')}:${String(m || 0).padStart(2,'0')}`;
159
+ }
160
+
161
+ function typePill(type) {
162
+ return `<span class="type-pill type-${type}">${type}</span>`;
163
+ }
164
+
165
+ function renderHero(s) {
166
+ const dot = s.learningEnabled
167
+ ? `<span class="status-dot dot-green"></span>Learning Active`
168
+ : `<span class="status-dot dot-grey"></span>Learning Paused`;
169
+ return `
170
+ <div class="hero">
171
+ <h1>Adaptive Home</h1>
172
+ <p>Your home learns your routines and suggests automations.</p>
173
+ <div class="stats-row">
174
+ <span class="stat-chip">${dot}</span>
175
+ <span class="stat-chip">${s.eventsLogged} events logged</span>
176
+ <span class="stat-chip">${s.daysObserved} days observed</span>
177
+ <span class="stat-chip">${(s.routines || []).length} routines detected</span>
178
+ </div>
179
+ </div>`;
180
+ }
181
+
182
+ function renderRoutines(routines) {
183
+ if (!routines || routines.length === 0) {
184
+ return `
185
+ <div class="section">
186
+ <div class="section-title">Detected Routines</div>
187
+ <div class="empty">
188
+ <div class="icon">🔍</div>
189
+ <p>No routines detected yet.<br>Keep Homebridge running for a few days<br>and check back here.</p>
190
+ </div>
191
+ </div>`;
192
+ }
193
+ const cards = routines.map(r => {
194
+ const conf = Math.round(r.confidence * 100);
195
+ const corrHtml = r.correlatedDevices && r.correlatedDevices.length
196
+ ? `<div class="devices-row">${r.correlatedDevices.map(d => `<span class="device-chip">${d}</span>`).join('')}</div>`
197
+ : '';
198
+ return `
199
+ <div class="card">
200
+ <div class="card-header">
201
+ <span class="card-title">${r.accessory}</span>
202
+ ${typePill(r.type)}
203
+ </div>
204
+ <div class="card-body">${r.label}</div>
205
+ <div class="meta-row">
206
+ <span class="meta-item">Peak: <strong>${fmtTime(r.peakHour, r.peakMinute)}</strong></span>
207
+ <span class="meta-item">Seen: <strong>${r.daysDetected} days</strong></span>
208
+ <span class="meta-item">Events: <strong>${r.occurrences}</strong></span>
209
+ </div>
210
+ ${corrHtml}
211
+ <div class="conf-wrap">
212
+ <div class="conf-label"><span>Confidence</span><span>${conf}%</span></div>
213
+ <div class="conf-bar"><div class="conf-fill" style="width:${conf}%;background:${confColor(r.confidence)}"></div></div>
214
+ </div>
215
+ </div>`;
216
+ }).join('');
217
+ return `
218
+ <div class="section">
219
+ <div class="section-title">Detected Routines <span class="badge">${routines.length}</span></div>
220
+ ${cards}
221
+ </div>`;
222
+ }
223
+
224
+ function renderSuggestions(suggestions) {
225
+ if (!suggestions || suggestions.length === 0) {
226
+ return `
227
+ <div class="section">
228
+ <div class="section-title">Suggestions</div>
229
+ <div class="empty">
230
+ <div class="icon">💡</div>
231
+ <p>No suggestions yet.<br>When a routine reaches the confidence<br>threshold, it will appear here.</p>
232
+ </div>
233
+ </div>`;
234
+ }
235
+ const cards = suggestions.map(s => {
236
+ const conf = Math.round(s.confidence * 100);
237
+ return `
238
+ <div class="card" id="sug-${s.id}">
239
+ <div class="card-header">
240
+ <span class="card-title">${s.accessory}</span>
241
+ ${typePill(s.type)}
242
+ </div>
243
+ <div class="card-body">${s.suggestion}</div>
244
+ <div class="conf-wrap">
245
+ <div class="conf-label"><span>Confidence</span><span>${conf}%</span></div>
246
+ <div class="conf-bar"><div class="conf-fill" style="width:${conf}%;background:${confColor(s.confidence)}"></div></div>
247
+ </div>
248
+ <div class="actions">
249
+ <button class="btn btn-approve" data-id="${s.id}" onclick="handleApprove('${s.id}')">Approve</button>
250
+ <button class="btn btn-reject" data-id="${s.id}" onclick="handleReject('${s.id}')">Dismiss</button>
251
+ </div>
252
+ </div>`;
253
+ }).join('');
254
+ return `
255
+ <div class="section">
256
+ <div class="section-title">Suggestions <span class="badge">${suggestions.length}</span></div>
257
+ ${cards}
258
+ </div>`;
259
+ }
260
+
261
+ function renderHeatmap(events) {
262
+ if (!events || events.length === 0) return '';
263
+ const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
264
+ const BUCKETS = 48;
265
+ const grid = Array.from({ length: 7 }, () => new Array(BUCKETS).fill(0));
266
+ for (const e of events) {
267
+ const b = Math.floor((e.hour * 60 + e.min) / 30);
268
+ if (e.dow >= 0 && e.dow < 7 && b >= 0 && b < BUCKETS) grid[e.dow][b]++;
269
+ }
270
+ const max = Math.max(1, ...grid.flat());
271
+
272
+ const rows = grid.map((row, d) => {
273
+ const cells = row.map(v => {
274
+ const opacity = v > 0 ? 0.15 + (v / max) * 0.85 : 0;
275
+ const bg = v > 0 ? `rgba(92,107,192,${opacity.toFixed(2)})` : 'var(--border)';
276
+ return `<div class="heatmap-cell" style="background:${bg}" title="${v} events"></div>`;
277
+ }).join('');
278
+ return `<div class="heatmap-label">${days[d]}</div>${cells}`;
279
+ }).join('');
280
+
281
+ const hourLabels = ['','3','','6','','9','','12','','15','','18','','21',''].join('</div><div class="heatmap-hour-label">');
282
+
283
+ return `
284
+ <div class="section">
285
+ <div class="section-title">Activity Heatmap (30-min buckets)</div>
286
+ <div class="card">
287
+ <div class="heatmap">${rows}</div>
288
+ <div class="heatmap-hours">
289
+ <div></div>
290
+ <div class="heatmap-hour-label">0</div>
291
+ <div class="heatmap-hour-label">3</div>
292
+ <div class="heatmap-hour-label">6</div>
293
+ <div class="heatmap-hour-label">9</div>
294
+ <div class="heatmap-hour-label">12</div>
295
+ <div class="heatmap-hour-label">15</div>
296
+ <div class="heatmap-hour-label">18</div>
297
+ <div class="heatmap-hour-label">21</div>
298
+ <div class="heatmap-hour-label">24</div>
299
+ <div class="heatmap-hour-label"></div>
300
+ <div class="heatmap-hour-label"></div>
301
+ <div class="heatmap-hour-label"></div>
302
+ </div>
303
+ </div>
304
+ </div>`;
305
+ }
306
+
307
+ window.handleApprove = async (id) => {
308
+ try {
309
+ await homebridge.request('/suggestion/approve', { id });
310
+ await refresh();
311
+ } catch(e) { console.error(e); }
312
+ };
313
+
314
+ window.handleReject = async (id) => {
315
+ try {
316
+ await homebridge.request('/suggestion/reject', { id });
317
+ await refresh();
318
+ } catch(e) { console.error(e); }
319
+ };
320
+
321
+ async function refresh() {
322
+ try {
323
+ status = await homebridge.request('/status', {});
324
+ } catch(e) {
325
+ status = { learningEnabled: false, eventsLogged: 0, daysObserved: 0, routines: [], suggestions: [] };
326
+ }
327
+ const eventsForHeatmap = status.recentEvents || [];
328
+ app.innerHTML =
329
+ renderHero(status) +
330
+ renderSuggestions(status.suggestions) +
331
+ renderRoutines(status.routines) +
332
+ renderHeatmap(eventsForHeatmap);
333
+ }
334
+
335
+ await refresh();
336
+
337
+ // Auto-refresh every 60 seconds
338
+ setInterval(refresh, 60000);
339
+ })();
340
+ </script>
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ // Custom UI server — handles homebridge.request() calls from the dashboard
4
+ module.exports = (api) => {
5
+ api.onRequest('/status', async (body) => {
6
+ const platform = getPlatform(api);
7
+ if (!platform) return emptyStatus();
8
+ const s = platform.getStatus();
9
+ s.recentEvents = platform.eventLogger.getEvents(7);
10
+ return s;
11
+ });
12
+
13
+ api.onRequest('/suggestion/approve', async (body) => {
14
+ const platform = getPlatform(api);
15
+ if (!platform) return { ok: false };
16
+ return { ok: platform.approveSuggestion(body.id) };
17
+ });
18
+
19
+ api.onRequest('/suggestion/reject', async (body) => {
20
+ const platform = getPlatform(api);
21
+ if (!platform) return { ok: false };
22
+ return { ok: platform.rejectSuggestion(body.id) };
23
+ });
24
+ };
25
+
26
+ function getPlatform(api) {
27
+ try {
28
+ const platforms = api.homebridgePluginManager
29
+ && api.homebridgePluginManager.getInstalledPlugins
30
+ && api.homebridgePluginManager.getInstalledPlugins();
31
+ // Access via global homebridge instance stored by the platform on init
32
+ if (global._adaptiveHomePlatform) return global._adaptiveHomePlatform;
33
+ } catch {}
34
+ return null;
35
+ }
36
+
37
+ function emptyStatus() {
38
+ return {
39
+ learningEnabled: false,
40
+ eventsLogged: 0,
41
+ daysObserved: 0,
42
+ lastAnalysis: null,
43
+ suggestions: [],
44
+ routines: [],
45
+ recentEvents: [],
46
+ };
47
+ }
package/index.js ADDED
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ const { AdaptiveHomePlatform } = require('./src/platform');
4
+
5
+ const PLUGIN_NAME = 'homebridge-adaptive-home';
6
+ const PLATFORM_NAME = 'AdaptiveHome';
7
+
8
+ module.exports = (api) => {
9
+ api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, AdaptiveHomePlatform);
10
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "homebridge-adaptive-home",
3
+ "version": "1.0.0",
4
+ "description": "The first Homebridge plugin that learns your behavior patterns and automatically suggests HomeKit automations. No new hardware required — works with all your existing HomeKit devices.",
5
+ "homepage": "https://github.com/azadaydinli/homebridge-adaptive-home",
6
+ "main": "index.js",
7
+ "author": "Azad Aydınlı",
8
+ "license": "MIT",
9
+ "scripts": {
10
+ "lint": "echo \"No linter configured\"",
11
+ "test": "echo \"No tests configured\""
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/azadaydinli/homebridge-adaptive-home/issues"
15
+ },
16
+ "keywords": [
17
+ "homebridge-plugin",
18
+ "homebridge",
19
+ "homekit",
20
+ "adaptive",
21
+ "learning",
22
+ "automation",
23
+ "smart-home",
24
+ "ai",
25
+ "pattern",
26
+ "routine",
27
+ "behavior",
28
+ "intelligent",
29
+ "machine-learning",
30
+ "home-automation",
31
+ "virtual",
32
+ "sensor",
33
+ "schedule"
34
+ ],
35
+ "funding": [
36
+ {
37
+ "type": "ko_fi",
38
+ "url": "https://ko-fi.com/azadaydinli"
39
+ },
40
+ {
41
+ "type": "github",
42
+ "url": "https://github.com/sponsors/azadaydinli"
43
+ }
44
+ ],
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/azadaydinli/homebridge-adaptive-home.git"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.0.0",
51
+ "homebridge": ">=1.6.0"
52
+ }
53
+ }