homebridge-lovesac-stealthtech 0.0.0-development
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 +155 -0
- package/config.schema.json +96 -0
- package/dist/accessory.d.ts +36 -0
- package/dist/accessory.js +352 -0
- package/dist/ble/BleClient.d.ts +28 -0
- package/dist/ble/BleClient.js +170 -0
- package/dist/ble/BleConnectionManager.d.ts +29 -0
- package/dist/ble/BleConnectionManager.js +150 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/platform.d.ts +13 -0
- package/dist/platform.js +71 -0
- package/dist/protocol/LovesacDevice.d.ts +35 -0
- package/dist/protocol/LovesacDevice.js +227 -0
- package/dist/protocol/commands.d.ts +20 -0
- package/dist/protocol/commands.js +113 -0
- package/dist/protocol/constants.d.ts +40 -0
- package/dist/protocol/constants.js +81 -0
- package/dist/protocol/responses.d.ts +36 -0
- package/dist/protocol/responses.js +184 -0
- package/dist/scan.d.ts +7 -0
- package/dist/scan.js +62 -0
- package/dist/settings.d.ts +38 -0
- package/dist/settings.js +44 -0
- package/package.json +88 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Logger } from 'homebridge';
|
|
2
|
+
export type NotificationHandler = (data: Buffer) => void;
|
|
3
|
+
export interface IBleClient {
|
|
4
|
+
connect(address?: string): Promise<void>;
|
|
5
|
+
disconnect(): Promise<void>;
|
|
6
|
+
isConnected(): boolean;
|
|
7
|
+
resolvedAddress: string;
|
|
8
|
+
write(characteristicUuid: string, data: Buffer): Promise<void>;
|
|
9
|
+
subscribeNotifications(handler: NotificationHandler): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare class BleClient implements IBleClient {
|
|
12
|
+
private readonly log;
|
|
13
|
+
private peripheral;
|
|
14
|
+
private characteristics;
|
|
15
|
+
private notificationHandler;
|
|
16
|
+
private _connected;
|
|
17
|
+
private _resolvedAddress;
|
|
18
|
+
constructor(log: Logger);
|
|
19
|
+
get resolvedAddress(): string;
|
|
20
|
+
connect(address?: string): Promise<void>;
|
|
21
|
+
disconnect(): Promise<void>;
|
|
22
|
+
isConnected(): boolean;
|
|
23
|
+
write(characteristicUuid: string, data: Buffer): Promise<void>;
|
|
24
|
+
subscribeNotifications(handler: NotificationHandler): Promise<void>;
|
|
25
|
+
private scanForAnyDevice;
|
|
26
|
+
private scanForDevice;
|
|
27
|
+
private scan;
|
|
28
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BleClient = void 0;
|
|
7
|
+
const noble_1 = __importDefault(require("@stoprocent/noble"));
|
|
8
|
+
const settings_1 = require("../settings");
|
|
9
|
+
class BleClient {
|
|
10
|
+
log;
|
|
11
|
+
peripheral = null;
|
|
12
|
+
characteristics = {};
|
|
13
|
+
notificationHandler = null;
|
|
14
|
+
_connected = false;
|
|
15
|
+
_resolvedAddress = '';
|
|
16
|
+
constructor(log) {
|
|
17
|
+
this.log = log;
|
|
18
|
+
}
|
|
19
|
+
get resolvedAddress() {
|
|
20
|
+
return this._resolvedAddress;
|
|
21
|
+
}
|
|
22
|
+
async connect(address) {
|
|
23
|
+
if (this._connected) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let peripheral;
|
|
27
|
+
if (address) {
|
|
28
|
+
this.log.debug('BLE: Starting scan for %s...', address);
|
|
29
|
+
peripheral = await this.scanForDevice(address);
|
|
30
|
+
if (!peripheral) {
|
|
31
|
+
throw new Error(`Device ${address} not found`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.log.debug('BLE: Starting auto-discovery scan...');
|
|
36
|
+
peripheral = await this.scanForAnyDevice();
|
|
37
|
+
if (!peripheral) {
|
|
38
|
+
throw new Error('No Lovesac StealthTech device found');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const resolvedId = peripheral.address !== '' && peripheral.address !== 'unknown'
|
|
42
|
+
? peripheral.address
|
|
43
|
+
: peripheral.id ?? peripheral.uuid ?? 'unknown';
|
|
44
|
+
this.log.debug('BLE: Connecting to %s...', resolvedId);
|
|
45
|
+
this._resolvedAddress = resolvedId;
|
|
46
|
+
// Register disconnect handler BEFORE connecting to avoid race (P0-2)
|
|
47
|
+
peripheral.once('disconnect', () => {
|
|
48
|
+
this.log.debug('BLE: Disconnected');
|
|
49
|
+
this._connected = false;
|
|
50
|
+
this.peripheral = null;
|
|
51
|
+
this.characteristics = {};
|
|
52
|
+
});
|
|
53
|
+
await peripheral.connectAsync();
|
|
54
|
+
this._connected = true;
|
|
55
|
+
this.peripheral = peripheral;
|
|
56
|
+
this.log.debug('BLE: Discovering services and characteristics...');
|
|
57
|
+
const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync([settings_1.SOFA_SERVICE_UUID_SHORT], Object.values(settings_1.CharUUID));
|
|
58
|
+
for (const char of characteristics) {
|
|
59
|
+
this.characteristics[char.uuid] = char;
|
|
60
|
+
}
|
|
61
|
+
this.log.debug('BLE: Found %d characteristics', Object.keys(this.characteristics).length);
|
|
62
|
+
}
|
|
63
|
+
async disconnect() {
|
|
64
|
+
if (this.peripheral && this._connected) {
|
|
65
|
+
try {
|
|
66
|
+
await this.peripheral.disconnectAsync();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Already disconnected
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this._connected = false;
|
|
73
|
+
this.peripheral = null;
|
|
74
|
+
this.characteristics = {};
|
|
75
|
+
}
|
|
76
|
+
isConnected() {
|
|
77
|
+
return this._connected;
|
|
78
|
+
}
|
|
79
|
+
async write(characteristicUuid, data) {
|
|
80
|
+
const char = this.characteristics[characteristicUuid];
|
|
81
|
+
if (!char) {
|
|
82
|
+
throw new Error(`Characteristic ${characteristicUuid} not found. Available: ${Object.keys(this.characteristics).join(', ')}`);
|
|
83
|
+
}
|
|
84
|
+
// Write without response (as per protocol spec)
|
|
85
|
+
await char.writeAsync(data, true);
|
|
86
|
+
}
|
|
87
|
+
async subscribeNotifications(handler) {
|
|
88
|
+
this.notificationHandler = handler;
|
|
89
|
+
const upstream = this.characteristics[settings_1.CharUUID.UpStream];
|
|
90
|
+
if (!upstream) {
|
|
91
|
+
throw new Error('UpStream characteristic not found');
|
|
92
|
+
}
|
|
93
|
+
// Remove previous listeners to prevent leak on reconnect (P0-1)
|
|
94
|
+
upstream.removeAllListeners('data');
|
|
95
|
+
upstream.on('data', (data) => {
|
|
96
|
+
if (this.notificationHandler) {
|
|
97
|
+
try {
|
|
98
|
+
this.notificationHandler(data);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
this.log.error('BLE: Notification handler error: %s', err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
await upstream.subscribeAsync();
|
|
106
|
+
this.log.debug('BLE: Subscribed to UpStream notifications');
|
|
107
|
+
}
|
|
108
|
+
scanForAnyDevice() {
|
|
109
|
+
return this.scan((_peripheral) => {
|
|
110
|
+
return true; // accept the first device with the right service UUID
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
scanForDevice(address) {
|
|
114
|
+
const normalized = address.toLowerCase().replace(/[:-]/g, '');
|
|
115
|
+
return this.scan((peripheral) => {
|
|
116
|
+
const id = peripheral.id?.toLowerCase().replace(/[:-]/g, '') ?? '';
|
|
117
|
+
const addr = peripheral.address?.toLowerCase().replace(/[:-]/g, '') ?? '';
|
|
118
|
+
const uuid = peripheral.uuid?.toLowerCase().replace(/[:-]/g, '') ?? '';
|
|
119
|
+
return id === normalized || addr === normalized || uuid === normalized;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
scan(match) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const onDiscover = (peripheral) => {
|
|
125
|
+
if (!match(peripheral)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
cleanup();
|
|
130
|
+
const name = peripheral.advertisement?.localName ?? '(unnamed)';
|
|
131
|
+
this.log.info('BLE: Discovered device: %s', name);
|
|
132
|
+
resolve(peripheral);
|
|
133
|
+
};
|
|
134
|
+
const cleanup = () => {
|
|
135
|
+
noble_1.default.stopScanning();
|
|
136
|
+
noble_1.default.removeListener('discover', onDiscover);
|
|
137
|
+
};
|
|
138
|
+
const timeout = setTimeout(() => {
|
|
139
|
+
cleanup();
|
|
140
|
+
resolve(null);
|
|
141
|
+
}, settings_1.BLE_SCAN_TIMEOUT);
|
|
142
|
+
noble_1.default.on('discover', onDiscover);
|
|
143
|
+
const startScan = () => {
|
|
144
|
+
noble_1.default.startScanning([settings_1.SOFA_SERVICE_UUID_SHORT], false, (err) => {
|
|
145
|
+
if (err) {
|
|
146
|
+
clearTimeout(timeout);
|
|
147
|
+
cleanup();
|
|
148
|
+
reject(err);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
if (noble_1.default.state === 'poweredOn') {
|
|
153
|
+
startScan();
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
noble_1.default.once('stateChange', (state) => {
|
|
157
|
+
if (state === 'poweredOn') {
|
|
158
|
+
startScan();
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
clearTimeout(timeout);
|
|
162
|
+
cleanup();
|
|
163
|
+
reject(new Error(`Bluetooth adapter state: ${state}`));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
exports.BleClient = BleClient;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Logger } from 'homebridge';
|
|
2
|
+
import type { IBleClient, NotificationHandler } from './BleClient';
|
|
3
|
+
import type { BleCommand } from '../protocol/commands';
|
|
4
|
+
export declare class BleConnectionManager {
|
|
5
|
+
private readonly client;
|
|
6
|
+
private readonly address;
|
|
7
|
+
private readonly idleTimeout;
|
|
8
|
+
private readonly log;
|
|
9
|
+
private queue;
|
|
10
|
+
private processing;
|
|
11
|
+
private idleTimer;
|
|
12
|
+
private connected;
|
|
13
|
+
private notificationHandler;
|
|
14
|
+
private connectPromise;
|
|
15
|
+
private resolvedAddress;
|
|
16
|
+
private onReconnectCallback;
|
|
17
|
+
constructor(client: IBleClient, address: string, idleTimeout: number, log: Logger);
|
|
18
|
+
getResolvedAddress(): string;
|
|
19
|
+
setNotificationHandler(handler: NotificationHandler): void;
|
|
20
|
+
onReconnect(callback: () => void): void;
|
|
21
|
+
enqueue(command: BleCommand): Promise<void>;
|
|
22
|
+
ensureConnected(): Promise<void>;
|
|
23
|
+
disconnect(): Promise<void>;
|
|
24
|
+
isConnected(): boolean;
|
|
25
|
+
private doConnect;
|
|
26
|
+
private processQueue;
|
|
27
|
+
private resetIdleTimer;
|
|
28
|
+
private clearIdleTimer;
|
|
29
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BleConnectionManager = void 0;
|
|
4
|
+
const settings_1 = require("../settings");
|
|
5
|
+
class BleConnectionManager {
|
|
6
|
+
client;
|
|
7
|
+
address;
|
|
8
|
+
idleTimeout;
|
|
9
|
+
log;
|
|
10
|
+
queue = [];
|
|
11
|
+
processing = false;
|
|
12
|
+
idleTimer = null;
|
|
13
|
+
connected = false;
|
|
14
|
+
notificationHandler = null;
|
|
15
|
+
connectPromise = null;
|
|
16
|
+
resolvedAddress;
|
|
17
|
+
onReconnectCallback = null;
|
|
18
|
+
constructor(client, address, idleTimeout, log) {
|
|
19
|
+
this.client = client;
|
|
20
|
+
this.address = address;
|
|
21
|
+
this.idleTimeout = idleTimeout;
|
|
22
|
+
this.log = log;
|
|
23
|
+
this.resolvedAddress = address;
|
|
24
|
+
}
|
|
25
|
+
getResolvedAddress() {
|
|
26
|
+
return this.resolvedAddress;
|
|
27
|
+
}
|
|
28
|
+
setNotificationHandler(handler) {
|
|
29
|
+
this.notificationHandler = handler;
|
|
30
|
+
}
|
|
31
|
+
onReconnect(callback) {
|
|
32
|
+
this.onReconnectCallback = callback;
|
|
33
|
+
}
|
|
34
|
+
async enqueue(command) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
this.queue.push({ command, resolve, reject });
|
|
37
|
+
this.processQueue();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async ensureConnected() {
|
|
41
|
+
if (this.connected && this.client.isConnected()) {
|
|
42
|
+
this.resetIdleTimer();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Reuse in-flight connection attempt
|
|
46
|
+
if (this.connectPromise) {
|
|
47
|
+
await this.connectPromise;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.connectPromise = this.doConnect();
|
|
51
|
+
try {
|
|
52
|
+
await this.connectPromise;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
this.connectPromise = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async disconnect() {
|
|
59
|
+
this.clearIdleTimer();
|
|
60
|
+
this.connected = false;
|
|
61
|
+
await this.client.disconnect();
|
|
62
|
+
}
|
|
63
|
+
isConnected() {
|
|
64
|
+
return this.connected && this.client.isConnected();
|
|
65
|
+
}
|
|
66
|
+
async doConnect() {
|
|
67
|
+
const label = this.resolvedAddress || 'auto-discovery';
|
|
68
|
+
this.log.info('Connecting to %s...', label);
|
|
69
|
+
try {
|
|
70
|
+
await this.client.connect(this.address || undefined);
|
|
71
|
+
// After first connect, lock to the resolved address for reconnects
|
|
72
|
+
if (!this.address && this.client.resolvedAddress) {
|
|
73
|
+
this.resolvedAddress = this.client.resolvedAddress;
|
|
74
|
+
this.log.info('Auto-discovered device: %s', this.resolvedAddress);
|
|
75
|
+
}
|
|
76
|
+
await this.client.subscribeNotifications((data) => {
|
|
77
|
+
this.resetIdleTimer();
|
|
78
|
+
if (this.notificationHandler) {
|
|
79
|
+
this.notificationHandler(data);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
this.connected = true;
|
|
83
|
+
this.resetIdleTimer();
|
|
84
|
+
this.log.info('Connected to %s', this.resolvedAddress);
|
|
85
|
+
if (this.onReconnectCallback) {
|
|
86
|
+
this.onReconnectCallback();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
this.connected = false;
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async processQueue() {
|
|
95
|
+
if (this.processing) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.processing = true;
|
|
99
|
+
try {
|
|
100
|
+
while (this.queue.length > 0) {
|
|
101
|
+
const item = this.queue.shift();
|
|
102
|
+
try {
|
|
103
|
+
await this.ensureConnected();
|
|
104
|
+
await this.client.write(item.command.characteristicUuid, item.command.data);
|
|
105
|
+
this.resetIdleTimer();
|
|
106
|
+
item.resolve();
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
// On write failure, disconnect and retry once
|
|
110
|
+
this.log.warn('BLE write failed, reconnecting: %s', (0, settings_1.errorMessage)(err));
|
|
111
|
+
this.connected = false;
|
|
112
|
+
try {
|
|
113
|
+
await this.client.disconnect();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Ignore disconnect errors
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await this.ensureConnected();
|
|
120
|
+
await this.client.write(item.command.characteristicUuid, item.command.data);
|
|
121
|
+
this.resetIdleTimer();
|
|
122
|
+
item.resolve();
|
|
123
|
+
}
|
|
124
|
+
catch (retryErr) {
|
|
125
|
+
item.reject(retryErr);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
this.processing = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
resetIdleTimer() {
|
|
135
|
+
this.clearIdleTimer();
|
|
136
|
+
this.idleTimer = setTimeout(() => {
|
|
137
|
+
this.log.info('Idle timeout, disconnecting from %s', this.resolvedAddress);
|
|
138
|
+
this.disconnect().catch((err) => {
|
|
139
|
+
this.log.warn('Error during idle disconnect: %s', (0, settings_1.errorMessage)(err));
|
|
140
|
+
});
|
|
141
|
+
}, this.idleTimeout * 1000);
|
|
142
|
+
}
|
|
143
|
+
clearIdleTimer() {
|
|
144
|
+
if (this.idleTimer) {
|
|
145
|
+
clearTimeout(this.idleTimer);
|
|
146
|
+
this.idleTimer = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
exports.BleConnectionManager = BleConnectionManager;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
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.LovesacPlatform);
|
|
7
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { API, DynamicPlatformPlugin, Logger, PlatformAccessory } from 'homebridge';
|
|
2
|
+
import type { LovesacPlatformConfig } from './settings';
|
|
3
|
+
export declare class LovesacPlatform implements DynamicPlatformPlugin {
|
|
4
|
+
readonly log: Logger;
|
|
5
|
+
readonly config: LovesacPlatformConfig;
|
|
6
|
+
readonly api: API;
|
|
7
|
+
private readonly accessories;
|
|
8
|
+
private device;
|
|
9
|
+
private connectionManager;
|
|
10
|
+
constructor(log: Logger, config: LovesacPlatformConfig, api: API);
|
|
11
|
+
configureAccessory(_accessory: PlatformAccessory): void;
|
|
12
|
+
private discoverDevices;
|
|
13
|
+
}
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LovesacPlatform = void 0;
|
|
4
|
+
const settings_1 = require("./settings");
|
|
5
|
+
const accessory_1 = require("./accessory");
|
|
6
|
+
const BleClient_1 = require("./ble/BleClient");
|
|
7
|
+
const BleConnectionManager_1 = require("./ble/BleConnectionManager");
|
|
8
|
+
const LovesacDevice_1 = require("./protocol/LovesacDevice");
|
|
9
|
+
class LovesacPlatform {
|
|
10
|
+
log;
|
|
11
|
+
config;
|
|
12
|
+
api;
|
|
13
|
+
accessories = [];
|
|
14
|
+
device = null;
|
|
15
|
+
connectionManager = null;
|
|
16
|
+
constructor(log, config, api) {
|
|
17
|
+
this.log = log;
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.api = api;
|
|
20
|
+
this.log.info('Lovesac StealthTech platform initialized');
|
|
21
|
+
this.api.on('didFinishLaunching', () => {
|
|
22
|
+
this.discoverDevices();
|
|
23
|
+
});
|
|
24
|
+
this.api.on('shutdown', () => {
|
|
25
|
+
this.device?.stopPolling();
|
|
26
|
+
this.connectionManager?.disconnect().catch(() => { });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Required by DynamicPlatformPlugin — we don't use cached accessories for external accessories
|
|
30
|
+
configureAccessory(_accessory) {
|
|
31
|
+
// External accessories are not cached, so this is a no-op
|
|
32
|
+
}
|
|
33
|
+
discoverDevices() {
|
|
34
|
+
let devices = this.config.devices ?? [];
|
|
35
|
+
// If no devices configured at all, create a default entry for auto-discovery
|
|
36
|
+
if (devices.length === 0) {
|
|
37
|
+
this.log.info('No devices configured — will auto-discover via BLE.');
|
|
38
|
+
devices = [{}];
|
|
39
|
+
}
|
|
40
|
+
if (devices.length > 1) {
|
|
41
|
+
this.log.warn('Multiple devices configured — only the first device is supported in this version.');
|
|
42
|
+
}
|
|
43
|
+
const rawConfig = devices[0];
|
|
44
|
+
const deviceConfig = (0, settings_1.resolveDeviceConfig)(rawConfig);
|
|
45
|
+
if (deviceConfig.address) {
|
|
46
|
+
this.log.info('Setting up device: %s (%s)', deviceConfig.name, deviceConfig.address);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.log.info('Setting up device: %s (auto-discovery)', deviceConfig.name);
|
|
50
|
+
}
|
|
51
|
+
// Generate a stable UUID from the BLE address + plugin name
|
|
52
|
+
// _testSuffix in config allows generating a fresh identity for testing
|
|
53
|
+
// For auto-discovery, use a fixed seed so the identity is stable
|
|
54
|
+
const addressSeed = deviceConfig.address || 'auto';
|
|
55
|
+
const uuidSeed = 'lovesac-st:' + addressSeed + (rawConfig._testSuffix ?? '');
|
|
56
|
+
const uuid = this.api.hap.uuid.generate(uuidSeed);
|
|
57
|
+
const accessory = new this.api.platformAccessory(deviceConfig.name, uuid);
|
|
58
|
+
// Create BLE stack
|
|
59
|
+
const bleClient = new BleClient_1.BleClient(this.log);
|
|
60
|
+
this.connectionManager = new BleConnectionManager_1.BleConnectionManager(bleClient, deviceConfig.address, deviceConfig.idleTimeout, this.log);
|
|
61
|
+
this.device = new LovesacDevice_1.LovesacDevice(this.connectionManager, this.log);
|
|
62
|
+
const device = this.device;
|
|
63
|
+
// Create accessory handler
|
|
64
|
+
const handler = new accessory_1.LovesacAccessory(this, accessory, deviceConfig, device);
|
|
65
|
+
this.accessories.push(handler);
|
|
66
|
+
// Audio receiver icon — closer to a soundbar than the TV icon
|
|
67
|
+
accessory.category = 34 /* this.api.hap.Categories.AUDIO_RECEIVER */;
|
|
68
|
+
this.api.publishExternalAccessories(settings_1.PLUGIN_NAME, [accessory]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.LovesacPlatform = LovesacPlatform;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Logger } from 'homebridge';
|
|
2
|
+
import { BleConnectionManager } from '../ble/BleConnectionManager';
|
|
3
|
+
import { DeviceState } from './responses';
|
|
4
|
+
import { ResponseCode, PresetWriteValue, PresetReadValue, SourceValue } from './constants';
|
|
5
|
+
export type StateChangeListener = (code: ResponseCode, value: number) => void;
|
|
6
|
+
export declare class LovesacDevice {
|
|
7
|
+
private readonly connectionManager;
|
|
8
|
+
private readonly log;
|
|
9
|
+
readonly state: DeviceState;
|
|
10
|
+
private stateListeners;
|
|
11
|
+
private stateInitialized;
|
|
12
|
+
mcuVersion: string;
|
|
13
|
+
private versionListeners;
|
|
14
|
+
private pollTimer;
|
|
15
|
+
constructor(connectionManager: BleConnectionManager, log: Logger);
|
|
16
|
+
onStateChange(listener: StateChangeListener): void;
|
|
17
|
+
isStateInitialized(): boolean;
|
|
18
|
+
onVersionResolved(callback: () => void): void;
|
|
19
|
+
requestStateRefresh(): Promise<void>;
|
|
20
|
+
startPolling(intervalSeconds: number): void;
|
|
21
|
+
stopPolling(): void;
|
|
22
|
+
getResolvedAddress(): string;
|
|
23
|
+
setPower(on: boolean): Promise<void>;
|
|
24
|
+
setVolume(volume: number): Promise<void>;
|
|
25
|
+
volumeToPercent(volume: number): number;
|
|
26
|
+
percentToVolume(percent: number): number;
|
|
27
|
+
volumeUp(step: number): Promise<void>;
|
|
28
|
+
volumeDown(step: number): Promise<void>;
|
|
29
|
+
setMute(muted: boolean): Promise<void>;
|
|
30
|
+
setQuietMode(on: boolean): Promise<void>;
|
|
31
|
+
setSource(source: SourceValue): Promise<void>;
|
|
32
|
+
setPreset(preset: PresetWriteValue): Promise<void>;
|
|
33
|
+
isPresetActive(readValue: PresetReadValue): boolean;
|
|
34
|
+
private handleNotification;
|
|
35
|
+
}
|