mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
130 lines
4.0 KiB
JavaScript
130 lines
4.0 KiB
JavaScript
// Home Assistant Transport
|
|
// Purpose: Manages websocket auth connection lifecycle, entity subscription, and reconnect behavior.
|
|
// Scope: Handles Home Assistant network transport and service-call plumbing without business policy logic.
|
|
const WebSocket = require('ws');
|
|
const { createConnection, subscribeEntities, callService, Auth } = require('home-assistant-js-websocket');
|
|
const { runtime } = require('./state');
|
|
|
|
if (!global.WebSocket) {
|
|
global.WebSocket = WebSocket;
|
|
}
|
|
|
|
function createTransport(deps) {
|
|
const { logger, enabled, haConfig, onSnapshot, onStatus } = deps;
|
|
function getCallerFrame() {
|
|
const stack = new Error().stack || '';
|
|
const lines = stack.split('\n').slice(2).map((line) => line.trim());
|
|
const frame = lines.find((line) => !line.includes('transport.js') && !line.includes('node:internal')) || null;
|
|
return frame;
|
|
}
|
|
|
|
function buildAuth() {
|
|
const token = haConfig?.token?.trim();
|
|
const url = haConfig?.url?.trim();
|
|
if (!token || !url) {
|
|
throw new Error('Home Assistant url/token missing');
|
|
}
|
|
return new Auth(
|
|
{
|
|
hassUrl: url.replace(/\/+$/, ''),
|
|
access_token: token,
|
|
expires: Date.now() + 1000 * 60 * 60 * 24 * 365 * 10,
|
|
refresh_token: null,
|
|
clientId: 'multiroomba-rover',
|
|
},
|
|
null,
|
|
);
|
|
}
|
|
|
|
function teardownConnection() {
|
|
if (runtime.unsubscribeEntities) {
|
|
try {
|
|
runtime.unsubscribeEntities();
|
|
} catch (err) {
|
|
logger.warn('Failed to unsubscribe entity stream', err.message);
|
|
}
|
|
}
|
|
runtime.unsubscribeEntities = null;
|
|
|
|
if (runtime.connection) {
|
|
try {
|
|
runtime.connection.close();
|
|
} catch (err) {
|
|
logger.warn('Error closing Home Assistant connection', err.message);
|
|
}
|
|
}
|
|
|
|
runtime.connection = null;
|
|
const wasConnected = runtime.connected;
|
|
runtime.connected = false;
|
|
if (wasConnected) {
|
|
onStatus();
|
|
}
|
|
}
|
|
|
|
function scheduleReconnect(delayMs = 5000) {
|
|
if (!enabled) return;
|
|
if (runtime.reconnectTimer) return;
|
|
runtime.reconnectTimer = setTimeout(() => {
|
|
runtime.reconnectTimer = null;
|
|
connect();
|
|
}, delayMs);
|
|
}
|
|
|
|
async function connect() {
|
|
if (!enabled) {
|
|
// Disabled and misconfigured are intentionally different states. The
|
|
// explicit switch prevents connection attempts; missing credentials are
|
|
// surfaced by buildAuth() as a runtime connection failure when enabled.
|
|
logger.info('Home Assistant disabled by config');
|
|
return;
|
|
}
|
|
if (runtime.connection) return;
|
|
|
|
try {
|
|
const auth = buildAuth();
|
|
runtime.connection = await createConnection({ auth, setupRetry: 0 });
|
|
runtime.connected = true;
|
|
onStatus();
|
|
logger.info('Connected to Home Assistant');
|
|
runtime.unsubscribeEntities = subscribeEntities(runtime.connection, onSnapshot);
|
|
runtime.connection.addEventListener('disconnected', () => {
|
|
logger.warn('Home Assistant connection lost');
|
|
teardownConnection();
|
|
scheduleReconnect();
|
|
});
|
|
} catch (err) {
|
|
logger.warn('Home Assistant connection failed', err.message);
|
|
teardownConnection();
|
|
scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
function isConnected() {
|
|
return Boolean(runtime.connection && runtime.connected);
|
|
}
|
|
|
|
async function callHomeAssistantService(domain, service, serviceData = {}) {
|
|
if (!enabled) throw new Error('Home Assistant not configured');
|
|
if (!runtime.connection) throw new Error('Home Assistant not connected');
|
|
if (!domain || !service) throw new Error('domain and service required');
|
|
logger.info('Home Assistant outbound service call', {
|
|
domain: String(domain),
|
|
service: String(service),
|
|
serviceData: serviceData && typeof serviceData === 'object' ? { ...serviceData } : serviceData,
|
|
caller: getCallerFrame(),
|
|
});
|
|
await callService(runtime.connection, String(domain), String(service), serviceData || {});
|
|
}
|
|
|
|
return {
|
|
connect,
|
|
isConnected,
|
|
callHomeAssistantService,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
createTransport,
|
|
};
|