homebridge-la-marzocco 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Matt Weiden
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,78 @@
1
+ # Homebridge plugin for La Marzocco
2
+ [![Tests](https://github.com/mweiden/lm-homebridge/actions/workflows/tests.yml/badge.svg)](https://github.com/mweiden/lm-homebridge/actions/workflows/tests.yml)
3
+ [![codecov](https://codecov.io/gh/mweiden/lm-homebridge/branch/main/graph/badge.svg)](https://codecov.io/gh/mweiden/lm-homebridge)
4
+
5
+ [Homebridge](https://homebridge.io/) plugin for La Marzocco espresso machines. Enables turning your La Marzocco IoT-enabled espresso machine (i.e. a Linea Micra or Linea Mini) on and off with [Apple HomeKit](https://en.wikipedia.org/wiki/Apple_Home).
6
+
7
+ <img height="600" alt="IMG_2168" src="https://github.com/user-attachments/assets/b8f2e146-a8cb-42f2-9bc0-d658649d067d" />
8
+
9
+ ## Configure
10
+ Add the platform in your Homebridge `config.json`:
11
+
12
+ ```json
13
+ {
14
+ "platforms": [
15
+ {
16
+ "platform": "LaMarzocco",
17
+ "name": "La Marzocco",
18
+ "serial": "YOUR_SERIAL",
19
+ "username": "YOUR_USERNAME",
20
+ "password": "YOUR_PASSWORD",
21
+ "pollIntervalSeconds": 30
22
+ }
23
+ ]
24
+ }
25
+ ```
26
+
27
+ The plugin stores the installation key under the Homebridge storage path by
28
+ default. Override with `installationKeyPath` if needed. Set
29
+ `pollIntervalSeconds` to `0` to disable polling.
30
+
31
+ ## Development
32
+
33
+ ### Install for development
34
+
35
+ Using the `homebridge` terminal copy the repo into your machine and
36
+
37
+ ```bash
38
+ npm install --prefix /var/lib/homebridge <path_to_homebridge_repo>
39
+ ```
40
+
41
+ You will then have to put homebridge into Debug mode and restart homebridge.
42
+
43
+ ### Prerequisites
44
+ - Node.js 18+ (for built-in `fetch` and crypto support).
45
+
46
+ ### Unit Tests
47
+ Run unit tests with:
48
+
49
+ ```bash
50
+ npx jest
51
+ ```
52
+
53
+ ### Manual integration test
54
+ This repo includes a Node.js script that exercises the LM cloud API flow using
55
+ the local client library.
56
+
57
+ Set environment variables and run the script:
58
+
59
+ ```bash
60
+ export LM_SERIAL="YOUR_SERIAL"
61
+ export LM_USERNAME="YOUR_USERNAME"
62
+ export LM_PASSWORD="YOUR_PASSWORD"
63
+ node scripts/lm_manual_test.js
64
+ ```
65
+
66
+ To toggle power:
67
+
68
+ ```bash
69
+ node scripts/lm_manual_test.js --power on
70
+ node scripts/lm_manual_test.js --power off
71
+ ```
72
+
73
+ The script stores the installation key in `installation_key.json` by default and
74
+ will auto-register it on first run. Override the location with `LM_KEY_PATH`.
75
+
76
+ ## Acknowledgements
77
+
78
+ Full props to @zweckj for figuring out the LM API. The client code here is based on https://github.com/zweckj/pylamarzocco.
@@ -0,0 +1,43 @@
1
+ {
2
+ "pluginAlias": "LaMarzocco",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "schema": {
6
+ "type": "object",
7
+ "properties": {
8
+ "name": {
9
+ "title": "Platform Name",
10
+ "type": "string",
11
+ "default": "La Marzocco"
12
+ },
13
+ "serial": {
14
+ "title": "Machine Serial Number",
15
+ "type": "string"
16
+ },
17
+ "username": {
18
+ "title": "LM Cloud Username",
19
+ "type": "string"
20
+ },
21
+ "password": {
22
+ "title": "LM Cloud Password",
23
+ "type": "string",
24
+ "format": "password"
25
+ },
26
+ "installationKeyPath": {
27
+ "title": "Installation Key Path",
28
+ "type": "string"
29
+ },
30
+ "pollIntervalSeconds": {
31
+ "title": "Polling Interval (seconds)",
32
+ "type": "integer",
33
+ "default": 30,
34
+ "minimum": 0
35
+ }
36
+ },
37
+ "required": [
38
+ "serial",
39
+ "username",
40
+ "password"
41
+ ]
42
+ }
43
+ }
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+
5
+ const BASE_URL = "https://lion.lamarzocco.io";
6
+ const CUSTOMER_APP_URL = `${BASE_URL}/api/customer-app`;
7
+ const TOKEN_TIME_TO_REFRESH_MS = 10 * 60 * 1000;
8
+ const TOKEN_EXPIRATION_MS = 60 * 60 * 1000;
9
+
10
+ function b64(buf) {
11
+ return Buffer.from(buf).toString("base64");
12
+ }
13
+
14
+ function sha256(buf) {
15
+ return crypto.createHash("sha256").update(buf).digest();
16
+ }
17
+
18
+ function generateRequestProof(baseString, secret32) {
19
+ if (!Buffer.isBuffer(secret32) || secret32.length !== 32) {
20
+ throw new Error("secret must be 32 bytes");
21
+ }
22
+
23
+ const work = Buffer.from(secret32);
24
+ const input = Buffer.from(baseString, "utf8");
25
+
26
+ for (const byteVal of input) {
27
+ const idx = byteVal % 32;
28
+ const shiftIdx = (idx + 1) % 32;
29
+ const shiftAmount = work[shiftIdx] & 7;
30
+
31
+ const xorResult = byteVal ^ work[idx];
32
+ const rotated = ((xorResult << shiftAmount) | (xorResult >> (8 - shiftAmount))) & 0xff;
33
+ work[idx] = rotated;
34
+ }
35
+
36
+ return b64(sha256(work));
37
+ }
38
+
39
+ function generateInstallationKey(installationId) {
40
+ const { privateKey } = crypto.generateKeyPairSync("ec", {
41
+ namedCurve: "prime256v1",
42
+ });
43
+
44
+ const privateKeyDer = privateKey.export({ format: "der", type: "pkcs8" });
45
+ const publicKeyDer = crypto.createPublicKey(privateKey).export({ format: "der", type: "spki" });
46
+
47
+ const pubB64 = b64(publicKeyDer);
48
+ const instHashB64 = b64(sha256(Buffer.from(installationId, "utf8")));
49
+ const secret = sha256(Buffer.from(`${installationId}.${pubB64}.${instHashB64}`, "utf8"));
50
+
51
+ return {
52
+ installation_id: installationId,
53
+ secret: b64(secret),
54
+ private_key: b64(privateKeyDer),
55
+ };
56
+ }
57
+
58
+ function parseInstallationKey(raw) {
59
+ if (!raw || !raw.installation_id || !raw.secret || !raw.private_key) {
60
+ throw new Error("Invalid installation key data");
61
+ }
62
+
63
+ return {
64
+ installation_id: raw.installation_id,
65
+ secret: Buffer.from(raw.secret, "base64"),
66
+ private_key: raw.private_key,
67
+ };
68
+ }
69
+
70
+ function loadPrivateKey(installationKey) {
71
+ return crypto.createPrivateKey({
72
+ key: Buffer.from(installationKey.private_key, "base64"),
73
+ format: "der",
74
+ type: "pkcs8",
75
+ });
76
+ }
77
+
78
+ function publicKeyBase64(installationKey) {
79
+ const privateKey = loadPrivateKey(installationKey);
80
+ const publicDer = crypto.createPublicKey(privateKey).export({ format: "der", type: "spki" });
81
+ return b64(publicDer);
82
+ }
83
+
84
+ function baseStringForRegistration(installationKey) {
85
+ const privateKey = loadPrivateKey(installationKey);
86
+ const publicDer = crypto.createPublicKey(privateKey).export({ format: "der", type: "spki" });
87
+ const pubHashB64 = b64(sha256(publicDer));
88
+ return `${installationKey.installation_id}.${pubHashB64}`;
89
+ }
90
+
91
+ function generateExtraRequestHeaders(installationKey) {
92
+ const nonce = crypto.randomUUID().toLowerCase();
93
+ const timestamp = Date.now().toString();
94
+ const proofInput = `${installationKey.installation_id}.${nonce}.${timestamp}`;
95
+ const proof = generateRequestProof(proofInput, installationKey.secret);
96
+ const signatureData = `${proofInput}.${proof}`;
97
+
98
+ const signer = crypto.createSign("SHA256");
99
+ signer.update(signatureData, "utf8");
100
+ signer.end();
101
+
102
+ const privateKey = loadPrivateKey(installationKey);
103
+ const signature = signer.sign(privateKey);
104
+
105
+ return {
106
+ "X-App-Installation-Id": installationKey.installation_id,
107
+ "X-Timestamp": timestamp,
108
+ "X-Nonce": nonce,
109
+ "X-Request-Signature": b64(signature),
110
+ };
111
+ }
112
+
113
+ const DEFAULT_TIMEOUT_MS = 10000;
114
+
115
+ async function jsonRequest(url, options) {
116
+ const timeoutMs =
117
+ typeof options.timeoutMs === "number" ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
118
+ const controller = new AbortController();
119
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
120
+ const { timeoutMs: _timeoutMs, ...fetchOptions } = options;
121
+
122
+ let response;
123
+ try {
124
+ response = await fetch(url, { ...fetchOptions, signal: controller.signal });
125
+ } catch (err) {
126
+ if (err && err.name === "AbortError") {
127
+ const timeoutError = new Error(`Request timed out after ${timeoutMs}ms`);
128
+ timeoutError.code = "ETIMEDOUT";
129
+ throw timeoutError;
130
+ }
131
+ throw err;
132
+ } finally {
133
+ clearTimeout(timeoutId);
134
+ }
135
+ const text = await response.text();
136
+ let data;
137
+ try {
138
+ data = text ? JSON.parse(text) : null;
139
+ } catch (err) {
140
+ data = text;
141
+ }
142
+
143
+ if (!response.ok) {
144
+ const error = new Error(`Request failed: ${response.status} ${response.statusText}`);
145
+ error.status = response.status;
146
+ error.payload = data;
147
+ throw error;
148
+ }
149
+
150
+ return data;
151
+ }
152
+
153
+ class LaMarzoccoCloudClient {
154
+ constructor({ username, password, installationKey }) {
155
+ this.username = username;
156
+ this.password = password;
157
+ this.installationKey = parseInstallationKey(installationKey);
158
+ this.token = null;
159
+ }
160
+
161
+ async registerClient() {
162
+ const baseString = baseStringForRegistration(this.installationKey);
163
+ const proof = generateRequestProof(baseString, this.installationKey.secret);
164
+
165
+ const headers = {
166
+ "X-App-Installation-Id": this.installationKey.installation_id,
167
+ "X-Request-Proof": proof,
168
+ };
169
+
170
+ const body = {
171
+ pk: publicKeyBase64(this.installationKey),
172
+ };
173
+
174
+ await jsonRequest(`${CUSTOMER_APP_URL}/auth/init`, {
175
+ method: "POST",
176
+ headers: {
177
+ ...headers,
178
+ "Content-Type": "application/json",
179
+ },
180
+ body: JSON.stringify(body),
181
+ });
182
+ }
183
+
184
+ async getAccessToken() {
185
+ if (!this.token) {
186
+ this.token = await this.signIn();
187
+ return this.token.access_token;
188
+ }
189
+
190
+ const now = Date.now();
191
+ if (this.token.expires_at <= now) {
192
+ this.token = await this.signIn();
193
+ return this.token.access_token;
194
+ }
195
+
196
+ if (this.token.expires_at <= now + TOKEN_TIME_TO_REFRESH_MS) {
197
+ this.token = await this.refreshToken();
198
+ return this.token.access_token;
199
+ }
200
+
201
+ return this.token.access_token;
202
+ }
203
+
204
+ async signIn() {
205
+ const headers = generateExtraRequestHeaders(this.installationKey);
206
+ const data = await jsonRequest(`${CUSTOMER_APP_URL}/auth/signin`, {
207
+ method: "POST",
208
+ headers: {
209
+ ...headers,
210
+ "Content-Type": "application/json",
211
+ },
212
+ body: JSON.stringify({
213
+ username: this.username,
214
+ password: this.password,
215
+ }),
216
+ });
217
+
218
+ return {
219
+ access_token: data.accessToken,
220
+ refresh_token: data.refreshToken,
221
+ expires_at: Date.now() + TOKEN_EXPIRATION_MS,
222
+ };
223
+ }
224
+
225
+ async refreshToken() {
226
+ if (!this.token) {
227
+ return this.signIn();
228
+ }
229
+
230
+ const headers = generateExtraRequestHeaders(this.installationKey);
231
+ const data = await jsonRequest(`${CUSTOMER_APP_URL}/auth/refreshtoken`, {
232
+ method: "POST",
233
+ headers: {
234
+ ...headers,
235
+ "Content-Type": "application/json",
236
+ },
237
+ body: JSON.stringify({
238
+ username: this.username,
239
+ refreshToken: this.token.refresh_token,
240
+ }),
241
+ });
242
+
243
+ return {
244
+ access_token: data.accessToken,
245
+ refresh_token: data.refreshToken,
246
+ expires_at: Date.now() + TOKEN_EXPIRATION_MS,
247
+ };
248
+ }
249
+
250
+ async apiCall({ url, method, body }) {
251
+ const token = await this.getAccessToken();
252
+ const headers = {
253
+ ...generateExtraRequestHeaders(this.installationKey),
254
+ Authorization: `Bearer ${token}`,
255
+ "Content-Type": "application/json",
256
+ };
257
+
258
+ return jsonRequest(url, {
259
+ method,
260
+ headers,
261
+ body: body ? JSON.stringify(body) : undefined,
262
+ });
263
+ }
264
+
265
+ async getDashboard(serialNumber) {
266
+ return this.apiCall({
267
+ url: `${CUSTOMER_APP_URL}/things/${serialNumber}/dashboard`,
268
+ method: "GET",
269
+ });
270
+ }
271
+
272
+ async setPower(serialNumber, enabled) {
273
+ const mode = enabled ? "BrewingMode" : "StandBy";
274
+ const response = await this.apiCall({
275
+ url: `${CUSTOMER_APP_URL}/things/${serialNumber}/command/CoffeeMachineChangeMode`,
276
+ method: "POST",
277
+ body: { mode },
278
+ });
279
+
280
+ return response;
281
+ }
282
+ }
283
+
284
+ function extractPowerFromDashboard(dashboard) {
285
+ if (!dashboard || !Array.isArray(dashboard.widgets)) {
286
+ return null;
287
+ }
288
+
289
+ const statusWidget = dashboard.widgets.find(
290
+ (widget) => widget && widget.code === "CMMachineStatus"
291
+ );
292
+ if (!statusWidget || !statusWidget.output || !statusWidget.output.mode) {
293
+ return null;
294
+ }
295
+
296
+ return statusWidget.output.mode === "BrewingMode";
297
+ }
298
+
299
+ module.exports = {
300
+ LaMarzoccoCloudClient,
301
+ generateInstallationKey,
302
+ extractPowerFromDashboard,
303
+ _test:
304
+ process.env.NODE_ENV === "test"
305
+ ? {
306
+ generateRequestProof,
307
+ parseInstallationKey,
308
+ }
309
+ : undefined,
310
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "homebridge-la-marzocco",
3
+ "version": "0.1.0",
4
+ "description": "Homebridge integration for La Marzocco espresso machines",
5
+ "displayName": "La Marzocco",
6
+ "main": "src/index.js",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "homebridge-plugin",
10
+ "homebridge",
11
+ "lamarzocco",
12
+ "espresso"
13
+ ],
14
+ "scripts": {
15
+ "test": "NODE_ENV=test npx jest --coverage test/*.test.js",
16
+ "integration-test": "node scripts/lm_manual_test.js"
17
+ },
18
+ "engines": {
19
+ "node": ">=18.0.0",
20
+ "homebridge": "^1.8.0 || ^2.0.0-beta.0"
21
+ },
22
+ "author": "",
23
+ "files": [
24
+ "src",
25
+ "lib",
26
+ "config.schema.json",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "homebridge": {
31
+ "platforms": [
32
+ "LaMarzocco"
33
+ ]
34
+ },
35
+ "devDependencies": {
36
+ "jest": "^30.2.0"
37
+ }
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+
3
+ const { PLUGIN_NAME, PLATFORM_NAME } = require("./settings");
4
+ const { LaMarzoccoPlatform } = require("./platform");
5
+
6
+ module.exports = (api) => {
7
+ api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, LaMarzoccoPlatform);
8
+ };
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const {
7
+ LaMarzoccoCloudClient,
8
+ generateInstallationKey,
9
+ } = require("../lib/lm_client");
10
+ const { LaMarzoccoPlatformAccessory } = require("./platformAccessory");
11
+ const { PLUGIN_NAME, PLATFORM_NAME } = require("./settings");
12
+
13
+ class LaMarzoccoPlatform {
14
+ constructor(log, config, api) {
15
+ this.log = log;
16
+ this.config = config || {};
17
+ this.api = api;
18
+ this.accessories = [];
19
+
20
+ this.Service = this.api.hap.Service;
21
+ this.Characteristic = this.api.hap.Characteristic;
22
+
23
+ if (!config) {
24
+ this.log.warn("No config supplied. Skipping plugin setup.");
25
+ return;
26
+ }
27
+
28
+ this.name = this.config.name || "La Marzocco";
29
+ this.serial = this.config.serial;
30
+ this.username = this.config.username;
31
+ this.password = this.config.password;
32
+ this.pollIntervalSeconds = Number(this.config.pollIntervalSeconds || 30);
33
+
34
+ if (!this.serial || !this.username || !this.password) {
35
+ this.log.error(
36
+ "Missing required config. Please set serial, username, and password."
37
+ );
38
+ return;
39
+ }
40
+
41
+ const storageRoot = this.api.user.storagePath();
42
+ const defaultKeyPath = path.join(
43
+ storageRoot,
44
+ "lm-homebridge",
45
+ "installation_key.json"
46
+ );
47
+ this.installationKeyPath =
48
+ this.config.installationKeyPath || defaultKeyPath;
49
+ this.ensureKeyDir();
50
+
51
+ const { key, created } = this.loadOrCreateInstallationKey();
52
+ this.client = new LaMarzoccoCloudClient({
53
+ username: this.username,
54
+ password: this.password,
55
+ installationKey: key,
56
+ });
57
+
58
+ if (created) {
59
+ this.registerInstallationKey().catch((err) => {
60
+ this.log.error("Failed to register installation key: %s", err.message);
61
+ });
62
+ }
63
+
64
+ this.api.on("didFinishLaunching", () => {
65
+ this.discoverDevices();
66
+ });
67
+ }
68
+
69
+ configureAccessory(accessory) {
70
+ this.log.info("Loading accessory from cache: %s", accessory.displayName);
71
+ this.accessories.push(accessory);
72
+ }
73
+
74
+ ensureKeyDir() {
75
+ const dir = path.dirname(this.installationKeyPath);
76
+ if (!fs.existsSync(dir)) {
77
+ fs.mkdirSync(dir, { recursive: true });
78
+ }
79
+ }
80
+
81
+ loadOrCreateInstallationKey() {
82
+ if (fs.existsSync(this.installationKeyPath)) {
83
+ const raw = fs.readFileSync(this.installationKeyPath, "utf8");
84
+ return { key: JSON.parse(raw), created: false };
85
+ }
86
+
87
+ const installationId = crypto.randomUUID().toLowerCase();
88
+ const key = generateInstallationKey(installationId);
89
+ fs.writeFileSync(this.installationKeyPath, JSON.stringify(key, null, 2), {
90
+ mode: 0o600,
91
+ });
92
+ return { key, created: true };
93
+ }
94
+
95
+ async registerInstallationKey() {
96
+ this.log.info("Registering installation key with LM cloud...");
97
+ await this.client.registerClient();
98
+ this.log.info("Installation key registration complete.");
99
+ }
100
+
101
+ discoverDevices() {
102
+ const uuid = this.api.hap.uuid.generate(this.serial);
103
+ const existingAccessory = this.accessories.find(
104
+ (accessory) => accessory.UUID === uuid
105
+ );
106
+
107
+ if (existingAccessory) {
108
+ existingAccessory.context.device = {
109
+ name: this.name,
110
+ serial: this.serial,
111
+ };
112
+ new LaMarzoccoPlatformAccessory(this, existingAccessory);
113
+ this.api.updatePlatformAccessories([existingAccessory]);
114
+ return;
115
+ }
116
+
117
+ const accessory = new this.api.platformAccessory(this.name, uuid);
118
+ accessory.context.device = {
119
+ name: this.name,
120
+ serial: this.serial,
121
+ };
122
+
123
+ new LaMarzoccoPlatformAccessory(this, accessory);
124
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
125
+ }
126
+ }
127
+
128
+ module.exports = { LaMarzoccoPlatform };
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+
3
+ const { extractPowerFromDashboard } = require("../lib/lm_client");
4
+
5
+ class LaMarzoccoPlatformAccessory {
6
+ constructor(platform, accessory) {
7
+ this.platform = platform;
8
+ this.accessory = accessory;
9
+ this.cachedPower = false;
10
+
11
+ this.service =
12
+ this.accessory.getService(this.platform.Service.Switch) ||
13
+ this.accessory.addService(
14
+ this.platform.Service.Switch,
15
+ this.accessory.displayName
16
+ );
17
+
18
+ this.service
19
+ .getCharacteristic(this.platform.Characteristic.On)
20
+ .onGet(this.handleGet.bind(this))
21
+ .onSet(this.handleSet.bind(this));
22
+
23
+ const pollIntervalSeconds = this.platform.pollIntervalSeconds;
24
+ if (pollIntervalSeconds > 0) {
25
+ this.startPolling(pollIntervalSeconds);
26
+ }
27
+ }
28
+
29
+ async handleGet() {
30
+ if (!this.platform.client) {
31
+ return this.cachedPower;
32
+ }
33
+
34
+ try {
35
+ const dashboard = await this.platform.client.getDashboard(
36
+ this.platform.serial
37
+ );
38
+ const power = extractPowerFromDashboard(dashboard);
39
+ if (power === null) {
40
+ this.platform.log.warn(
41
+ "Unable to determine machine power from dashboard."
42
+ );
43
+ return this.cachedPower;
44
+ }
45
+ this.cachedPower = power;
46
+ return power;
47
+ } catch (err) {
48
+ this.platform.log.error(
49
+ "Failed to fetch dashboard: %s",
50
+ err.message || err
51
+ );
52
+ return this.cachedPower;
53
+ }
54
+ }
55
+
56
+ async handleSet(value) {
57
+ if (!this.platform.client) {
58
+ throw new Error("Accessory not configured.");
59
+ }
60
+
61
+ const enabled = value === true;
62
+ try {
63
+ await this.platform.client.setPower(this.platform.serial, enabled);
64
+ this.cachedPower = enabled;
65
+ } catch (err) {
66
+ this.platform.log.error(
67
+ "Failed to set power: %s",
68
+ err.message || err
69
+ );
70
+ throw err;
71
+ }
72
+ }
73
+
74
+ startPolling(intervalSeconds) {
75
+ const intervalMs = intervalSeconds * 1000;
76
+ setInterval(async () => {
77
+ try {
78
+ const power = await this.handleGet();
79
+ this.service.updateCharacteristic(
80
+ this.platform.Characteristic.On,
81
+ power
82
+ );
83
+ } catch (err) {
84
+ this.platform.log.debug("Polling error: %s", err.message || err);
85
+ }
86
+ }, intervalMs);
87
+ }
88
+ }
89
+
90
+ module.exports = { LaMarzoccoPlatformAccessory };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ const PLUGIN_NAME = "homebridge-lm-homebridge";
4
+ const PLATFORM_NAME = "LaMarzocco";
5
+
6
+ module.exports = {
7
+ PLUGIN_NAME,
8
+ PLATFORM_NAME,
9
+ };