Repository navigation
Expand file tree
/
Copy pathserver.js
More file actions
306 lines (265 loc) · 11.3 KB
/
Copy pathserver.js
File metadata and controls
306 lines (265 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
const express = require('express');
const path = require('path');
const fs = require('fs');
const http = require('http');
const WebSocket = require('ws');
const { createProxyMiddleware } = require('http-proxy-middleware');
const ini = require('ini');
const app = express();
const PORT = process.env.INGRESS_PORT || 8099;
// Get options from Home Assistant add-on config or environment
let options = {
device_name: 'Hag',
wake_word: 'computer',
stt_timeout: 15,
auto_start_listening: false
};
// Load credentials from file
let credentials = {};
try {
if (fs.existsSync(path.join(__dirname, 'credentals.ini'))) {
const config = ini.parse(fs.readFileSync(path.join(__dirname, 'credentals.ini'), 'utf-8'));
credentials = {
ha_url: config.HomeAssistantURL,
access_token: config.AccessToken
};
// Ensure HA URL has protocol
if (credentials.ha_url && !credentials.ha_url.startsWith('http://') && !credentials.ha_url.startsWith('https://')) {
credentials.ha_url = 'http://' + credentials.ha_url;
}
console.log('Credentials loaded:', { ha_url: credentials.ha_url, has_token: !!credentials.access_token });
}
} catch (e) {
console.log('Could not load credentials file, using environment variables');
}
// Try to load add-on options
try {
if (fs.existsSync('/data/options.json')) {
const addOnOptions = JSON.parse(fs.readFileSync('/data/options.json', 'utf8'));
options = { ...options, ...addOnOptions };
}
} catch (e) {
console.log('Using default options');
}
// Middleware
app.use(express.json());
app.use(express.static(path.join(__dirname, 'www')));
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
app.use('/openwakeword', express.static(path.join(__dirname, 'node_modules/openwakeword-wasm-browser')));
// Proxy requests to Home Assistant
if (credentials.ha_url) {
console.log('Configuring HTTP proxy for Home Assistant API...');
app.use('/api/ha', createProxyMiddleware({
target: credentials.ha_url,
changeOrigin: true,
secure: false, // Disable SSL verification for now
logLevel: 'debug',
pathRewrite: {
'^/api/ha': '/api'
},
onProxyReq: (proxyReq, req, res) => {
console.log('Proxying request to:', proxyReq.getHeader('host') + proxyReq.path);
// Add authorization header
if (credentials.access_token) {
proxyReq.setHeader('Authorization', `Bearer ${credentials.access_token}`);
}
},
onError: (err, req, res) => {
console.error('HTTP proxy error:', err);
res.status(500).json({ error: 'Proxy error', details: err.message });
}
}));
}
// API endpoint to get configuration
app.get('/api/config', async (req, res) => {
console.log('Config API called');
// Fetch HA config to get uuid
if (credentials.ha_url && credentials.access_token) {
try {
const haConfigResponse = await fetch(credentials.ha_url + '/api/core/config', {
headers: {
'Authorization': `Bearer ${credentials.access_token}`,
'Content-Type': 'application/json'
}
});
if (haConfigResponse.ok) {
const responseText = await haConfigResponse.text();
console.log('HA core config response text:', responseText);
try {
const haConfig = JSON.parse(responseText);
console.log('HA core config parsed:', JSON.stringify(haConfig, null, 2));
console.log('HA UUID:', haConfig.uuid);
} catch (e) {
console.log('Failed to parse HA core config JSON:', e.message);
}
} else {
console.log('Failed to fetch HA core config:', haConfigResponse.status, haConfigResponse.statusText);
}
} catch (error) {
console.log('Error fetching HA core config:', error.message);
}
}
res.json({
options,
credentials,
supervisor_token: process.env.SUPERVISOR_TOKEN || null,
ingress_path: process.env.INGRESS_PATH || '',
ha_url: process.env.HA_URL || credentials.ha_url || null
});
});
// API endpoint for saving device name
app.post('/api/device-name', (req, res) => {
const { deviceName } = req.body;
options.device_name = deviceName;
// Persist to file if possible
try {
fs.writeFileSync('/data/device_name.txt', deviceName);
} catch (e) {
console.log('Could not persist device name');
}
res.json({ success: true, deviceName });
});
// Serve the main app
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'www', 'index.html'));
});
// Create HTTP server
const server = http.createServer(app);
// WebSocket proxy using ws library
if (credentials.ha_url) {
const wsProxy = new WebSocket.Server({ server, path: '/ws/ha' });
wsProxy.on('connection', (clientWs) => {
console.log('WebSocket proxy connection established from client');
// Connect to HA WebSocket (without token in URL - we'll handle auth properly)
const targetUrl = credentials.ha_url.replace(/^https/, 'wss').replace(/^http/, 'ws') + '/api/websocket';
console.log('Connecting to HA WebSocket:', targetUrl);
const targetWs = new WebSocket(targetUrl);
let haConnected = false;
let clientMessages = [];
targetWs.on('open', () => {
console.log('Connected to HA WebSocket');
haConnected = true;
// Send any queued messages
while (clientMessages.length > 0) {
const msg = clientMessages.shift();
targetWs.send(msg);
}
});
// Forward messages from HA to client
targetWs.on('message', (data, isBinary) => {
// Handle binary audio data (for TTS streaming)
if (isBinary) {
console.log('Forwarding binary audio from HA, length:', data.length);
if (clientWs.readyState === WebSocket.OPEN) {
clientWs.send(data);
}
return;
}
const dataStr = data.toString();
console.log('Received message from HA');
console.log('Received from HA:', dataStr.substring(0, 200) + (dataStr.length > 200 ? '...' : ''));
try {
const message = JSON.parse(dataStr);
// Handle auth_required from HA - send our server-side token
if (message.type === 'auth_required') {
console.log('HA requires auth, sending server-side access token');
console.log('Token being sent:', credentials.access_token ? credentials.access_token.substring(0, 20) + '...' : 'NO TOKEN');
targetWs.send(JSON.stringify({
type: 'auth',
access_token: credentials.access_token
}));
// Don't forward auth_required to client - we handle it server-side
return;
}
// Forward auth_ok and auth_invalid to client so it knows the result
if (message.type === 'auth_ok') {
console.log('HA auth successful, notifying client');
clientWs.send(JSON.stringify({ type: 'auth_ok' }));
return;
}
if (message.type === 'auth_invalid') {
console.log('HA auth failed:', message.message);
clientWs.send(JSON.stringify({ type: 'auth_invalid', message: message.message }));
return;
}
} catch (e) {
// Not JSON, just forward as-is
}
// Forward all other messages to client
if (clientWs.readyState === WebSocket.OPEN) {
clientWs.send(data);
}
});
// Forward messages from client to HA
clientWs.on('message', (data, isBinary) => {
// Handle binary audio data (for STT)
if (isBinary) {
console.log('Forwarding binary audio data to HA, length:', data.length);
if (haConnected && targetWs.readyState === WebSocket.OPEN) {
targetWs.send(data);
}
return;
}
const dataStr = data.toString();
console.log('Received from client:', dataStr);
try {
const message = JSON.parse(dataStr);
// Client sends auth but we already handled it server-side, ignore
if (message.type === 'auth') {
console.log('Ignoring client auth message (handled server-side)');
return;
}
} catch (e) {
// Not JSON
}
// Forward to HA if connected, otherwise queue
if (haConnected && targetWs.readyState === WebSocket.OPEN) {
console.log('Forwarding to HA:', dataStr.substring(0, 100));
targetWs.send(dataStr);
} else {
console.log('HA not connected yet, queueing message. haConnected:', haConnected, 'readyState:', targetWs.readyState);
clientMessages.push(dataStr);
}
});
targetWs.on('close', (code, reason) => {
console.log('HA WebSocket closed:', code, reason ? reason.toString() : '');
if (clientWs.readyState === WebSocket.OPEN) {
const validCode = (typeof code === 'number' && code >= 1000 && code <= 4999) ? code : 1000;
clientWs.close(validCode, reason);
}
});
targetWs.on('error', (error) => {
console.error('HA WebSocket error:', error);
if (clientWs.readyState === WebSocket.OPEN) {
clientWs.close(1006, 'Proxy error');
}
});
clientWs.on('close', (code, reason) => {
console.log('Client WebSocket closed:', code, reason ? reason.toString() : '');
if (targetWs.readyState === WebSocket.OPEN) {
const validCode = (typeof code === 'number' && code >= 1000 && code <= 4999) ? code : 1000;
targetWs.close(validCode, reason);
}
});
clientWs.on('error', (error) => {
console.error('Client WebSocket error:', error);
if (targetWs.readyState === WebSocket.OPEN) {
targetWs.close(1006, 'Client error');
}
});
});
}
server.listen(PORT, '0.0.0.0', () => {
console.log(`Hag server running on port ${PORT}`);
});
server.on('error', (error) => {
console.error('Server error:', error);
});
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
process.exit(1);
});