homebridge-boiler-ai 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/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # Homebridge Boiler AI
2
+
3
+ AI-powered hot water controller for Homebridge. Once configured, it runs fully autonomously — checking weather, estimating your tank temperature, and turning the boiler on/off as needed. No daily interaction required.
4
+
5
+ The AI makes sure you have hot water when you need it — using solar heating when possible and only running the electric heater when necessary.
6
+
7
+ Works with [Switcher](#switcher), [Shelly](#shelly), [Tasmota](#tasmota), and [any HTTP-controllable plug](#other-smart-plugs).
8
+
9
+ ## Setup (5 minutes)
10
+
11
+ Install the plugin from the Homebridge UI: search for **homebridge-boiler-ai** and click install.
12
+
13
+ Then configure in the plugin settings (or paste into `config.json`):
14
+
15
+ **Minimal config — Switcher:**
16
+
17
+ ```jsonc
18
+ {
19
+ "platform": "BoilerAI",
20
+ "name": "Boiler AI",
21
+
22
+ // ── Required ──────────────────────────────────
23
+ "geminiApiKey": "YOUR_GEMINI_API_KEY", // get from https://aistudio.google.com/apikey
24
+ "location": "Tel Aviv", // your city (verify: wttr.in/YourCity)
25
+ "timezone": "Asia/Jerusalem", // your timezone
26
+ "switcher": {
27
+ "deviceId": "Switcher_Touch_386C" // name from Switcher app, IP, or hex ID
28
+ },
29
+ "usage": [
30
+ { "time": "07:00", "label": "Morning shower", "liters": 60, "temp": 45 },
31
+ { "time": "20:00", "label": "Evening shower", "liters": 100, "temp": 50 }
32
+ ],
33
+
34
+ // ── Optional ──────────────────────────────────
35
+ // "tank": { "liters": 120, "heaterKw": 2.5 }, // auto-detected from location
36
+ // "xaiApiKey": "", // alternative to Gemini
37
+ // "switcher.token": "", // only if you get auth errors
38
+ // "maxDurationMinutes": 90 // safety cap per cycle
39
+ }
40
+ ```
41
+
42
+ **Minimal config — Shelly / HTTP plug:**
43
+
44
+ ```jsonc
45
+ {
46
+ "platform": "BoilerAI",
47
+ "name": "Boiler AI",
48
+
49
+ // ── Required ──────────────────────────────────
50
+ "geminiApiKey": "YOUR_GEMINI_API_KEY",
51
+ "location": "Tel Aviv",
52
+ "timezone": "Asia/Jerusalem",
53
+ "boilerPlug": {
54
+ "onUrl": "http://192.168.1.50/relay/0?turn=on",
55
+ "offUrl": "http://192.168.1.50/relay/0?turn=off"
56
+ },
57
+ "usage": [
58
+ { "time": "07:00", "label": "Morning shower", "liters": 60, "temp": 45 },
59
+ { "time": "20:00", "label": "Evening shower", "liters": 100, "temp": 50 }
60
+ ]
61
+ }
62
+ ```
63
+
64
+ ---
65
+
66
+ ### 1. AI API Key
67
+
68
+ Get an API key from [Google AI Studio](https://aistudio.google.com/apikey) — sign in, click "Create API Key", and paste it into the **Gemini API Key** field.
69
+
70
+ The plugin uses Gemini Flash-Lite which has a free tier (1,000 requests/day — the plugin uses ~5-10/day). New Google accounts may need to set up billing first, but usage within the free limits is not charged. See [Gemini API pricing](https://ai.google.dev/gemini-api/docs/pricing) for current details.
71
+
72
+ Alternatively, you can use [xAI Grok](https://console.x.ai/) (paid, but faster responses).
73
+
74
+ ### 2. Location
75
+
76
+ Enter your city name in the **Location** field. To verify it works, open `wttr.in/YourCity` in your browser (e.g. [wttr.in/Tel+Aviv](https://wttr.in/Tel+Aviv)) — if it shows the right weather, use that city name.
77
+
78
+ Enter your timezone in the **Timezone** field. To find it, run `timedatectl | grep "Time zone"`.
79
+
80
+ ### 3. Smart Plug
81
+
82
+ Tell the plugin how to turn your boiler on and off. Pick your plug type:
83
+
84
+ #### Switcher
85
+
86
+ Native support — the plugin finds and controls the Switcher directly on your local network. No URLs needed, no extra plugins.
87
+
88
+ Enter your device name (as it appears in the Switcher app), IP address, or device ID — any of these work:
89
+
90
+ ```json
91
+ "switcher": {
92
+ "deviceId": "Switcher_Touch_386C"
93
+ }
94
+ ```
95
+
96
+ If you get auth errors in the logs, your model may need a token — get it from https://switcher.co.il/GetKey/ and add `"token": "your-token"`.
97
+
98
+ > **Note:** If you have `homebridge-switcher-platform` installed, disable or remove it first — two plugins can't control the same Switcher device simultaneously.
99
+
100
+ #### Shelly
101
+
102
+ ```json
103
+ "boilerPlug": {
104
+ "onUrl": "http://192.168.1.50/relay/0?turn=on",
105
+ "offUrl": "http://192.168.1.50/relay/0?turn=off"
106
+ }
107
+ ```
108
+
109
+ For Gen2+ (Plus/Pro series):
110
+ ```json
111
+ "boilerPlug": {
112
+ "onUrl": "http://192.168.1.50/rpc/Switch.Set?id=0&on=true",
113
+ "offUrl": "http://192.168.1.50/rpc/Switch.Set?id=0&on=false"
114
+ }
115
+ ```
116
+
117
+ #### Tasmota
118
+
119
+ ```json
120
+ "boilerPlug": {
121
+ "onUrl": "http://192.168.1.51/cm?cmnd=Power%20On",
122
+ "offUrl": "http://192.168.1.51/cm?cmnd=Power%20Off"
123
+ }
124
+ ```
125
+
126
+ #### Other smart plugs
127
+
128
+ Any plug with an HTTP on/off URL works. For plugs that need POST requests or auth headers:
129
+
130
+ ```json
131
+ "boilerPlug": {
132
+ "onUrl": "http://192.168.1.53/api/switch/on",
133
+ "offUrl": "http://192.168.1.53/api/switch/off",
134
+ "method": "POST",
135
+ "headers": "{\"Authorization\": \"Bearer TOKEN\", \"Content-Type\": \"application/json\"}",
136
+ "body": "{\"device\": \"boiler\"}"
137
+ }
138
+ ```
139
+
140
+ > **Note:** Use either `switcher` or `boilerPlug` — not both.
141
+
142
+ ### 4. Hot Water Schedule
143
+
144
+ Add the times your household needs hot water:
145
+
146
+ ```json
147
+ "usage": [
148
+ { "time": "07:00", "label": "Morning shower", "liters": 60, "temp": 45 },
149
+ { "time": "18:30", "label": "Kid bath", "liters": 50, "temp": 45 },
150
+ { "time": "22:00", "label": "Evening shower", "liters": 100, "temp": 50 }
151
+ ]
152
+ ```
153
+
154
+ The plugin checks automatically ~1 hour before each event and only heats if needed. On sunny days, the sun does the work and the electric heater stays off.
155
+
156
+ ### Tank (auto-detected)
157
+
158
+ On first startup, the plugin detects the standard tank specs for your location automatically. If your tank is different, override by adding the `tank` section to your config:
159
+
160
+ ```json
161
+ "tank": {
162
+ "liters": 120,
163
+ "heaterKw": 2.5,
164
+ "solar": true
165
+ }
166
+ ```
167
+
168
+ Set `"solar": false` if your tank is electric-only (no rooftop solar panel).
169
+
170
+ ## How it works
171
+
172
+ **The plugin is fully autonomous.** Once configured, it runs on its own. Before each time you need hot water, it:
173
+
174
+ 1. Fetches weather and sunrise/sunset for your location
175
+ 2. Estimates the tank temperature from heating history and solar gain
176
+ 3. Asks the AI whether heating is needed and for how long
177
+ 4. Turns the boiler on/off via your smart plug
178
+
179
+ This runs in the background as long as Homebridge is running — no interaction needed.
180
+
181
+ **Important:** There is no physical temperature sensor. The tank temperature is estimated based on weather conditions, solar gain, heating history, and standby heat loss. The AI uses this estimate to make decisions. It works well in practice, but it's a model — not a measurement.
182
+
183
+ On the first day after installation, the system has no heating history, so the initial temperature estimate may be off. After the first heating cycle it calibrates itself and becomes more accurate over time.
184
+
185
+ ### The HomeKit switch
186
+
187
+ The boiler appears as a switch in the Home app, but it does **not** enable/disable the system. Think of it as a button:
188
+
189
+ - **Tap ON** = "check now" — manually triggers one AI decision. The switch turns itself back off afterward.
190
+ - **Tap OFF** = emergency stop — immediately turns off the boiler if it's heating.
191
+
192
+ The automatic schedule runs regardless. You never need to touch the switch — it's there for manual overrides only.
193
+
194
+ ## Safety
195
+
196
+ - **Max duration cap** — no single cycle exceeds 90 minutes (configurable)
197
+ - **Watchdog timer** — force-stops 5 minutes after max, no matter what
198
+ - **Crash recovery** — sends OFF on Homebridge restart if boiler was left on
199
+ - **Retry logic** — 3 attempts for every on/off command, with emergency double-off on failure
@@ -0,0 +1,184 @@
1
+ {
2
+ "pluginAlias": "BoilerAI",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "AI-powered solar hot water tank controller. Uses Gemini or Grok to decide when to run the electric heater, optimizing for minimal electricity by leveraging solar heating.",
6
+ "schema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "name": {
10
+ "title": "Plugin Name",
11
+ "type": "string",
12
+ "default": "Boiler AI"
13
+ },
14
+ "location": {
15
+ "title": "Location",
16
+ "type": "string",
17
+ "description": "City name for weather (test with: curl wttr.in/YourCity)",
18
+ "placeholder": "Tel Aviv",
19
+ "required": true
20
+ },
21
+ "timezone": {
22
+ "title": "Timezone",
23
+ "type": "string",
24
+ "description": "IANA timezone (find yours: timedatectl | grep 'Time zone')",
25
+ "placeholder": "Asia/Jerusalem",
26
+ "required": true
27
+ },
28
+ "geminiApiKey": {
29
+ "title": "Gemini API Key",
30
+ "type": "string",
31
+ "description": "Google Gemini API key (free tier available at aistudio.google.com)",
32
+ "x-schema-form": { "type": "password" }
33
+ },
34
+ "xaiApiKey": {
35
+ "title": "xAI (Grok) API Key",
36
+ "type": "string",
37
+ "description": "Optional. If set, Grok is preferred over Gemini",
38
+ "x-schema-form": { "type": "password" }
39
+ },
40
+ "tank": {
41
+ "title": "Tank",
42
+ "type": "object",
43
+ "properties": {
44
+ "liters": {
45
+ "title": "Capacity (liters)",
46
+ "type": "integer",
47
+ "default": 120,
48
+ "minimum": 20,
49
+ "maximum": 500,
50
+ "required": true
51
+ },
52
+ "heaterKw": {
53
+ "title": "Heater Power (kW)",
54
+ "type": "number",
55
+ "default": 2.5,
56
+ "minimum": 0.5,
57
+ "maximum": 10,
58
+ "required": true
59
+ },
60
+ "solar": {
61
+ "title": "Has Solar Collector",
62
+ "type": "boolean",
63
+ "default": true,
64
+ "description": "Disable for electric-only tanks"
65
+ }
66
+ }
67
+ },
68
+ "boilerPlug": {
69
+ "title": "Boiler Smart Plug",
70
+ "type": "object",
71
+ "properties": {
72
+ "onUrl": {
73
+ "title": "ON URL",
74
+ "type": "string",
75
+ "description": "HTTP URL to turn boiler on",
76
+ "required": true
77
+ },
78
+ "offUrl": {
79
+ "title": "OFF URL",
80
+ "type": "string",
81
+ "description": "HTTP URL to turn boiler off",
82
+ "required": true
83
+ },
84
+ "method": {
85
+ "title": "HTTP Method",
86
+ "type": "string",
87
+ "default": "GET",
88
+ "oneOf": [
89
+ { "title": "GET", "enum": ["GET"] },
90
+ { "title": "POST", "enum": ["POST"] }
91
+ ]
92
+ },
93
+ "headers": {
94
+ "title": "HTTP Headers (JSON)",
95
+ "type": "string",
96
+ "description": "Optional JSON object, e.g. {\"Authorization\": \"Bearer token\"}"
97
+ },
98
+ "body": {
99
+ "title": "Request Body",
100
+ "type": "string",
101
+ "description": "Optional body for POST requests"
102
+ }
103
+ }
104
+ },
105
+ "switcher": {
106
+ "title": "Switcher (Israeli boiler plug)",
107
+ "type": "object",
108
+ "description": "If configured, the plugin controls the boiler via Switcher directly (no HTTP URLs needed). Leave empty to use HTTP smart plug instead.",
109
+ "properties": {
110
+ "deviceId": {
111
+ "title": "Device ID",
112
+ "type": "string",
113
+ "description": "Device name (as shown in Switcher app), IP address, or hex device ID"
114
+ },
115
+ "deviceIp": {
116
+ "title": "Device IP (optional)",
117
+ "type": "string",
118
+ "description": "Optional — device is auto-discovered on your network. Set only if discovery fails."
119
+ },
120
+ "token": {
121
+ "title": "Token (optional)",
122
+ "type": "string",
123
+ "description": "Only if needed — get it from https://switcher.co.il/GetKey/",
124
+ "x-schema-form": { "type": "password" }
125
+ }
126
+ }
127
+ },
128
+ "usage": {
129
+ "title": "Hot Water Usage",
130
+ "type": "array",
131
+ "items": {
132
+ "type": "object",
133
+ "properties": {
134
+ "time": {
135
+ "title": "Time (HH:MM)",
136
+ "type": "string",
137
+ "pattern": "^\\d{2}:\\d{2}$",
138
+ "required": true
139
+ },
140
+ "label": {
141
+ "title": "Label",
142
+ "type": "string",
143
+ "required": true
144
+ },
145
+ "liters": {
146
+ "title": "Liters",
147
+ "type": "integer",
148
+ "minimum": 5,
149
+ "required": true
150
+ },
151
+ "temp": {
152
+ "title": "Temperature (°C)",
153
+ "type": "number",
154
+ "minimum": 30,
155
+ "maximum": 65,
156
+ "required": true
157
+ }
158
+ }
159
+ },
160
+ "default": [
161
+ { "time": "06:00", "label": "Morning wash", "liters": 30, "temp": 38 },
162
+ { "time": "18:30", "label": "Kid bath", "liters": 50, "temp": 45 },
163
+ { "time": "22:00", "label": "Showers", "liters": 120, "temp": 50 }
164
+ ]
165
+ },
166
+ "maxDurationMinutes": {
167
+ "title": "Max Heating Duration (minutes)",
168
+ "type": "integer",
169
+ "default": 90,
170
+ "minimum": 10,
171
+ "maximum": 120,
172
+ "description": "Safety cap per heating cycle"
173
+ },
174
+ "aiTemperature": {
175
+ "title": "AI Temperature",
176
+ "type": "number",
177
+ "default": 0.3,
178
+ "minimum": 0,
179
+ "maximum": 1,
180
+ "description": "Lower = more conservative decisions"
181
+ }
182
+ }
183
+ }
184
+ }
package/dist/ai.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { Logger } from 'homebridge';
2
+ import * as http from 'http';
3
+ export declare function callAI(prompt: string, timeoutSecs: number, xaiApiKey?: string, geminiApiKey?: string, temperature?: number, log?: Logger): Promise<string>;
4
+ declare function httpRequest(url: string, options: http.RequestOptions & {
5
+ timeout?: number;
6
+ }, body?: string): Promise<string>;
7
+ export { httpRequest };
package/dist/ai.js ADDED
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.callAI = callAI;
37
+ exports.httpRequest = httpRequest;
38
+ const https = __importStar(require("https"));
39
+ const http = __importStar(require("http"));
40
+ async function callAI(prompt, timeoutSecs, xaiApiKey, geminiApiKey, temperature = 0.3, log) {
41
+ if (xaiApiKey) {
42
+ return callOpenAICompatible('https://api.x.ai/v1/chat/completions', xaiApiKey, 'grok-3-mini-fast', prompt, timeoutSecs, temperature);
43
+ }
44
+ if (geminiApiKey) {
45
+ return callGeminiREST(geminiApiKey, 'gemini-2.5-flash-lite', prompt, timeoutSecs);
46
+ }
47
+ throw new Error('No AI API key set (need geminiApiKey or xaiApiKey)');
48
+ }
49
+ function callOpenAICompatible(endpoint, apiKey, model, prompt, timeoutSecs, temperature) {
50
+ const body = JSON.stringify({
51
+ model,
52
+ messages: [{ role: 'user', content: prompt }],
53
+ temperature,
54
+ });
55
+ return httpRequest(endpoint, {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ 'Authorization': `Bearer ${apiKey}`,
60
+ },
61
+ timeout: timeoutSecs * 1000,
62
+ }, body).then(data => {
63
+ const result = JSON.parse(data);
64
+ if (!result.choices?.length)
65
+ throw new Error('Empty AI response');
66
+ return result.choices[0].message.content;
67
+ });
68
+ }
69
+ function callGeminiREST(apiKey, model, prompt, timeoutSecs) {
70
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
71
+ const body = JSON.stringify({
72
+ contents: [{ parts: [{ text: prompt }] }],
73
+ });
74
+ return httpRequest(url, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ 'x-goog-api-key': apiKey,
79
+ },
80
+ timeout: timeoutSecs * 1000,
81
+ }, body).then(data => {
82
+ const result = JSON.parse(data);
83
+ if (!result.candidates?.[0]?.content?.parts?.[0]?.text) {
84
+ throw new Error('Empty AI response');
85
+ }
86
+ return result.candidates[0].content.parts[0].text;
87
+ });
88
+ }
89
+ function httpRequest(url, options, body) {
90
+ return new Promise((resolve, reject) => {
91
+ const lib = url.startsWith('https') ? https : http;
92
+ const req = lib.request(url, options, (res) => {
93
+ let data = '';
94
+ res.on('data', chunk => data += chunk);
95
+ res.on('end', () => {
96
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
97
+ resolve(data);
98
+ }
99
+ else {
100
+ reject(new Error(`API error ${res.statusCode}: ${data.slice(0, 200)}`));
101
+ }
102
+ });
103
+ });
104
+ req.on('error', reject);
105
+ if (options.timeout) {
106
+ req.setTimeout(options.timeout, () => {
107
+ req.destroy();
108
+ reject(new Error('Request timeout'));
109
+ });
110
+ }
111
+ if (body)
112
+ req.write(body);
113
+ req.end();
114
+ });
115
+ }
116
+ //# sourceMappingURL=ai.js.map
package/dist/ai.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai.js","sourceRoot":"","sources":["../src/ai.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { Logger } from 'homebridge';
2
+ import { BoilerPlugConfig } from './settings';
3
+ export declare function sendWebhook(on: boolean, plug: BoilerPlugConfig, log: Logger): Promise<void>;
package/dist/boiler.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendWebhook = sendWebhook;
4
+ const ai_1 = require("./ai");
5
+ const WEBHOOK_RETRIES = 3;
6
+ const WEBHOOK_TIMEOUT = 5000;
7
+ const WEBHOOK_RETRY_DELAY = 2000;
8
+ async function sendWebhook(on, plug, log) {
9
+ const url = on ? plug.onUrl : plug.offUrl;
10
+ const method = (plug.method || 'GET').toUpperCase();
11
+ let headers = {};
12
+ if (plug.headers) {
13
+ try {
14
+ headers = JSON.parse(plug.headers);
15
+ }
16
+ catch {
17
+ log.warn('WEBHOOK: failed to parse headers JSON');
18
+ }
19
+ }
20
+ let lastErr = null;
21
+ for (let attempt = 1; attempt <= WEBHOOK_RETRIES; attempt++) {
22
+ try {
23
+ await (0, ai_1.httpRequest)(url, {
24
+ method,
25
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
26
+ timeout: WEBHOOK_TIMEOUT,
27
+ }, plug.body || undefined);
28
+ log.info(`WEBHOOK: boiler ${on ? 'true' : 'false'} (attempt ${attempt})`);
29
+ return;
30
+ }
31
+ catch (err) {
32
+ lastErr = err;
33
+ log.warn(`WEBHOOK: attempt ${attempt} failed: ${lastErr.message}`);
34
+ if (attempt < WEBHOOK_RETRIES) {
35
+ await sleep(WEBHOOK_RETRY_DELAY);
36
+ }
37
+ }
38
+ }
39
+ throw new Error(`Webhook failed after ${WEBHOOK_RETRIES} attempts: ${lastErr?.message}`);
40
+ }
41
+ function sleep(ms) {
42
+ return new Promise(resolve => setTimeout(resolve, ms));
43
+ }
44
+ //# sourceMappingURL=boiler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boiler.js","sourceRoot":"","sources":["../src/boiler.ts"],"names":[],"mappings":";;AAQA,kCAkCC;AAxCD,6BAAmC;AAEnC,MAAM,eAAe,GAAG,CAAC,CAAC;AAC1B,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAE1B,KAAK,UAAU,WAAW,CAAC,EAAW,EAAE,IAAsB,EAAE,GAAW;IAChF,MAAM,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;IAC1C,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;IAEpD,IAAI,OAAO,GAA2B,EAAE,CAAC;IACzC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAED,IAAI,OAAO,GAAiB,IAAI,CAAC;IACjC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,eAAe,EAAE,OAAO,EAAE,EAAE,CAAC;QAC5D,IAAI,CAAC;YACH,MAAM,IAAA,gBAAW,EAAC,GAAG,EAAE;gBACrB,MAAM;gBACN,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;gBAC9D,OAAO,EAAE,eAAe;aACzB,EAAE,IAAI,CAAC,IAAI,IAAI,SAAS,CAAC,CAAC;YAE3B,GAAG,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,aAAa,OAAO,GAAG,CAAC,CAAC;YAC1E,OAAO;QACT,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,GAAG,GAAY,CAAC;YACvB,GAAG,CAAC,IAAI,CAAC,oBAAoB,OAAO,YAAY,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;YACnE,IAAI,OAAO,GAAG,eAAe,EAAE,CAAC;gBAC9B,MAAM,KAAK,CAAC,mBAAmB,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,wBAAwB,eAAe,cAAc,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AAC3F,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { PlatformAccessory, CharacteristicValue, Logger } from 'homebridge';
2
+ import { BoilerAIPlatform } from './platform';
3
+ export declare class BoilerAccessory {
4
+ private readonly platform;
5
+ private readonly accessory;
6
+ private readonly log;
7
+ private service;
8
+ constructor(platform: BoilerAIPlatform, accessory: PlatformAccessory, log: Logger);
9
+ getOn(): Promise<CharacteristicValue>;
10
+ setOn(value: CharacteristicValue): Promise<void>;
11
+ updateState(on: boolean): void;
12
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BoilerAccessory = void 0;
4
+ class BoilerAccessory {
5
+ constructor(platform, accessory, log) {
6
+ this.platform = platform;
7
+ this.accessory = accessory;
8
+ this.log = log;
9
+ this.accessory.getService(this.platform.Service.AccessoryInformation)
10
+ .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Boiler AI')
11
+ .setCharacteristic(this.platform.Characteristic.Model, 'Solar Hot Water Controller')
12
+ .setCharacteristic(this.platform.Characteristic.SerialNumber, 'BOILER-AI-001');
13
+ this.service = this.accessory.getService(this.platform.Service.Switch)
14
+ || this.accessory.addService(this.platform.Service.Switch, 'Boiler AI');
15
+ this.service.getCharacteristic(this.platform.Characteristic.On)
16
+ .onGet(this.getOn.bind(this))
17
+ .onSet(this.setOn.bind(this));
18
+ }
19
+ async getOn() {
20
+ return this.platform.isBoilerOn();
21
+ }
22
+ async setOn(value) {
23
+ if (value) {
24
+ this.log.info('HomeKit: triggering AI decision cycle');
25
+ this.platform.triggerDecisionCycle('homekit');
26
+ }
27
+ else {
28
+ this.log.info('HomeKit: emergency stop');
29
+ await this.platform.stopBoiler();
30
+ }
31
+ }
32
+ updateState(on) {
33
+ this.service.updateCharacteristic(this.platform.Characteristic.On, on);
34
+ }
35
+ }
36
+ exports.BoilerAccessory = BoilerAccessory;
37
+ //# sourceMappingURL=boilerAccessory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boilerAccessory.js","sourceRoot":"","sources":["../src/boilerAccessory.ts"],"names":[],"mappings":";;;AAGA,MAAa,eAAe;IAG1B,YACmB,QAA0B,EAC1B,SAA4B,EAC5B,GAAW;QAFX,aAAQ,GAAR,QAAQ,CAAkB;QAC1B,cAAS,GAAT,SAAS,CAAmB;QAC5B,QAAG,GAAH,GAAG,CAAQ;QAE5B,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,oBAAoB,CAAE;aACnE,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,YAAY,EAAE,WAAW,CAAC;aACzE,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,EAAE,4BAA4B,CAAC;aACnF,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC;QAEjF,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;eACjE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAE1E,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;aAC5D,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;aAC5B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,KAAK;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAA0B;QACpC,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAC;YACvD,IAAI,CAAC,QAAQ,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC;QAChD,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;YACzC,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;QACnC,CAAC;IACH,CAAC;IAED,WAAW,CAAC,EAAW;QACrB,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACzE,CAAC;CACF;AAtCD,0CAsCC"}
@@ -0,0 +1,3 @@
1
+ import { API } from 'homebridge';
2
+ declare const _default: (api: API) => void;
3
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const settings_1 = require("./settings");
4
+ const platform_1 = require("./platform");
5
+ exports.default = (api) => {
6
+ api.registerPlatform(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, platform_1.BoilerAIPlatform);
7
+ };
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AACA,yCAAwD;AACxD,yCAA8C;AAE9C,kBAAe,CAAC,GAAQ,EAAQ,EAAE;IAChC,GAAG,CAAC,gBAAgB,CAAC,sBAAW,EAAE,wBAAa,EAAE,2BAAgB,CAAC,CAAC;AACrE,CAAC,CAAC"}