Add Server-Sent Events (SSE) support for Home Assistant integration

- Implemented comprehensive SSE manager with advanced client management
- Added dynamic event subscription and broadcasting capabilities
- Created robust rate limiting and client connection tracking
- Enhanced Home Assistant event handling with new SSE endpoints
- Updated package.json with UUID dependency for client identification
- Expanded test coverage for SSE and WebSocket event handling
- Improved type definitions for Home Assistant events and entities
This commit is contained in:
jango-blockchained
2025-01-30 11:42:19 +01:00
parent 70c9287c68
commit 64e619252c
8 changed files with 872 additions and 196 deletions

View File

@@ -1,5 +1,57 @@
import { HassInstance } from '../../src/hass/index.js';
import { HassInstanceImpl } from '../../src/hass/index.js';
import * as HomeAssistant from '../../src/types/hass.js';
import { HassWebSocketClient } from '../../src/websocket/client.js';
// Add DOM types for WebSocket and events
type CloseEvent = {
code: number;
reason: string;
wasClean: boolean;
};
type MessageEvent = {
data: any;
type: string;
lastEventId: string;
};
type Event = {
type: string;
};
interface WebSocketLike {
send(data: string): void;
close(): void;
addEventListener(type: string, listener: (event: any) => void): void;
removeEventListener(type: string, listener: (event: any) => void): void;
dispatchEvent(event: Event): boolean;
onopen: ((event: Event) => void) | null;
onclose: ((event: CloseEvent) => void) | null;
onmessage: ((event: MessageEvent) => void) | null;
onerror: ((event: Event) => void) | null;
url: string;
readyState: number;
bufferedAmount: number;
extensions: string;
protocol: string;
binaryType: string;
}
interface MockWebSocketInstance extends WebSocketLike {
send: jest.Mock;
close: jest.Mock;
addEventListener: jest.Mock;
removeEventListener: jest.Mock;
dispatchEvent: jest.Mock;
}
interface MockWebSocketConstructor extends jest.Mock<MockWebSocketInstance> {
CONNECTING: 0;
OPEN: 1;
CLOSING: 2;
CLOSED: 3;
prototype: WebSocketLike;
}
// Mock the entire hass module
jest.mock('../../src/hass/index.js', () => ({
@@ -7,10 +59,40 @@ jest.mock('../../src/hass/index.js', () => ({
}));
describe('Home Assistant API', () => {
let hass: HassInstance;
let hass: HassInstanceImpl;
let mockWs: MockWebSocketInstance;
let MockWebSocket: MockWebSocketConstructor;
beforeEach(() => {
hass = new HassInstance('http://localhost:8123', 'test_token');
hass = new HassInstanceImpl('http://localhost:8123', 'test_token');
mockWs = {
send: jest.fn(),
close: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
onopen: null,
onclose: null,
onmessage: null,
onerror: null,
url: '',
readyState: 1,
bufferedAmount: 0,
extensions: '',
protocol: '',
binaryType: 'blob'
} as MockWebSocketInstance;
// Create a mock WebSocket constructor
MockWebSocket = jest.fn().mockImplementation(() => mockWs) as MockWebSocketConstructor;
MockWebSocket.CONNECTING = 0;
MockWebSocket.OPEN = 1;
MockWebSocket.CLOSING = 2;
MockWebSocket.CLOSED = 3;
MockWebSocket.prototype = {} as WebSocketLike;
// Mock WebSocket globally
(global as any).WebSocket = MockWebSocket;
});
describe('State Management', () => {
@@ -105,24 +187,16 @@ describe('Home Assistant API', () => {
describe('Event Subscription', () => {
it('should subscribe to events', async () => {
const callback = jest.fn();
const mockWs = {
send: jest.fn(),
close: jest.fn(),
addEventListener: jest.fn()
};
global.WebSocket = jest.fn().mockImplementation(() => mockWs);
await hass.subscribeEvents(callback, 'state_changed');
expect(WebSocket).toHaveBeenCalledWith(
expect(MockWebSocket).toHaveBeenCalledWith(
'ws://localhost:8123/api/websocket'
);
});
it('should handle subscription errors', async () => {
const callback = jest.fn();
global.WebSocket = jest.fn().mockImplementation(() => {
MockWebSocket.mockImplementation(() => {
throw new Error('WebSocket connection failed');
});
@@ -131,4 +205,23 @@ describe('Home Assistant API', () => {
).rejects.toThrow('WebSocket connection failed');
});
});
describe('WebSocket connection', () => {
it('should connect to WebSocket endpoint', async () => {
await hass.subscribeEvents(() => { });
expect(MockWebSocket).toHaveBeenCalledWith(
'ws://localhost:8123/api/websocket'
);
});
it('should handle connection errors', async () => {
MockWebSocket.mockImplementation(() => {
throw new Error('Connection failed');
});
await expect(hass.subscribeEvents(() => { })).rejects.toThrow(
'Connection failed'
);
});
});
});

View File

@@ -109,7 +109,11 @@ describe('WebSocket Event Handling', () => {
attributes: { brightness: 255 },
last_changed: '2024-01-01T00:00:00Z',
last_updated: '2024-01-01T00:00:00Z',
context: { id: '123' }
context: {
id: '123',
parent_id: null,
user_id: null
}
},
old_state: {
entity_id: 'light.living_room',
@@ -117,12 +121,20 @@ describe('WebSocket Event Handling', () => {
attributes: {},
last_changed: '2024-01-01T00:00:00Z',
last_updated: '2024-01-01T00:00:00Z',
context: { id: '122' }
context: {
id: '122',
parent_id: null,
user_id: null
}
}
},
origin: 'LOCAL',
time_fired: '2024-01-01T00:00:00Z',
context: { id: '123' }
context: {
id: '123',
parent_id: null,
user_id: null
}
};
client.on('event', (event) => {