@glasshome/sync-layer 0.4.0 → 0.5.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.
@@ -1,2067 +0,0 @@
1
- import { createDemoFixtures, state } from "../demo-data-CcG2efdQ.js";
2
-
3
- //#region src/testing/mock-behaviors/base.ts
4
- /**
5
- * Helper to create a simple state update result
6
- */
7
- function simpleStateUpdate(update) {
8
- return { stateUpdate: update };
9
- }
10
- /**
11
- * Helper to merge attributes safely
12
- */
13
- function mergeAttributes(current, updates) {
14
- return {
15
- ...current,
16
- ...updates
17
- };
18
- }
19
-
20
- //#endregion
21
- //#region src/testing/mock-behaviors/climate.ts
22
- var ClimateBehavior = class {
23
- domain = "climate";
24
- handleService(entity, service, serviceData, _context) {
25
- switch (service) {
26
- case "set_temperature": return this.handleSetTemperature(entity, serviceData);
27
- case "set_hvac_mode": return simpleStateUpdate({ state: serviceData?.hvac_mode ?? entity.state });
28
- case "set_preset_mode": return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, { preset_mode: serviceData?.preset_mode }) });
29
- case "set_fan_mode": return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, { fan_mode: serviceData?.fan_mode }) });
30
- case "set_humidity": return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, { humidity: serviceData?.humidity }) });
31
- case "set_swing_mode": return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, { swing_mode: serviceData?.swing_mode }) });
32
- case "turn_on": return simpleStateUpdate({ state: entity.attributes.last_hvac_mode || "heat" });
33
- case "turn_off": return simpleStateUpdate({ state: "off" });
34
- default: return { stateUpdate: null };
35
- }
36
- }
37
- handleSetTemperature(entity, serviceData) {
38
- const attributeUpdates = {};
39
- if (serviceData?.temperature !== void 0) attributeUpdates.temperature = serviceData.temperature;
40
- if (serviceData?.target_temp_high !== void 0) attributeUpdates.target_temp_high = serviceData.target_temp_high;
41
- if (serviceData?.target_temp_low !== void 0) attributeUpdates.target_temp_low = serviceData.target_temp_low;
42
- const stateUpdate = { attributes: mergeAttributes(entity.attributes, attributeUpdates) };
43
- if (serviceData?.hvac_mode !== void 0) stateUpdate.state = serviceData.hvac_mode;
44
- return simpleStateUpdate(stateUpdate);
45
- }
46
- };
47
-
48
- //#endregion
49
- //#region src/testing/mock-behaviors/cover.ts
50
- var CoverBehavior = class {
51
- domain = "cover";
52
- handleService(entity, service, serviceData, _context) {
53
- switch (service) {
54
- case "open_cover": return simpleStateUpdate({
55
- state: "open",
56
- attributes: mergeAttributes(entity.attributes, { current_position: 100 })
57
- });
58
- case "close_cover": return simpleStateUpdate({
59
- state: "closed",
60
- attributes: mergeAttributes(entity.attributes, { current_position: 0 })
61
- });
62
- case "stop_cover": return simpleStateUpdate({ state: entity.state === "opening" || entity.state === "closing" ? "open" : entity.state });
63
- case "set_cover_position": {
64
- const position = serviceData?.position ?? 50;
65
- return simpleStateUpdate({
66
- state: position === 0 ? "closed" : "open",
67
- attributes: mergeAttributes(entity.attributes, { current_position: position })
68
- });
69
- }
70
- case "open_cover_tilt": return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, { current_tilt_position: 100 }) });
71
- case "close_cover_tilt": return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, { current_tilt_position: 0 }) });
72
- case "set_cover_tilt_position": {
73
- const tiltPosition = serviceData?.tilt_position ?? 50;
74
- return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, { current_tilt_position: tiltPosition }) });
75
- }
76
- case "toggle": return simpleStateUpdate({
77
- state: entity.state === "closed" ? "open" : "closed",
78
- attributes: mergeAttributes(entity.attributes, { current_position: entity.state === "closed" ? 100 : 0 })
79
- });
80
- default: return { stateUpdate: null };
81
- }
82
- }
83
- };
84
-
85
- //#endregion
86
- //#region src/testing/mock-behaviors/light.ts
87
- var LightBehavior = class {
88
- domain = "light";
89
- handleService(entity, service, serviceData, _context) {
90
- switch (service) {
91
- case "turn_on": return this.handleTurnOn(entity, serviceData);
92
- case "turn_off": return this.handleTurnOff(entity, serviceData);
93
- case "toggle": return this.handleToggle(entity, serviceData);
94
- default: return { stateUpdate: null };
95
- }
96
- }
97
- handleTurnOn(entity, serviceData) {
98
- const attributeUpdates = {};
99
- if (serviceData?.brightness_pct !== void 0) {
100
- const brightnessPct = Math.max(0, Math.min(100, serviceData.brightness_pct));
101
- attributeUpdates.brightness = Math.round(brightnessPct / 100 * 255);
102
- }
103
- if (serviceData?.brightness_step_pct !== void 0) {
104
- const currentBrightness = entity.attributes.brightness ?? 255;
105
- const currentBrightnessPct = currentBrightness / 255 * 100;
106
- const newBrightnessPct = Math.max(0, Math.min(100, currentBrightnessPct + serviceData.brightness_step_pct));
107
- attributeUpdates.brightness = Math.round(newBrightnessPct / 100 * 255);
108
- }
109
- if (serviceData?.rgb_color !== void 0) {
110
- let rgbArray = [
111
- 255,
112
- 255,
113
- 255
114
- ];
115
- if (typeof serviceData.rgb_color === "string") {
116
- const parts = serviceData.rgb_color.split(",").map((s) => Number.parseInt(s.trim(), 10));
117
- if (parts.length === 3 && parts.every((n) => !Number.isNaN(n))) {
118
- const r = parts[0];
119
- const g = parts[1];
120
- const b = parts[2];
121
- if (r !== void 0 && g !== void 0 && b !== void 0) rgbArray = [
122
- r,
123
- g,
124
- b
125
- ];
126
- }
127
- } else {
128
- const rgb = serviceData.rgb_color;
129
- if (Array.isArray(rgb) && rgb.length === 3) {
130
- const r = rgb[0];
131
- const g = rgb[1];
132
- const b = rgb[2];
133
- if (typeof r === "number" && typeof g === "number" && typeof b === "number" && !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b)) rgbArray = [
134
- r,
135
- g,
136
- b
137
- ];
138
- }
139
- }
140
- attributeUpdates.rgb_color = rgbArray;
141
- attributeUpdates.color_temp = void 0;
142
- attributeUpdates.color_temp_kelvin = void 0;
143
- attributeUpdates.hs_color = void 0;
144
- attributeUpdates.xy_color = void 0;
145
- }
146
- if (serviceData?.color_temp_kelvin !== void 0) {
147
- const kelvin = typeof serviceData.color_temp_kelvin === "string" ? Number.parseInt(serviceData.color_temp_kelvin, 10) : serviceData.color_temp_kelvin;
148
- if (typeof kelvin === "number" && !Number.isNaN(kelvin)) {
149
- attributeUpdates.color_temp_kelvin = kelvin;
150
- attributeUpdates.rgb_color = void 0;
151
- attributeUpdates.hs_color = void 0;
152
- attributeUpdates.xy_color = void 0;
153
- }
154
- }
155
- if (serviceData?.transition !== void 0) attributeUpdates.transition = serviceData.transition;
156
- if (serviceData?.effect !== void 0) attributeUpdates.effect = serviceData.effect;
157
- return simpleStateUpdate({
158
- state: "on",
159
- attributes: mergeAttributes(entity.attributes, attributeUpdates)
160
- });
161
- }
162
- handleTurnOff(entity, serviceData) {
163
- const attributeUpdates = {};
164
- if (serviceData?.transition !== void 0) attributeUpdates.transition = serviceData.transition;
165
- return simpleStateUpdate({
166
- state: "off",
167
- attributes: mergeAttributes(entity.attributes, attributeUpdates)
168
- });
169
- }
170
- handleToggle(entity, serviceData) {
171
- const newState = entity.state === "on" ? "off" : "on";
172
- const attributeUpdates = {};
173
- if (newState === "on" && serviceData) {
174
- if (serviceData.brightness_pct !== void 0) {
175
- const brightnessPct = Math.max(0, Math.min(100, serviceData.brightness_pct));
176
- attributeUpdates.brightness = Math.round(brightnessPct / 100 * 255);
177
- } else if (entity.attributes.brightness) attributeUpdates.brightness = entity.attributes.brightness;
178
- if (serviceData.rgb_color !== void 0) {
179
- let rgbArray = [
180
- 255,
181
- 255,
182
- 255
183
- ];
184
- if (typeof serviceData.rgb_color === "string") {
185
- const parts = serviceData.rgb_color.split(",").map((s) => Number.parseInt(s.trim(), 10));
186
- if (parts.length === 3 && parts.every((n) => !Number.isNaN(n))) {
187
- const r = parts[0];
188
- const g = parts[1];
189
- const b = parts[2];
190
- if (r !== void 0 && g !== void 0 && b !== void 0) rgbArray = [
191
- r,
192
- g,
193
- b
194
- ];
195
- }
196
- } else {
197
- const rgb = serviceData.rgb_color;
198
- if (Array.isArray(rgb) && rgb.length === 3) {
199
- const r = rgb[0];
200
- const g = rgb[1];
201
- const b = rgb[2];
202
- if (typeof r === "number" && typeof g === "number" && typeof b === "number" && !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b)) rgbArray = [
203
- r,
204
- g,
205
- b
206
- ];
207
- }
208
- }
209
- attributeUpdates.rgb_color = rgbArray;
210
- attributeUpdates.color_temp_kelvin = void 0;
211
- attributeUpdates.hs_color = void 0;
212
- attributeUpdates.xy_color = void 0;
213
- }
214
- if (serviceData.color_temp_kelvin !== void 0) {
215
- const kelvin = typeof serviceData.color_temp_kelvin === "string" ? Number.parseInt(serviceData.color_temp_kelvin, 10) : serviceData.color_temp_kelvin;
216
- if (typeof kelvin === "number" && !Number.isNaN(kelvin)) {
217
- attributeUpdates.color_temp_kelvin = kelvin;
218
- attributeUpdates.rgb_color = void 0;
219
- attributeUpdates.hs_color = void 0;
220
- attributeUpdates.xy_color = void 0;
221
- }
222
- }
223
- if (serviceData.effect !== void 0) attributeUpdates.effect = serviceData.effect;
224
- if (serviceData.transition !== void 0) attributeUpdates.transition = serviceData.transition;
225
- }
226
- return simpleStateUpdate({
227
- state: newState,
228
- attributes: Object.keys(attributeUpdates).length > 0 ? mergeAttributes(entity.attributes, attributeUpdates) : entity.attributes
229
- });
230
- }
231
- };
232
-
233
- //#endregion
234
- //#region src/testing/mock-behaviors/lock.ts
235
- var LockBehavior = class {
236
- domain = "lock";
237
- handleService(entity, service, _serviceData, _context) {
238
- if (entity.state === "unavailable" || entity.state === "unknown") return { stateUpdate: null };
239
- switch (service) {
240
- case "lock": return this.handleLock(entity);
241
- case "unlock": return this.handleUnlock(entity);
242
- case "open": return this.handleOpen(entity);
243
- default: return { stateUpdate: null };
244
- }
245
- }
246
- handleLock(entity) {
247
- if (entity.state === "locked") return { stateUpdate: null };
248
- if (entity.state === "jammed") return { stateUpdate: null };
249
- return {
250
- stateUpdate: { state: "locking" },
251
- additionalActions: [{
252
- delay: 500,
253
- update: { state: "locked" }
254
- }]
255
- };
256
- }
257
- handleUnlock(entity) {
258
- if (entity.state === "unlocked") return { stateUpdate: null };
259
- if (entity.state === "jammed") return { stateUpdate: null };
260
- return {
261
- stateUpdate: { state: "unlocking" },
262
- additionalActions: [{
263
- delay: 500,
264
- update: { state: "unlocked" }
265
- }]
266
- };
267
- }
268
- handleOpen(entity) {
269
- if (entity.state === "unlocked") return { stateUpdate: null };
270
- if (entity.state === "jammed") return { stateUpdate: null };
271
- return {
272
- stateUpdate: { state: "opening" },
273
- additionalActions: [{
274
- delay: 500,
275
- update: { state: "unlocked" }
276
- }]
277
- };
278
- }
279
- };
280
-
281
- //#endregion
282
- //#region src/testing/mock-behaviors/media_player.ts
283
- /**
284
- * Example album covers for mock media players
285
- * Using placeholder image services that provide nice album art
286
- */
287
- const EXAMPLE_ALBUM_COVERS = [
288
- "https://picsum.photos/seed/album8/300/300",
289
- "https://picsum.photos/seed/album2/300/300",
290
- "https://picsum.photos/seed/album3/300/300",
291
- "https://picsum.photos/seed/album4/300/300",
292
- "https://picsum.photos/seed/album5/300/300"
293
- ];
294
- /**
295
- * Example tracks for mock media players
296
- */
297
- const EXAMPLE_TRACKS = [
298
- {
299
- title: "Midnight Horizons",
300
- artist: "Synthetic Dreams",
301
- album: "Neon Nights",
302
- duration: 245
303
- },
304
- {
305
- title: "Electric Pulse",
306
- artist: "Digital Waves",
307
- album: "Synth Collection",
308
- duration: 198
309
- },
310
- {
311
- title: "Cosmic Journey",
312
- artist: "Space Echo",
313
- album: "Galaxy Sounds",
314
- duration: 312
315
- },
316
- {
317
- title: "Urban Nights",
318
- artist: "City Lights",
319
- album: "Metropolitan",
320
- duration: 267
321
- },
322
- {
323
- title: "Ocean Breeze",
324
- artist: "Coastal Vibes",
325
- album: "Beach Sessions",
326
- duration: 189
327
- }
328
- ];
329
- let currentTrackIndex = 0;
330
- function getNextTrack() {
331
- const track = EXAMPLE_TRACKS[currentTrackIndex % EXAMPLE_TRACKS.length];
332
- currentTrackIndex++;
333
- return track;
334
- }
335
- function getCurrentTrack() {
336
- return EXAMPLE_TRACKS[currentTrackIndex % EXAMPLE_TRACKS.length];
337
- }
338
- function getAlbumCover(trackIndex) {
339
- return EXAMPLE_ALBUM_COVERS[trackIndex % EXAMPLE_ALBUM_COVERS.length];
340
- }
341
- var MediaPlayerBehavior = class {
342
- domain = "media_player";
343
- handleService(entity, service, serviceData, _context) {
344
- switch (service) {
345
- case "media_play": return this.handlePlay(entity);
346
- case "media_pause": return this.handlePause(entity);
347
- case "media_play_pause": return this.handlePlayPause(entity);
348
- case "media_stop": return this.handleStop(entity);
349
- case "media_next_track": return this.handleNextTrack(entity);
350
- case "media_previous_track": return this.handlePreviousTrack(entity);
351
- case "media_seek": return this.handleSeek(entity, serviceData);
352
- case "volume_up": return this.handleVolumeUp(entity);
353
- case "volume_down": return this.handleVolumeDown(entity);
354
- case "set_volume_level": return this.handleSetVolumeLevel(entity, serviceData);
355
- case "mute_volume": return this.handleMuteVolume(entity, serviceData);
356
- case "turn_on": return this.handleTurnOn(entity);
357
- case "turn_off": return this.handleTurnOff(entity);
358
- case "toggle": return this.handleToggle(entity);
359
- default: return { stateUpdate: null };
360
- }
361
- }
362
- handlePlay(entity) {
363
- const attributeUpdates = {};
364
- if (!entity.attributes.media_title) {
365
- const track = getCurrentTrack();
366
- attributeUpdates.media_title = track.title;
367
- attributeUpdates.media_artist = track.artist;
368
- attributeUpdates.media_album_name = track.album;
369
- attributeUpdates.media_duration = track.duration;
370
- attributeUpdates.media_position = 0;
371
- attributeUpdates.entity_picture = getAlbumCover(currentTrackIndex - 1);
372
- }
373
- if (entity.state === "paused" && entity.attributes.media_position) attributeUpdates.media_position = entity.attributes.media_position;
374
- return simpleStateUpdate({
375
- state: "playing",
376
- attributes: mergeAttributes(entity.attributes, attributeUpdates)
377
- });
378
- }
379
- handlePause(entity) {
380
- return simpleStateUpdate({
381
- state: "paused",
382
- attributes: entity.attributes
383
- });
384
- }
385
- handlePlayPause(entity) {
386
- if (entity.state === "playing") return this.handlePause(entity);
387
- return this.handlePlay(entity);
388
- }
389
- handleStop(entity) {
390
- return simpleStateUpdate({
391
- state: "idle",
392
- attributes: mergeAttributes(entity.attributes, { media_position: 0 })
393
- });
394
- }
395
- handleNextTrack(entity) {
396
- const track = getNextTrack();
397
- const trackIndex = currentTrackIndex - 1;
398
- return simpleStateUpdate({
399
- state: entity.state === "playing" ? "playing" : entity.state,
400
- attributes: mergeAttributes(entity.attributes, {
401
- media_title: track.title,
402
- media_artist: track.artist,
403
- media_album_name: track.album,
404
- media_duration: track.duration,
405
- media_position: 0,
406
- entity_picture: getAlbumCover(trackIndex)
407
- })
408
- });
409
- }
410
- handlePreviousTrack(entity) {
411
- currentTrackIndex = Math.max(0, currentTrackIndex - 2);
412
- const track = getNextTrack();
413
- const trackIndex = currentTrackIndex - 1;
414
- return simpleStateUpdate({
415
- state: entity.state === "playing" ? "playing" : entity.state,
416
- attributes: mergeAttributes(entity.attributes, {
417
- media_title: track.title,
418
- media_artist: track.artist,
419
- media_album_name: track.album,
420
- media_duration: track.duration,
421
- media_position: 0,
422
- entity_picture: getAlbumCover(trackIndex)
423
- })
424
- });
425
- }
426
- handleSeek(entity, serviceData) {
427
- const seekPosition = serviceData?.seek_position;
428
- if (seekPosition === void 0) return { stateUpdate: null };
429
- const duration = entity.attributes.media_duration ?? 0;
430
- const newPosition = Math.max(0, Math.min(duration, seekPosition));
431
- return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, { media_position: newPosition }) });
432
- }
433
- handleVolumeUp(entity) {
434
- const currentVolume = entity.attributes.volume_level ?? 0;
435
- const newVolume = Math.min(1, currentVolume + .1);
436
- return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, {
437
- volume_level: newVolume,
438
- is_volume_muted: false
439
- }) });
440
- }
441
- handleVolumeDown(entity) {
442
- const currentVolume = entity.attributes.volume_level ?? 0;
443
- const newVolume = Math.max(0, currentVolume - .1);
444
- return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, {
445
- volume_level: newVolume,
446
- is_volume_muted: newVolume === 0
447
- }) });
448
- }
449
- handleSetVolumeLevel(entity, serviceData) {
450
- const volumeLevel = serviceData?.volume_level;
451
- if (volumeLevel === void 0) return { stateUpdate: null };
452
- const newVolume = Math.max(0, Math.min(1, volumeLevel));
453
- return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, {
454
- volume_level: newVolume,
455
- is_volume_muted: newVolume === 0
456
- }) });
457
- }
458
- handleMuteVolume(entity, serviceData) {
459
- const isMuted = serviceData?.is_volume_muted ?? !entity.attributes.is_volume_muted;
460
- return simpleStateUpdate({ attributes: mergeAttributes(entity.attributes, { is_volume_muted: isMuted }) });
461
- }
462
- handleTurnOn(entity) {
463
- const attributeUpdates = {};
464
- if (!entity.attributes.media_title) {
465
- const track = getCurrentTrack();
466
- attributeUpdates.media_title = track.title;
467
- attributeUpdates.media_artist = track.artist;
468
- attributeUpdates.media_album_name = track.album;
469
- attributeUpdates.media_duration = track.duration;
470
- attributeUpdates.media_position = 0;
471
- attributeUpdates.entity_picture = getAlbumCover(currentTrackIndex - 1);
472
- }
473
- return simpleStateUpdate({
474
- state: "playing",
475
- attributes: mergeAttributes(entity.attributes, attributeUpdates)
476
- });
477
- }
478
- handleTurnOff(entity) {
479
- return simpleStateUpdate({
480
- state: "off",
481
- attributes: mergeAttributes(entity.attributes, { media_position: 0 })
482
- });
483
- }
484
- handleToggle(entity) {
485
- if (entity.state === "off") return this.handleTurnOn(entity);
486
- return this.handleTurnOff(entity);
487
- }
488
- };
489
-
490
- //#endregion
491
- //#region src/testing/mock-behaviors/switch.ts
492
- var SwitchBehavior = class {
493
- domain = "switch";
494
- handleService(entity, service, _serviceData, _context) {
495
- switch (service) {
496
- case "turn_on": return simpleStateUpdate({ state: "on" });
497
- case "turn_off": return simpleStateUpdate({ state: "off" });
498
- case "toggle": return simpleStateUpdate({ state: entity.state === "on" ? "off" : "on" });
499
- default: return { stateUpdate: null };
500
- }
501
- }
502
- };
503
-
504
- //#endregion
505
- //#region src/testing/mock-behaviors/registry.ts
506
- /**
507
- * Central registry of all domain behavior handlers
508
- */
509
- var BehaviorRegistry = class {
510
- handlers = /* @__PURE__ */ new Map();
511
- constructor() {
512
- this.registerDefaultHandlers();
513
- }
514
- /**
515
- * Register all built-in domain handlers
516
- */
517
- registerDefaultHandlers() {
518
- this.register(new LightBehavior());
519
- this.register(new SwitchBehavior());
520
- this.register(new CoverBehavior());
521
- this.register(new ClimateBehavior());
522
- this.register(new MediaPlayerBehavior());
523
- this.register(new LockBehavior());
524
- }
525
- /**
526
- * Register a domain behavior handler
527
- */
528
- register(handler) {
529
- this.handlers.set(handler.domain, handler);
530
- }
531
- /**
532
- * Get handler for a specific domain
533
- */
534
- getHandler(domain) {
535
- return this.handlers.get(domain);
536
- }
537
- /**
538
- * Check if a domain has a registered handler
539
- */
540
- hasHandler(domain) {
541
- return this.handlers.has(domain);
542
- }
543
- /**
544
- * Get all registered domains
545
- */
546
- getRegisteredDomains() {
547
- return Array.from(this.handlers.keys());
548
- }
549
- /**
550
- * Clear all handlers (useful for testing)
551
- */
552
- clear() {
553
- this.handlers.clear();
554
- }
555
- /**
556
- * Reset to default handlers
557
- */
558
- reset() {
559
- this.clear();
560
- this.registerDefaultHandlers();
561
- }
562
- };
563
- /**
564
- * Global behavior registry instance
565
- */
566
- const behaviorRegistry = new BehaviorRegistry();
567
- /**
568
- * Get behavior handler for a domain
569
- */
570
- function getBehaviorHandler(domain) {
571
- return behaviorRegistry.getHandler(domain);
572
- }
573
-
574
- //#endregion
575
- //#region src/testing/mock.ts
576
- const ERROR_CODES = {
577
- UNKNOWN_ERROR: "unknown_error",
578
- INVALID_FORMAT: "invalid_format",
579
- NOT_FOUND: "not_found",
580
- UNAUTHORIZED: "unauthorized",
581
- HOME_ASSISTANT_ERROR: "home_assistant_error"
582
- };
583
- /**
584
- * Mock WebSocket connection for testing
585
- *
586
- * Simulates a Home Assistant WebSocket connection without requiring
587
- * a real backend.
588
- *
589
- * MockConnection does NOT manipulate the store directly.
590
- * SyncLayer loads fixtures via `getFixtures()` and manages state itself.
591
- */
592
- var MockConnection = class {
593
- connected = false;
594
- authState = "pending";
595
- messageHandlers = [];
596
- eventSubscriptions = /* @__PURE__ */ new Map();
597
- subscriptions = /* @__PURE__ */ new Map();
598
- latency;
599
- fixtures;
600
- mockVersion = "2024.11.0";
601
- messageIdCounter = 1;
602
- autoReact;
603
- reactionDelay;
604
- configOverride;
605
- serviceCalls = [];
606
- sentCommands = [];
607
- constructor(fixtures, latency = 0, options) {
608
- this.fixtures = fixtures;
609
- this.latency = latency;
610
- this.autoReact = options?.autoReact ?? true;
611
- this.reactionDelay = options?.reactionDelay ?? 0;
612
- this.configOverride = options?.config ?? {};
613
- }
614
- /** Replace the core config and fire core_config_updated (simulates HA Region change). */
615
- setConfig(config) {
616
- this.configOverride = {
617
- ...this.configOverride,
618
- ...config
619
- };
620
- this.emitEvent({
621
- event_type: "core_config_updated",
622
- data: {},
623
- origin: "LOCAL",
624
- time_fired: (/* @__PURE__ */ new Date(0)).toISOString(),
625
- context: {
626
- id: "mock",
627
- parent_id: null,
628
- user_id: null
629
- }
630
- });
631
- }
632
- /**
633
- * Get connected state
634
- */
635
- get connected$() {
636
- return this.connected;
637
- }
638
- /**
639
- * Simulate connection - sends auth_required per WebSocket spec
640
- */
641
- async connect() {
642
- await this.delay(this.latency);
643
- this.connected = true;
644
- this.authState = "pending";
645
- this.sendAuthRequired();
646
- }
647
- /**
648
- * Register message handler for auth and other server messages
649
- */
650
- addEventListener(handler) {
651
- this.messageHandlers.push(handler);
652
- return () => {
653
- const index = this.messageHandlers.indexOf(handler);
654
- if (index > -1) this.messageHandlers.splice(index, 1);
655
- };
656
- }
657
- /**
658
- * Handle authentication message (from client)
659
- */
660
- async authenticate(_accessToken) {
661
- if (!this.connected) throw new Error("Not connected");
662
- if (this.authState !== "pending") throw new Error("Authentication already completed");
663
- await this.delay(this.latency);
664
- const isValid = true;
665
- if (isValid) {
666
- this.authState = "authenticated";
667
- const message = {
668
- type: "auth_ok",
669
- ha_version: this.mockVersion
670
- };
671
- for (const handler of this.messageHandlers) handler(message);
672
- } else {
673
- this.authState = "failed";
674
- const message = {
675
- type: "auth_invalid",
676
- message: "Invalid access token"
677
- };
678
- for (const handler of this.messageHandlers) handler(message);
679
- this.disconnect();
680
- }
681
- }
682
- /**
683
- * Send auth_required message (server-initiated)
684
- */
685
- sendAuthRequired() {
686
- const message = {
687
- type: "auth_required",
688
- ha_version: this.mockVersion
689
- };
690
- for (const handler of this.messageHandlers) handler(message);
691
- }
692
- /**
693
- * Simulate disconnection
694
- */
695
- disconnect() {
696
- this.connected = false;
697
- this.authState = "pending";
698
- this.messageHandlers = [];
699
- this.eventSubscriptions.clear();
700
- this.subscriptions.clear();
701
- }
702
- /**
703
- * Close connection (alias for disconnect for compatibility)
704
- */
705
- close() {
706
- this.disconnect();
707
- }
708
- /**
709
- * Send a message (mock implementation)
710
- */
711
- async sendMessage(message) {
712
- if (!this.connected) throw new Error("Not connected");
713
- await this.delay(this.latency);
714
- if (message.type === "get_states") this.sendResponse(message.id, Object.values(this.fixtures.entities));
715
- else if (message.type === "call_service") this.sendResponse(message.id, { success: true });
716
- }
717
- /**
718
- * Call a service (mock implementation)
719
- */
720
- async callService(domain, service, serviceData, target) {
721
- if (!this.connected) throw new Error("Not connected");
722
- if (this.authState !== "authenticated") throw new Error("Not authenticated");
723
- this.serviceCalls.push({
724
- domain,
725
- service,
726
- serviceData,
727
- target,
728
- timestamp: Date.now()
729
- });
730
- return await this.sendMessagePromise({
731
- type: "call_service",
732
- domain,
733
- service,
734
- service_data: serviceData || {},
735
- target: target || {}
736
- });
737
- }
738
- /**
739
- * Send a message and wait for response - returns proper WebSocket result format
740
- */
741
- async sendMessagePromise(message) {
742
- if (!this.connected) throw new Error("Not connected");
743
- if (this.authState !== "authenticated") throw new Error("Not authenticated");
744
- if (!message.type || typeof message.type !== "string") throw new Error("Invalid message: type must be a string");
745
- if (!message.id || typeof message.id !== "number" || message.id < 0) message.id = this.messageIdCounter++;
746
- this.sentCommands.push({
747
- type: message.type,
748
- data: message,
749
- timestamp: Date.now()
750
- });
751
- await this.delay(this.latency);
752
- try {
753
- const result = this.handleMessage(message);
754
- const response = {
755
- id: message.id,
756
- type: "result",
757
- success: true,
758
- result
759
- };
760
- for (const handler of this.messageHandlers) handler(response);
761
- return result;
762
- } catch (error) {
763
- const code = error.code || ERROR_CODES.UNKNOWN_ERROR;
764
- const errorMessage = error instanceof Error ? error.message : String(error);
765
- const errorResponse = {
766
- id: message.id,
767
- type: "result",
768
- success: false,
769
- error: {
770
- code,
771
- message: errorMessage
772
- }
773
- };
774
- for (const handler of this.messageHandlers) handler(errorResponse);
775
- throw error;
776
- }
777
- }
778
- /**
779
- * Handle different message types and return appropriate data
780
- */
781
- handleMessage(message) {
782
- switch (message.type) {
783
- case "get_states": return Object.values(this.fixtures.entities);
784
- case "config/entity_registry/list": return this.fixtures.entityRegistry;
785
- case "config/entity_registry/get": {
786
- const entityId = message.entity_id;
787
- const entry = this.fixtures.entityRegistry.find((e) => e.entity_id === entityId);
788
- if (!entry) {
789
- const error = new Error("Entity registry entry not found");
790
- error.code = ERROR_CODES.NOT_FOUND;
791
- throw error;
792
- }
793
- return entry;
794
- }
795
- case "config/device_registry/list": return this.fixtures.deviceRegistry;
796
- case "config/area_registry/list": return this.fixtures.areaRegistry;
797
- case "config/floor_registry/list": return this.fixtures.floorRegistry;
798
- case "config/label_registry/list": return this.fixtures.labelRegistry;
799
- case "config/entity_registry/update": return this.handleEntityUpdate(message);
800
- case "call_service": return this.handleServiceCall(message);
801
- case "subscribe_events": return this.handleSubscribeEvents(message);
802
- case "unsubscribe_events": return this.handleUnsubscribeEvents(message);
803
- case "ping": return {};
804
- case "supported_features": return {};
805
- case "get_config": return {
806
- latitude: 0,
807
- longitude: 0,
808
- elevation: 0,
809
- unit_system: {
810
- length: "km",
811
- mass: "kg",
812
- temperature: "°C",
813
- volume: "L"
814
- },
815
- location_name: "Mock Home",
816
- time_zone: "UTC",
817
- components: [],
818
- config_dir: "/config",
819
- whitelist_external_dirs: [],
820
- allowlist_external_dirs: [],
821
- allowlist_external_urls: [],
822
- version: this.mockVersion,
823
- config_source: "default_config",
824
- safe_mode: false,
825
- state: "RUNNING",
826
- external_url: null,
827
- internal_url: null,
828
- currency: "USD",
829
- country: null,
830
- language: "en",
831
- ...this.configOverride
832
- };
833
- case "get_services": return {};
834
- case "fire_event": {
835
- this.emitEvent({
836
- event_type: message.event_type,
837
- data: message.event_data || {},
838
- origin: "LOCAL",
839
- time_fired: (/* @__PURE__ */ new Date()).toISOString(),
840
- context: {
841
- id: this.generateId(),
842
- parent_id: null,
843
- user_id: null
844
- }
845
- });
846
- return {};
847
- }
848
- case "history/history_during_period": {
849
- const entityIds = message.entity_ids || [];
850
- const result = {};
851
- if (entityIds.length === 0) return {};
852
- const startTime = message.start_time ? new Date(message.start_time).getTime() / 1e3 : void 0;
853
- const endTime = message.end_time ? new Date(message.end_time).getTime() / 1e3 : void 0;
854
- for (const entityId of entityIds) if (this.fixtures.history && this.fixtures.history[entityId]) {
855
- let historyData = this.fixtures.history[entityId];
856
- if (startTime !== void 0 || endTime !== void 0) historyData = historyData.filter((state$1) => {
857
- if (startTime !== void 0 && state$1.lu < startTime) return false;
858
- if (endTime !== void 0 && state$1.lu > endTime) return false;
859
- return true;
860
- });
861
- if (historyData.length > 0) result[entityId] = historyData;
862
- } else {
863
- const entity = this.fixtures.entities[entityId];
864
- if (entity) result[entityId] = [{
865
- s: entity.state,
866
- lu: Math.floor(Date.now() / 1e3),
867
- lc: Math.floor(Date.now() / 1e3),
868
- a: entity.attributes || {}
869
- }];
870
- }
871
- return result;
872
- }
873
- case "camera/stream": {
874
- const entityId = message.entity_id;
875
- const entity = this.fixtures.entities[entityId];
876
- if (!entity) {
877
- const error = new Error(`Camera entity not found: ${entityId}`);
878
- error.code = ERROR_CODES.NOT_FOUND;
879
- throw error;
880
- }
881
- return { url: `http://homeassistant.local:8123/api/camera_proxy_stream/${entityId}` };
882
- }
883
- default: {
884
- const error = new Error(`Unhandled message type: ${message.type}`);
885
- error.code = ERROR_CODES.INVALID_FORMAT;
886
- throw error;
887
- }
888
- }
889
- }
890
- /**
891
- * Subscribe to events
892
- */
893
- subscribeEvents(callback, eventType) {
894
- if (!this.connected) return Promise.reject(new Error("Not connected"));
895
- const handlers = this.eventSubscriptions.get(eventType || "*") || [];
896
- handlers.push(callback);
897
- this.eventSubscriptions.set(eventType || "*", handlers);
898
- return Promise.resolve(() => {
899
- const handlers$1 = this.eventSubscriptions.get(eventType || "*") || [];
900
- const index = handlers$1.indexOf(callback);
901
- if (index > -1) handlers$1.splice(index, 1);
902
- });
903
- }
904
- /**
905
- * Subscribe to WebSocket messages
906
- *
907
- * Used for subscribing to command responses like history/stream
908
- *
909
- * @param callback - Callback function for messages
910
- * @param message - Message to subscribe to
911
- * @returns Promise resolving to unsubscribe function
912
- */
913
- async subscribeMessage(callback, message) {
914
- if (!this.connected) throw new Error("Not connected");
915
- if (this.authState !== "authenticated") throw new Error("Not authenticated");
916
- const subscriptionId = this.messageIdCounter++;
917
- this.subscriptions.set(subscriptionId, {});
918
- if (message.type === "subscribe_entities") {
919
- const additions = {};
920
- for (const [entityId, entity] of Object.entries(this.fixtures.entities)) additions[entityId] = {
921
- s: entity.state,
922
- a: entity.attributes || {},
923
- c: entity.context || {
924
- id: "",
925
- parent_id: null,
926
- user_id: null
927
- },
928
- lc: Math.floor(new Date(entity.last_changed || Date.now()).getTime() / 1e3),
929
- lu: Math.floor(new Date(entity.last_updated || Date.now()).getTime() / 1e3)
930
- };
931
- if (Object.keys(additions).length > 0) callback({ a: additions });
932
- this.subscriptions.set(subscriptionId, {
933
- callback,
934
- isEntitySubscription: true
935
- });
936
- return async () => {
937
- this.subscriptions.delete(subscriptionId);
938
- };
939
- }
940
- if (message.type === "weather/subscribe_forecast") {
941
- const entityId = message.entity_id;
942
- const forecastType = message.forecast_type || "hourly";
943
- if (!entityId) throw new Error("Entity ID required for forecast subscription");
944
- if (!this.fixtures.entities[entityId]) throw new Error(`Weather entity not found: ${entityId}`);
945
- const forecast = this.fixtures.forecasts?.[entityId]?.[forecastType] || [];
946
- setImmediate(() => {
947
- const forecastEvent = {
948
- type: forecastType,
949
- forecast: forecast.length > 0 ? forecast : null
950
- };
951
- callback(forecastEvent);
952
- });
953
- return async () => {
954
- this.subscriptions.delete(subscriptionId);
955
- };
956
- }
957
- if (message.type === "history/stream") {
958
- const entityIds = message.entity_ids || [];
959
- const startTime = message.start_time;
960
- const endTime = message.end_time;
961
- const startTimeSeconds = startTime ? Math.floor(new Date(startTime).getTime() / 1e3) : void 0;
962
- const endTimeSeconds = endTime ? Math.floor(new Date(endTime).getTime() / 1e3) : void 0;
963
- const initialHistory = {};
964
- for (const entityId of entityIds) if (this.fixtures.history && this.fixtures.history[entityId]) {
965
- let historyData = this.fixtures.history[entityId];
966
- if (startTimeSeconds !== void 0 || endTimeSeconds !== void 0) historyData = historyData.filter((state$1) => {
967
- if (startTimeSeconds !== void 0 && state$1.lu < startTimeSeconds) return false;
968
- if (endTimeSeconds !== void 0 && state$1.lu > endTimeSeconds) return false;
969
- return true;
970
- });
971
- if (historyData.length > 0) initialHistory[entityId] = historyData;
972
- } else {
973
- const entity = this.fixtures.entities[entityId];
974
- if (entity) initialHistory[entityId] = [{
975
- s: entity.state,
976
- lu: Math.floor(Date.now() / 1e3),
977
- lc: Math.floor(Date.now() / 1e3),
978
- a: entity.attributes || {}
979
- }];
980
- }
981
- await this.delay(this.latency);
982
- const initialMessage = {
983
- states: initialHistory,
984
- start_time: startTimeSeconds,
985
- end_time: endTimeSeconds
986
- };
987
- callback(initialMessage);
988
- await this.delay(this.latency);
989
- const emptyMessage = { states: {} };
990
- callback(emptyMessage);
991
- } else {
992
- await this.delay(this.latency);
993
- const result = this.handleMessage(message);
994
- callback(result);
995
- }
996
- return async () => {
997
- this.subscriptions.delete(subscriptionId);
998
- };
999
- }
1000
- /**
1001
- * Simulate state change
1002
- */
1003
- setState(entityId, updates) {
1004
- const current = this.fixtures.entities[entityId];
1005
- if (!current) throw new Error(`Entity not found: ${entityId}`);
1006
- const newState = {
1007
- ...current,
1008
- ...updates,
1009
- last_changed: (/* @__PURE__ */ new Date()).toISOString(),
1010
- last_updated: (/* @__PURE__ */ new Date()).toISOString()
1011
- };
1012
- this.fixtures.entities[entityId] = newState;
1013
- for (const [, subscription] of this.subscriptions.entries()) if (subscription.callback && subscription.isEntitySubscription) {
1014
- const changeMsg = { c: { [entityId]: { "+": {
1015
- s: newState.state,
1016
- a: newState.attributes || {},
1017
- lc: Math.floor(new Date(newState.last_changed || Date.now()).getTime() / 1e3),
1018
- lu: Math.floor(new Date(newState.last_updated || Date.now()).getTime() / 1e3)
1019
- } } } };
1020
- subscription.callback(changeMsg);
1021
- }
1022
- }
1023
- /**
1024
- * Simulate entity registry update
1025
- */
1026
- updateEntityRegistry(entityId, updates) {
1027
- const index = this.fixtures.entityRegistry.findIndex((e) => e.entity_id === entityId);
1028
- if (index === -1) throw new Error(`Entity registry entry not found: ${entityId}`);
1029
- const updated = {
1030
- ...this.fixtures.entityRegistry[index],
1031
- ...updates
1032
- };
1033
- this.fixtures.entityRegistry[index] = updated;
1034
- this.emitEvent({
1035
- event_type: "entity_registry_updated",
1036
- data: {
1037
- action: "update",
1038
- entity_id: entityId,
1039
- changes: updates
1040
- },
1041
- origin: "LOCAL",
1042
- time_fired: (/* @__PURE__ */ new Date()).toISOString(),
1043
- context: {
1044
- id: this.generateId(),
1045
- parent_id: null,
1046
- user_id: null
1047
- }
1048
- });
1049
- }
1050
- /**
1051
- * Simulate area registry update
1052
- */
1053
- updateAreaRegistry(areaId, updates) {
1054
- const index = this.fixtures.areaRegistry.findIndex((a) => a.id === areaId);
1055
- if (index === -1) throw new Error(`Area registry entry not found: ${areaId}`);
1056
- const updated = {
1057
- ...this.fixtures.areaRegistry[index],
1058
- ...updates
1059
- };
1060
- this.fixtures.areaRegistry[index] = updated;
1061
- this.emitEvent({
1062
- event_type: "area_registry_updated",
1063
- data: {
1064
- action: "update",
1065
- area_id: areaId,
1066
- changes: updates
1067
- },
1068
- origin: "LOCAL",
1069
- time_fired: (/* @__PURE__ */ new Date()).toISOString(),
1070
- context: {
1071
- id: this.generateId(),
1072
- parent_id: null,
1073
- user_id: null
1074
- }
1075
- });
1076
- }
1077
- /**
1078
- * Simulate device registry update
1079
- */
1080
- updateDeviceRegistry(deviceId, updates) {
1081
- const index = this.fixtures.deviceRegistry.findIndex((d) => d.id === deviceId);
1082
- if (index === -1) throw new Error(`Device registry entry not found: ${deviceId}`);
1083
- const updated = {
1084
- ...this.fixtures.deviceRegistry[index],
1085
- ...updates
1086
- };
1087
- this.fixtures.deviceRegistry[index] = updated;
1088
- this.emitEvent({
1089
- event_type: "device_registry_updated",
1090
- data: {
1091
- action: "update",
1092
- device_id: deviceId,
1093
- changes: updates
1094
- },
1095
- origin: "LOCAL",
1096
- time_fired: (/* @__PURE__ */ new Date()).toISOString(),
1097
- context: {
1098
- id: this.generateId(),
1099
- parent_id: null,
1100
- user_id: null
1101
- }
1102
- });
1103
- }
1104
- /**
1105
- * Emit custom event - sends proper WebSocket event messages to subscriptions
1106
- */
1107
- emitEvent(event) {
1108
- for (const [subscriptionId, subscription] of this.subscriptions.entries()) if (!subscription.eventType || subscription.eventType === event.event_type) {
1109
- const eventMessage = {
1110
- id: subscriptionId,
1111
- type: "event",
1112
- event
1113
- };
1114
- for (const handler of this.messageHandlers) handler(eventMessage);
1115
- }
1116
- const specificHandlers = this.eventSubscriptions.get(event.event_type) || [];
1117
- for (const handler of specificHandlers) handler(event);
1118
- const wildcardHandlers = this.eventSubscriptions.get("*") || [];
1119
- for (const handler of wildcardHandlers) handler(event);
1120
- }
1121
- /**
1122
- * Handle subscribe_events command
1123
- */
1124
- handleSubscribeEvents(message) {
1125
- this.subscriptions.set(message.id, { eventType: message.event_type });
1126
- return {};
1127
- }
1128
- /**
1129
- * Handle unsubscribe_events command
1130
- */
1131
- handleUnsubscribeEvents(message) {
1132
- const { subscription } = message;
1133
- if (!this.subscriptions.has(subscription)) {
1134
- const error = new Error(`Subscription ${subscription} not found`);
1135
- error.code = ERROR_CODES.NOT_FOUND;
1136
- throw error;
1137
- }
1138
- this.subscriptions.delete(subscription);
1139
- return {};
1140
- }
1141
- /**
1142
- * Get current fixtures
1143
- *
1144
- * SyncLayer calls this to load fixtures into its store.
1145
- */
1146
- getFixtures() {
1147
- return { ...this.fixtures };
1148
- }
1149
- /**
1150
- * Load new fixtures
1151
- */
1152
- loadFixtures(fixtures) {
1153
- this.fixtures = {
1154
- ...this.fixtures,
1155
- ...fixtures
1156
- };
1157
- }
1158
- /**
1159
- * Clear tracking data
1160
- */
1161
- clearTracking() {
1162
- this.serviceCalls = [];
1163
- this.sentCommands = [];
1164
- }
1165
- /**
1166
- * Get last service call
1167
- */
1168
- getLastServiceCall() {
1169
- return this.serviceCalls[this.serviceCalls.length - 1];
1170
- }
1171
- /**
1172
- * Get last command
1173
- */
1174
- getLastCommand() {
1175
- return this.sentCommands[this.sentCommands.length - 1];
1176
- }
1177
- async delay(ms) {
1178
- if (ms > 0) return new Promise((resolve) => setTimeout(resolve, ms));
1179
- }
1180
- sendResponse(id, result) {
1181
- for (const handler of this.messageHandlers) handler({
1182
- id,
1183
- type: "result",
1184
- success: true,
1185
- result
1186
- });
1187
- }
1188
- handleEntityUpdate(message) {
1189
- const entityId = message.entity_id;
1190
- const updates = { ...message };
1191
- delete updates.type;
1192
- delete updates.entity_id;
1193
- delete updates.id;
1194
- const index = this.fixtures.entityRegistry.findIndex((e) => e.entity_id === entityId);
1195
- if (index === -1) {
1196
- const error = new Error("Entity registry entry not found");
1197
- error.code = ERROR_CODES.NOT_FOUND;
1198
- throw error;
1199
- }
1200
- this.updateEntityRegistry(entityId, updates);
1201
- const updated = this.fixtures.entityRegistry[index];
1202
- if (!updated) throw new Error("Entity registry entry not found after update");
1203
- return updated;
1204
- }
1205
- handleServiceCall(message) {
1206
- const { domain, service, service_data, target, return_response } = message;
1207
- const context = {
1208
- id: this.generateId(),
1209
- parent_id: null,
1210
- user_id: null
1211
- };
1212
- if (this.autoReact) setTimeout(() => {
1213
- this.handleServiceReaction(domain, service, service_data, target, context);
1214
- }, this.reactionDelay);
1215
- if (domain === "weather" && service === "get_forecasts" && return_response) {
1216
- const entityId = target?.entity_id;
1217
- const forecastType = service_data?.type || "hourly";
1218
- if (!entityId) {
1219
- const error = new Error("Entity ID required for forecast");
1220
- error.code = ERROR_CODES.INVALID_FORMAT;
1221
- throw error;
1222
- }
1223
- if (!this.fixtures.entities[entityId]) {
1224
- const error = new Error(`Weather entity not found: ${entityId}`);
1225
- error.code = ERROR_CODES.NOT_FOUND;
1226
- throw error;
1227
- }
1228
- const forecast = this.fixtures.forecasts?.[entityId]?.[forecastType] || [];
1229
- return {
1230
- context,
1231
- response: { [entityId]: { forecast } }
1232
- };
1233
- }
1234
- if (return_response) return {
1235
- context,
1236
- response: {}
1237
- };
1238
- return { context };
1239
- }
1240
- /**
1241
- * Handle automatic reactions to service calls using behavior handlers
1242
- */
1243
- handleServiceReaction(domain, service, serviceData, target, context) {
1244
- const entityIds = this.resolveTargetEntities(domain, target);
1245
- const handler = getBehaviorHandler(domain);
1246
- if (!handler) return;
1247
- for (const entityId of entityIds) {
1248
- const entity = this.fixtures.entities[entityId];
1249
- if (!entity) continue;
1250
- try {
1251
- const result = handler.handleService(entity, service, serviceData, context);
1252
- if (result.error) {
1253
- console.warn(`Mock behavior error for ${entityId}.${service}:`, result.error.message);
1254
- continue;
1255
- }
1256
- if (result.stateUpdate) this.setStateWithContext(entityId, result.stateUpdate, context);
1257
- if (result.additionalActions) for (const action of result.additionalActions) setTimeout(() => {
1258
- this.setStateWithContext(entityId, action.update, context);
1259
- }, action.delay);
1260
- } catch (error) {
1261
- console.error(`Mock behavior error for ${entityId}:`, error);
1262
- }
1263
- }
1264
- }
1265
- /**
1266
- * Resolve target entities from service call target
1267
- */
1268
- resolveTargetEntities(domain, target) {
1269
- if (!target) return [];
1270
- const entityIds = [];
1271
- if (target.entity_id) if (Array.isArray(target.entity_id)) entityIds.push(...target.entity_id);
1272
- else entityIds.push(target.entity_id);
1273
- if (target.area_id) {
1274
- const areaId = target.area_id;
1275
- for (const entry of this.fixtures.entityRegistry) if (entry.area_id === areaId) {
1276
- const entity = this.fixtures.entities[entry.entity_id];
1277
- if (entity && entity.entity_id.startsWith(domain + ".")) entityIds.push(entry.entity_id);
1278
- }
1279
- }
1280
- if (target.device_id) {
1281
- const deviceId = target.device_id;
1282
- for (const entry of this.fixtures.entityRegistry) if (entry.device_id === deviceId) {
1283
- const entity = this.fixtures.entities[entry.entity_id];
1284
- if (entity && entity.entity_id.startsWith(domain + ".")) entityIds.push(entry.entity_id);
1285
- }
1286
- }
1287
- return entityIds;
1288
- }
1289
- /**
1290
- * Set state with custom context (internal use)
1291
- */
1292
- setStateWithContext(entityId, updates, context) {
1293
- const current = this.fixtures.entities[entityId];
1294
- if (!current) throw new Error(`Entity not found: ${entityId}`);
1295
- const newState = {
1296
- ...current,
1297
- ...updates,
1298
- last_changed: updates.state !== current.state ? (/* @__PURE__ */ new Date()).toISOString() : current.last_changed,
1299
- last_updated: (/* @__PURE__ */ new Date()).toISOString()
1300
- };
1301
- this.fixtures.entities[entityId] = newState;
1302
- for (const [, subscription] of this.subscriptions.entries()) if (subscription.isEntitySubscription && subscription.callback) {
1303
- const changeMsg = { c: { [entityId]: { "+": {
1304
- s: newState.state,
1305
- a: newState.attributes || {},
1306
- lc: Math.floor(new Date(newState.last_changed || Date.now()).getTime() / 1e3),
1307
- lu: Math.floor(new Date(newState.last_updated || Date.now()).getTime() / 1e3)
1308
- } } } };
1309
- subscription.callback(changeMsg);
1310
- }
1311
- }
1312
- /**
1313
- * Enable/disable automatic reactions
1314
- */
1315
- setAutoReact(enabled) {
1316
- this.autoReact = enabled;
1317
- }
1318
- /**
1319
- * Set reaction delay (useful for testing timing)
1320
- */
1321
- setReactionDelay(delay$1) {
1322
- this.reactionDelay = delay$1;
1323
- }
1324
- generateId() {
1325
- return `mock_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
1326
- }
1327
- };
1328
-
1329
- //#endregion
1330
- //#region src/testing/fixtures.ts
1331
- /**
1332
- * Create a mock entity state
1333
- *
1334
- * @param entityId - Entity ID
1335
- * @param overrides - Optional field overrides
1336
- * @returns Mock entity state
1337
- *
1338
- * @example
1339
- * ```typescript
1340
- * const light = createEntity('light.kitchen', {
1341
- * state: 'on',
1342
- * attributes: { brightness: 255 },
1343
- * });
1344
- * ```
1345
- */
1346
- function createEntity(entityId, overrides = {}) {
1347
- entityId.split(".");
1348
- return {
1349
- entity_id: entityId,
1350
- state: "unknown",
1351
- attributes: {},
1352
- last_changed: (/* @__PURE__ */ new Date()).toISOString(),
1353
- last_updated: (/* @__PURE__ */ new Date()).toISOString(),
1354
- context: {
1355
- id: generateId(),
1356
- parent_id: null,
1357
- user_id: null
1358
- },
1359
- ...overrides
1360
- };
1361
- }
1362
- /**
1363
- * Create a mock entity registry entry
1364
- *
1365
- * @param entityId - Entity ID
1366
- * @param overrides - Optional field overrides
1367
- * @returns Mock registry entry
1368
- *
1369
- * @example
1370
- * ```typescript
1371
- * const entry = createEntityRegistryEntry('light.kitchen', {
1372
- * name: 'Kitchen Light',
1373
- * area_id: 'kitchen',
1374
- * });
1375
- * ```
1376
- */
1377
- function createEntityRegistryEntry(entityId, overrides = {}) {
1378
- const [, objectId] = entityId.split(".");
1379
- if (!objectId) throw new Error(`Invalid entity ID: ${entityId}`);
1380
- return {
1381
- entity_id: entityId,
1382
- name: overrides.name || objectId.replace(/_/g, " "),
1383
- icon: overrides.icon || null,
1384
- platform: overrides.platform || "mock",
1385
- area_id: overrides.area_id || null,
1386
- device_id: overrides.device_id || null,
1387
- disabled_by: overrides.disabled_by || null,
1388
- hidden_by: overrides.hidden_by || null,
1389
- entity_category: overrides.entity_category || null,
1390
- has_entity_name: overrides.has_entity_name || false,
1391
- original_name: overrides.original_name || null,
1392
- unique_id: overrides.unique_id || `mock_${entityId}`,
1393
- config_entry_id: overrides.config_entry_id || null,
1394
- labels: overrides.labels || [],
1395
- translation_key: overrides.translation_key || null,
1396
- ...overrides
1397
- };
1398
- }
1399
- /**
1400
- * Create a mock area registry entry
1401
- *
1402
- * @param areaId - Area ID
1403
- * @param overrides - Optional field overrides
1404
- * @returns Mock area entry
1405
- *
1406
- * @example
1407
- * ```typescript
1408
- * const area = createArea('kitchen', {
1409
- * name: 'Kitchen',
1410
- * floor_id: 'ground_floor',
1411
- * });
1412
- * ```
1413
- */
1414
- function createArea(areaId, overrides = {}) {
1415
- return {
1416
- id: areaId,
1417
- name: overrides.name || areaId.replace(/_/g, " "),
1418
- normalized_name: areaId.toLowerCase(),
1419
- picture: overrides.picture || null,
1420
- floor_id: overrides.floor_id || null,
1421
- icon: overrides.icon || null,
1422
- aliases: overrides.aliases || [],
1423
- labels: overrides.labels || [],
1424
- created_at: overrides.created_at || (/* @__PURE__ */ new Date()).toISOString(),
1425
- modified_at: overrides.modified_at || (/* @__PURE__ */ new Date()).toISOString(),
1426
- humidity_entity_id: overrides.humidity_entity_id || null,
1427
- temperature_entity_id: overrides.temperature_entity_id || null,
1428
- ...overrides
1429
- };
1430
- }
1431
- /**
1432
- * Create a mock device registry entry
1433
- *
1434
- * @param deviceId - Device ID
1435
- * @param overrides - Optional field overrides
1436
- * @returns Mock device entry
1437
- *
1438
- * @example
1439
- * ```typescript
1440
- * const device = createDevice('device_123', {
1441
- * name: 'Smart Light',
1442
- * area_id: 'kitchen',
1443
- * });
1444
- * ```
1445
- */
1446
- function createDevice(deviceId, overrides = {}) {
1447
- return {
1448
- id: deviceId,
1449
- name: overrides.name || "Mock Device",
1450
- area_id: overrides.area_id || null,
1451
- name_by_user: overrides.name_by_user || null,
1452
- disabled_by: overrides.disabled_by || null,
1453
- configuration_url: overrides.configuration_url || null,
1454
- manufacturer: overrides.manufacturer || "Mock Manufacturer",
1455
- model: overrides.model || "Mock Model",
1456
- sw_version: overrides.sw_version || "1.0.0",
1457
- hw_version: overrides.hw_version || null,
1458
- via_device_id: overrides.via_device_id || null,
1459
- entry_type: overrides.entry_type || null,
1460
- identifiers: overrides.identifiers || [[deviceId]],
1461
- connections: overrides.connections || [],
1462
- config_entries: overrides.config_entries || [],
1463
- config_entries_subentries: overrides.config_entries_subentries || {},
1464
- labels: overrides.labels || [],
1465
- created_at: overrides.created_at || (/* @__PURE__ */ new Date()).toISOString(),
1466
- modified_at: overrides.modified_at || (/* @__PURE__ */ new Date()).toISOString(),
1467
- model_id: overrides.model_id || null,
1468
- primary_config_entry: overrides.primary_config_entry || null,
1469
- serial_number: overrides.serial_number || null,
1470
- ...overrides
1471
- };
1472
- }
1473
- /**
1474
- * Create a mock floor registry entry
1475
- *
1476
- * @param floorId - Floor ID
1477
- * @param overrides - Optional field overrides
1478
- * @returns Mock floor entry
1479
- *
1480
- * @example
1481
- * ```typescript
1482
- * const floor = createFloor('ground_floor', {
1483
- * name: 'Ground Floor',
1484
- * level: 0,
1485
- * });
1486
- * ```
1487
- */
1488
- function createFloor(floorId, overrides = {}) {
1489
- return {
1490
- floor_id: floorId,
1491
- name: overrides.name || floorId.replace(/_/g, " "),
1492
- level: overrides.level || 0,
1493
- icon: overrides.icon || null,
1494
- aliases: overrides.aliases || [],
1495
- ...overrides
1496
- };
1497
- }
1498
- /**
1499
- * Create a mock label registry entry
1500
- *
1501
- * @param labelId - Label ID
1502
- * @param overrides - Optional field overrides
1503
- * @returns Mock label entry
1504
- *
1505
- * @example
1506
- * ```typescript
1507
- * const label = createLabel('important', {
1508
- * name: 'Important',
1509
- * color: 'red',
1510
- * });
1511
- * ```
1512
- */
1513
- function createLabel(labelId, overrides = {}) {
1514
- return {
1515
- label_id: labelId,
1516
- name: overrides.name || labelId.replace(/_/g, " "),
1517
- color: overrides.color || null,
1518
- icon: overrides.icon || null,
1519
- description: overrides.description || null,
1520
- ...overrides
1521
- };
1522
- }
1523
- /**
1524
- * Create a basic fixture set for testing
1525
- *
1526
- * @returns Complete fixture set with basic entities
1527
- *
1528
- * @example
1529
- * ```typescript
1530
- * const fixtures = createBasicFixtures();
1531
- * const mock = initMockHA(fixtures);
1532
- * ```
1533
- */
1534
- function createBasicFixtures() {
1535
- const kitchen = createArea("kitchen", { name: "Kitchen" });
1536
- const livingRoom = createArea("living_room", { name: "Living Room" });
1537
- const bedroom = createArea("bedroom", { name: "Bedroom" });
1538
- const office = createArea("office", { name: "Office" });
1539
- const lightDevice = createDevice("device_light_1", {
1540
- name: "Kitchen Light Strip",
1541
- area_id: "kitchen"
1542
- });
1543
- const entities = {
1544
- "light.kitchen": createEntity("light.kitchen", {
1545
- state: "on",
1546
- attributes: {
1547
- brightness: 255,
1548
- friendly_name: "Kitchen Light"
1549
- }
1550
- }),
1551
- "light.living_room": createEntity("light.living_room", {
1552
- state: "off",
1553
- attributes: { friendly_name: "Living Room Light" }
1554
- }),
1555
- "light.bedroom": createEntity("light.bedroom", {
1556
- state: "on",
1557
- attributes: {
1558
- brightness: 128,
1559
- color_temp: 370,
1560
- friendly_name: "Bedroom Light"
1561
- }
1562
- }),
1563
- "switch.fan": createEntity("switch.fan", {
1564
- state: "off",
1565
- attributes: { friendly_name: "Ceiling Fan" }
1566
- }),
1567
- "switch.outlet": createEntity("switch.outlet", {
1568
- state: "on",
1569
- attributes: { friendly_name: "Living Room Outlet" }
1570
- }),
1571
- "button.doorbell": createEntity("button.doorbell", {
1572
- state: "unknown",
1573
- attributes: { friendly_name: "Doorbell" }
1574
- }),
1575
- "button.garage": createEntity("button.garage", {
1576
- state: "unknown",
1577
- attributes: { friendly_name: "Garage Door" }
1578
- }),
1579
- "cover.garage_door": createEntity("cover.garage_door", {
1580
- state: "closed",
1581
- attributes: {
1582
- friendly_name: "Garage Door",
1583
- current_position: 0,
1584
- device_class: "garage"
1585
- }
1586
- }),
1587
- "cover.blind": createEntity("cover.blind", {
1588
- state: "open",
1589
- attributes: {
1590
- friendly_name: "Living Room Blinds",
1591
- current_position: 75,
1592
- device_class: "blind"
1593
- }
1594
- }),
1595
- "media_player.living_room": createEntity("media_player.living_room", {
1596
- state: "playing",
1597
- attributes: {
1598
- friendly_name: "Living Room TV",
1599
- media_title: "Midnight Horizons",
1600
- media_artist: "Synthetic Dreams",
1601
- media_album_name: "Neon Nights",
1602
- entity_picture: "https://picsum.photos/seed/album1/300/300",
1603
- volume_level: .5,
1604
- is_volume_muted: false,
1605
- media_content_type: "music",
1606
- media_duration: 245,
1607
- media_position: 45,
1608
- supported_features: 16383,
1609
- device_class: "speaker"
1610
- }
1611
- }),
1612
- "media_player.bedroom": createEntity("media_player.bedroom", {
1613
- state: "paused",
1614
- attributes: {
1615
- friendly_name: "Bedroom Speaker",
1616
- media_title: "Electric Pulse",
1617
- media_artist: "Digital Waves",
1618
- media_album_name: "Synth Collection",
1619
- entity_picture: "https://picsum.photos/seed/album2/300/300",
1620
- volume_level: .3,
1621
- is_volume_muted: false,
1622
- media_content_type: "music",
1623
- media_duration: 198,
1624
- media_position: 67,
1625
- supported_features: 16383,
1626
- device_class: "speaker"
1627
- }
1628
- }),
1629
- "climate.thermostat": createEntity("climate.thermostat", {
1630
- state: "heat",
1631
- attributes: {
1632
- friendly_name: "Living Room Thermostat",
1633
- current_temperature: 22,
1634
- temperature: 23,
1635
- hvac_modes: [
1636
- "off",
1637
- "heat",
1638
- "cool",
1639
- "auto"
1640
- ],
1641
- hvac_mode: "heat",
1642
- min_temp: 16,
1643
- max_temp: 30
1644
- }
1645
- }),
1646
- "climate.bedroom": createEntity("climate.bedroom", {
1647
- state: "cool",
1648
- attributes: {
1649
- friendly_name: "Bedroom AC",
1650
- current_temperature: 20,
1651
- temperature: 21,
1652
- hvac_modes: [
1653
- "off",
1654
- "heat",
1655
- "cool",
1656
- "auto"
1657
- ],
1658
- hvac_mode: "cool",
1659
- min_temp: 16,
1660
- max_temp: 30
1661
- }
1662
- }),
1663
- "sensor.temperature": createEntity("sensor.temperature", {
1664
- state: "22.5",
1665
- attributes: {
1666
- unit_of_measurement: "°C",
1667
- device_class: "temperature",
1668
- friendly_name: "Temperature Sensor"
1669
- }
1670
- }),
1671
- "sensor.humidity": createEntity("sensor.humidity", {
1672
- state: "45",
1673
- attributes: {
1674
- unit_of_measurement: "%",
1675
- device_class: "humidity",
1676
- friendly_name: "Humidity Sensor"
1677
- }
1678
- }),
1679
- "sensor.motion": createEntity("sensor.motion", {
1680
- state: "on",
1681
- attributes: {
1682
- friendly_name: "Motion Sensor",
1683
- device_class: "motion"
1684
- }
1685
- }),
1686
- "sensor.battery_phone": createEntity("sensor.battery_phone", {
1687
- state: "85",
1688
- attributes: {
1689
- unit_of_measurement: "%",
1690
- device_class: "battery",
1691
- friendly_name: "Phone Battery"
1692
- }
1693
- }),
1694
- "sensor.battery_sensor": createEntity("sensor.battery_sensor", {
1695
- state: "92",
1696
- attributes: {
1697
- unit_of_measurement: "%",
1698
- device_class: "battery",
1699
- friendly_name: "Motion Sensor Battery"
1700
- }
1701
- }),
1702
- "sensor.battery_laptop": createEntity("sensor.battery_laptop", {
1703
- state: "78",
1704
- attributes: {
1705
- unit_of_measurement: "%",
1706
- device_class: "battery",
1707
- friendly_name: "Laptop Battery"
1708
- }
1709
- }),
1710
- "sensor.battery_tablet": createEntity("sensor.battery_tablet", {
1711
- state: "15",
1712
- attributes: {
1713
- unit_of_measurement: "%",
1714
- device_class: "battery",
1715
- friendly_name: "Tablet Battery"
1716
- }
1717
- }),
1718
- "sensor.battery_door_sensor": createEntity("sensor.battery_door_sensor", {
1719
- state: "100",
1720
- attributes: {
1721
- unit_of_measurement: "%",
1722
- device_class: "battery",
1723
- friendly_name: "Door Sensor Battery"
1724
- }
1725
- }),
1726
- "sensor.battery_window_sensor": createEntity("sensor.battery_window_sensor", {
1727
- state: "67",
1728
- attributes: {
1729
- unit_of_measurement: "%",
1730
- device_class: "battery",
1731
- friendly_name: "Window Sensor Battery"
1732
- }
1733
- }),
1734
- "camera.front_door": createEntity("camera.front_door", {
1735
- state: "idle",
1736
- attributes: {
1737
- friendly_name: "Front Door Camera",
1738
- entity_picture: "/api/camera_proxy/camera.front_door"
1739
- }
1740
- }),
1741
- "weather.home": createEntity("weather.home", {
1742
- state: "sunny",
1743
- attributes: {
1744
- friendly_name: "Home Weather",
1745
- temperature: 22,
1746
- temperature_unit: "°C",
1747
- humidity: 45,
1748
- pressure: 1013,
1749
- wind_speed: 5,
1750
- wind_bearing: 180,
1751
- condition: "sunny"
1752
- }
1753
- }),
1754
- "scene.movie_night": createEntity("scene.movie_night", {
1755
- state: "scening",
1756
- attributes: { friendly_name: "Movie Night" }
1757
- }),
1758
- "scene.relax": createEntity("scene.relax", {
1759
- state: "scening",
1760
- attributes: { friendly_name: "Relax" }
1761
- }),
1762
- "lock.front_door": createEntity("lock.front_door", {
1763
- state: "locked",
1764
- attributes: {
1765
- friendly_name: "Front Door Lock",
1766
- code_format: "number"
1767
- }
1768
- })
1769
- };
1770
- const entityRegistry = [
1771
- createEntityRegistryEntry("light.kitchen", {
1772
- area_id: "kitchen",
1773
- device_id: "device_light_1"
1774
- }),
1775
- createEntityRegistryEntry("light.living_room", { area_id: "living_room" }),
1776
- createEntityRegistryEntry("light.bedroom", { area_id: "bedroom" }),
1777
- createEntityRegistryEntry("switch.fan", { area_id: "bedroom" }),
1778
- createEntityRegistryEntry("switch.outlet", { area_id: "living_room" }),
1779
- createEntityRegistryEntry("button.doorbell", { area_id: "kitchen" }),
1780
- createEntityRegistryEntry("button.garage", { area_id: "kitchen" }),
1781
- createEntityRegistryEntry("cover.garage_door", { area_id: "kitchen" }),
1782
- createEntityRegistryEntry("cover.blind", { area_id: "living_room" }),
1783
- createEntityRegistryEntry("media_player.living_room", { area_id: "living_room" }),
1784
- createEntityRegistryEntry("media_player.bedroom", { area_id: "bedroom" }),
1785
- createEntityRegistryEntry("climate.thermostat", { area_id: "living_room" }),
1786
- createEntityRegistryEntry("climate.bedroom", { area_id: "bedroom" }),
1787
- createEntityRegistryEntry("sensor.temperature", { area_id: "kitchen" }),
1788
- createEntityRegistryEntry("sensor.humidity", { area_id: "kitchen" }),
1789
- createEntityRegistryEntry("sensor.motion", { area_id: "living_room" }),
1790
- createEntityRegistryEntry("sensor.battery_phone", { area_id: "bedroom" }),
1791
- createEntityRegistryEntry("sensor.battery_sensor", { area_id: "living_room" }),
1792
- createEntityRegistryEntry("sensor.battery_laptop", { area_id: "office" }),
1793
- createEntityRegistryEntry("sensor.battery_tablet", { area_id: "living_room" }),
1794
- createEntityRegistryEntry("sensor.battery_door_sensor", { area_id: "kitchen" }),
1795
- createEntityRegistryEntry("sensor.battery_window_sensor", { area_id: "bedroom" }),
1796
- createEntityRegistryEntry("camera.front_door", { area_id: "kitchen" }),
1797
- createEntityRegistryEntry("weather.home", { area_id: null }),
1798
- createEntityRegistryEntry("scene.movie_night", { area_id: "living_room" }),
1799
- createEntityRegistryEntry("scene.relax", { area_id: "bedroom" }),
1800
- createEntityRegistryEntry("lock.front_door", { area_id: "kitchen" })
1801
- ];
1802
- return {
1803
- entities,
1804
- entityRegistry,
1805
- deviceRegistry: [lightDevice],
1806
- areaRegistry: [
1807
- kitchen,
1808
- livingRoom,
1809
- bedroom,
1810
- office
1811
- ],
1812
- floorRegistry: [],
1813
- labelRegistry: []
1814
- };
1815
- }
1816
- /**
1817
- * Load fixtures from JSON
1818
- *
1819
- * @param json - JSON string or object
1820
- * @returns Parsed fixtures
1821
- *
1822
- * @example
1823
- * ```typescript
1824
- * const fixtures = loadFixture(`{
1825
- * "entities": { ... },
1826
- * "entityRegistry": [ ... ]
1827
- * }`);
1828
- * ```
1829
- */
1830
- function loadFixture(json) {
1831
- if (typeof json === "string") return JSON.parse(json);
1832
- return json;
1833
- }
1834
- /**
1835
- * Create a fixture set from partial data
1836
- *
1837
- * Fills in missing fields with defaults.
1838
- *
1839
- * @param partial - Partial fixture data
1840
- * @returns Complete fixture set
1841
- */
1842
- function createFixture(partial = {}) {
1843
- return {
1844
- entities: partial.entities || {},
1845
- entityRegistry: partial.entityRegistry || [],
1846
- deviceRegistry: partial.deviceRegistry || [],
1847
- areaRegistry: partial.areaRegistry || [],
1848
- floorRegistry: partial.floorRegistry || [],
1849
- labelRegistry: partial.labelRegistry || [],
1850
- history: partial.history || {}
1851
- };
1852
- }
1853
- /**
1854
- * Create a history state point
1855
- *
1856
- * @param state - State value
1857
- * @param timestamp - Unix timestamp in seconds
1858
- * @param attributes - Optional attributes
1859
- * @param lastChanged - Optional last changed timestamp (defaults to timestamp)
1860
- * @returns History state point
1861
- *
1862
- * @example
1863
- * ```typescript
1864
- * const historyState = createHistoryState("20.5", Math.floor(Date.now() / 1000) - 3600, {
1865
- * unit_of_measurement: "°C",
1866
- * });
1867
- * ```
1868
- */
1869
- function createHistoryState(state$1, timestamp, attributes = {}, lastChanged) {
1870
- return {
1871
- s: state$1,
1872
- lu: timestamp,
1873
- lc: lastChanged ?? timestamp,
1874
- a: attributes
1875
- };
1876
- }
1877
- /**
1878
- * Generate a random ID
1879
- */
1880
- function generateId() {
1881
- return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1882
- }
1883
-
1884
- //#endregion
1885
- //#region src/testing/helpers.ts
1886
- /**
1887
- * Wait for a condition to be true
1888
- *
1889
- * @param condition - Function that returns true when ready
1890
- * @param timeout - Maximum wait time in ms
1891
- * @param interval - Check interval in ms
1892
- * @returns Promise that resolves when condition is true
1893
- *
1894
- * @example
1895
- * ```typescript
1896
- * await waitFor(() => entity('light.kitchen').get()?.state === 'on', 1000);
1897
- * ```
1898
- */
1899
- async function waitFor(condition, timeout = 1e3, interval = 1) {
1900
- const start = Date.now();
1901
- while (!condition()) {
1902
- if (Date.now() - start > timeout) throw new Error(`Timeout waiting for condition after ${timeout}ms`);
1903
- await delay(interval);
1904
- }
1905
- }
1906
- /**
1907
- * Simulate state change event
1908
- *
1909
- * Updates entity state and emits event through mock connection.
1910
- *
1911
- * @param mockConnection - MockConnection instance
1912
- * @param entityId - Entity ID
1913
- * @param updates - State updates
1914
- * @param options - Optional simulation options
1915
- * @returns Promise that resolves when update completes
1916
- *
1917
- * @example
1918
- * ```typescript
1919
- * const mockConnection = new MockConnection(fixtures);
1920
- * await simulateStateChange(mockConnection, 'light.kitchen', {
1921
- * state: 'on',
1922
- * attributes: { brightness: 255 },
1923
- * });
1924
- * ```
1925
- */
1926
- async function simulateStateChange(mockConnection, entityId, updates, options) {
1927
- mockConnection.setState(entityId, updates);
1928
- await delay(options?.delay || 10);
1929
- }
1930
- /**
1931
- * Simulate registry update event
1932
- *
1933
- * Updates entity registry and emits event through mock connection.
1934
- *
1935
- * @param mockConnection - MockConnection instance
1936
- * @param entityId - Entity ID
1937
- * @param updates - Registry updates
1938
- * @param options - Optional simulation options
1939
- * @returns Promise that resolves when update completes
1940
- *
1941
- * @example
1942
- * ```typescript
1943
- * const mockConnection = new MockConnection(fixtures);
1944
- * await simulateRegistryUpdate(mockConnection, 'light.kitchen', {
1945
- * name: 'New Name',
1946
- * area_id: 'bedroom',
1947
- * });
1948
- * ```
1949
- */
1950
- async function simulateRegistryUpdate(mockConnection, entityId, updates, options) {
1951
- mockConnection.updateEntityRegistry(entityId, updates);
1952
- await delay(options?.delay || 10);
1953
- }
1954
- /**
1955
- * Simulate multiple state changes in sequence
1956
- *
1957
- * @param mockConnection - MockConnection instance
1958
- * @param changes - Array of state change configurations
1959
- * @param delayBetween - Delay between changes in ms
1960
- * @returns Promise that resolves when all changes complete
1961
- *
1962
- * @example
1963
- * ```typescript
1964
- * const mockConnection = new MockConnection(fixtures);
1965
- * await simulateStateChanges(mockConnection, [
1966
- * { entityId: 'light.kitchen', updates: { state: 'on' } },
1967
- * { entityId: 'light.living_room', updates: { state: 'on' } },
1968
- * ], 100);
1969
- * ```
1970
- */
1971
- async function simulateStateChanges(mockConnection, changes, delayBetween = 0) {
1972
- for (const change of changes) {
1973
- await simulateStateChange(mockConnection, change.entityId, change.updates);
1974
- if (delayBetween > 0) await delay(delayBetween);
1975
- }
1976
- }
1977
- /**
1978
- * Get current store state snapshot
1979
- *
1980
- * @returns Current store state
1981
- */
1982
- function getStoreSnapshot() {
1983
- return state;
1984
- }
1985
- /**
1986
- * Get entity from store
1987
- *
1988
- * @param entityId - Entity ID
1989
- * @returns Entity state or undefined
1990
- */
1991
- function getEntityFromStore(entityId) {
1992
- return state.entities[entityId];
1993
- }
1994
- /**
1995
- * Get entity registry entry from store
1996
- *
1997
- * @param entityId - Entity ID
1998
- * @returns Registry entry or undefined
1999
- */
2000
- function getEntityRegistryFromStore(entityId) {
2001
- return state.entityRegistry[entityId];
2002
- }
2003
- /**
2004
- * Check if entity exists in store
2005
- *
2006
- * @param entityId - Entity ID
2007
- * @returns True if entity exists
2008
- */
2009
- function entityExistsInStore(entityId) {
2010
- return entityId in state.entities;
2011
- }
2012
- /**
2013
- * Delay for specified time
2014
- *
2015
- * @param ms - Milliseconds to delay
2016
- * @returns Promise that resolves after delay
2017
- */
2018
- function delay(ms) {
2019
- return new Promise((resolve) => setTimeout(resolve, ms));
2020
- }
2021
- /**
2022
- * Wait for mock connection to complete
2023
- *
2024
- * Helper to ensure fixtures are loaded before running tests.
2025
- * Use after creating MockConnection and connecting.
2026
- *
2027
- * @example
2028
- * ```typescript
2029
- * const mockConnection = new MockConnection(fixtures);
2030
- * await mockConnection.connect();
2031
- * await waitForConnection();
2032
- * // Now safe to run queries
2033
- * ```
2034
- */
2035
- function waitForConnection() {
2036
- return delay(10);
2037
- }
2038
- /**
2039
- * Create a spy function for testing
2040
- *
2041
- * @returns Spy function with call tracking
2042
- *
2043
- * @example
2044
- * ```typescript
2045
- * const spy = createSpy();
2046
- * entities().byDomain('light').subscribe(spy);
2047
- * // Check spy.calls, spy.callCount, etc.
2048
- * ```
2049
- */
2050
- function createSpy() {
2051
- const calls = [];
2052
- const spy = (...args) => {
2053
- calls.push(args);
2054
- };
2055
- spy.calls = calls;
2056
- spy.reset = () => {
2057
- calls.length = 0;
2058
- };
2059
- Object.defineProperty(spy, "callCount", {
2060
- get: () => calls.length,
2061
- configurable: true
2062
- });
2063
- return spy;
2064
- }
2065
-
2066
- //#endregion
2067
- export { MockConnection, createArea, createBasicFixtures, createDemoFixtures, createDevice, createEntity, createEntityRegistryEntry, createFixture, createFloor, createHistoryState, createLabel, createSpy, delay, entityExistsInStore, getEntityFromStore, getEntityRegistryFromStore, getStoreSnapshot, loadFixture, simulateRegistryUpdate, simulateStateChange, simulateStateChanges, waitFor, waitForConnection };