@mp-consulting/homebridge-daikin-cloud 1.3.5 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +39 -1
- package/README.md +5 -3
- package/dist/src/accessories/air-conditioning-accessory.d.ts +2 -2
- package/dist/src/accessories/air-conditioning-accessory.d.ts.map +1 -1
- package/dist/src/accessories/air-conditioning-accessory.js.map +1 -1
- package/dist/src/accessories/altherma-accessory.d.ts +2 -2
- package/dist/src/accessories/altherma-accessory.d.ts.map +1 -1
- package/dist/src/accessories/altherma-accessory.js.map +1 -1
- package/dist/src/accessories/base-accessory.d.ts +6 -6
- package/dist/src/accessories/base-accessory.d.ts.map +1 -1
- package/dist/src/accessories/base-accessory.js +15 -15
- package/dist/src/accessories/base-accessory.js.map +1 -1
- package/dist/src/api/daikin-api.d.ts +26 -26
- package/dist/src/api/daikin-api.d.ts.map +1 -1
- package/dist/src/api/daikin-api.js +68 -42
- package/dist/src/api/daikin-api.js.map +1 -1
- package/dist/src/api/daikin-cloud.repository.d.ts.map +1 -1
- package/dist/src/api/daikin-cloud.repository.js +22 -14
- package/dist/src/api/daikin-cloud.repository.js.map +1 -1
- package/dist/src/api/daikin-controller.d.ts +41 -47
- package/dist/src/api/daikin-controller.d.ts.map +1 -1
- package/dist/src/api/daikin-controller.js +40 -64
- package/dist/src/api/daikin-controller.js.map +1 -1
- package/dist/src/api/daikin-device.d.ts +36 -31
- package/dist/src/api/daikin-device.d.ts.map +1 -1
- package/dist/src/api/daikin-device.js +45 -31
- package/dist/src/api/daikin-device.js.map +1 -1
- package/dist/src/api/daikin-mobile-oauth.d.ts +20 -20
- package/dist/src/api/daikin-mobile-oauth.d.ts.map +1 -1
- package/dist/src/api/daikin-mobile-oauth.js +49 -44
- package/dist/src/api/daikin-mobile-oauth.js.map +1 -1
- package/dist/src/api/daikin-oauth.d.ts +32 -32
- package/dist/src/api/daikin-oauth.d.ts.map +1 -1
- package/dist/src/api/daikin-oauth.js +64 -56
- package/dist/src/api/daikin-oauth.js.map +1 -1
- package/dist/src/api/daikin-schemas.d.ts +476 -351
- package/dist/src/api/daikin-schemas.d.ts.map +1 -1
- package/dist/src/api/daikin-schemas.js +11 -42
- package/dist/src/api/daikin-schemas.js.map +1 -1
- package/dist/src/api/daikin-types.d.ts +5 -1
- package/dist/src/api/daikin-types.d.ts.map +1 -1
- package/dist/src/api/daikin-types.js.map +1 -1
- package/dist/src/api/daikin-websocket.d.ts +31 -32
- package/dist/src/api/daikin-websocket.d.ts.map +1 -1
- package/dist/src/api/daikin-websocket.js +55 -35
- package/dist/src/api/daikin-websocket.js.map +1 -1
- package/dist/src/api/index.d.ts +2 -1
- package/dist/src/api/index.d.ts.map +1 -1
- package/dist/src/api/index.js +3 -1
- package/dist/src/api/index.js.map +1 -1
- package/dist/src/api/token-storage.d.ts +21 -0
- package/dist/src/api/token-storage.d.ts.map +1 -0
- package/dist/src/api/token-storage.js +90 -0
- package/dist/src/api/token-storage.js.map +1 -0
- package/dist/src/config/config-manager.d.ts +33 -33
- package/dist/src/config/config-manager.d.ts.map +1 -1
- package/dist/src/config/config-manager.js +33 -33
- package/dist/src/config/config-manager.js.map +1 -1
- package/dist/src/constants/api.constants.d.ts +4 -0
- package/dist/src/constants/api.constants.d.ts.map +1 -1
- package/dist/src/constants/api.constants.js +5 -1
- package/dist/src/constants/api.constants.js.map +1 -1
- package/dist/src/constants/device.constants.d.ts +4 -0
- package/dist/src/constants/device.constants.d.ts.map +1 -1
- package/dist/src/constants/device.constants.js +5 -1
- package/dist/src/constants/device.constants.js.map +1 -1
- package/dist/src/device/accessory-factory.d.ts +10 -10
- package/dist/src/device/accessory-factory.d.ts.map +1 -1
- package/dist/src/device/accessory-factory.js +7 -7
- package/dist/src/device/accessory-factory.js.map +1 -1
- package/dist/src/device/capability-detector.d.ts +8 -8
- package/dist/src/device/capability-detector.d.ts.map +1 -1
- package/dist/src/device/capability-detector.js +6 -6
- package/dist/src/device/capability-detector.js.map +1 -1
- package/dist/src/device/capability-docs.d.ts +1 -9
- package/dist/src/device/capability-docs.d.ts.map +1 -1
- package/dist/src/device/capability-docs.js +19 -73
- package/dist/src/device/capability-docs.js.map +1 -1
- package/dist/src/device/profiles/device-profile.d.ts +1 -1
- package/dist/src/device/profiles/device-profile.d.ts.map +1 -1
- package/dist/src/device/profiles/device-profile.js +4 -4
- package/dist/src/device/profiles/device-profile.js.map +1 -1
- package/dist/src/features/base-feature.d.ts +2 -2
- package/dist/src/features/base-feature.d.ts.map +1 -1
- package/dist/src/features/base-feature.js +2 -3
- package/dist/src/features/base-feature.js.map +1 -1
- package/dist/src/features/feature-manager.d.ts +8 -16
- package/dist/src/features/feature-manager.d.ts.map +1 -1
- package/dist/src/features/feature-manager.js +5 -17
- package/dist/src/features/feature-manager.js.map +1 -1
- package/dist/src/features/modes/dry-operation-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/dry-operation-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/dry-operation-mode.feature.js.map +1 -1
- package/dist/src/features/modes/econo-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/econo-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/econo-mode.feature.js.map +1 -1
- package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/fan-only-operation-mode.feature.js.map +1 -1
- package/dist/src/features/modes/indoor-silent-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/indoor-silent-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/indoor-silent-mode.feature.js.map +1 -1
- package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/outdoor-silent-mode.feature.js.map +1 -1
- package/dist/src/features/modes/powerful-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/powerful-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/powerful-mode.feature.js.map +1 -1
- package/dist/src/features/modes/streamer-mode.feature.d.ts +1 -1
- package/dist/src/features/modes/streamer-mode.feature.d.ts.map +1 -1
- package/dist/src/features/modes/streamer-mode.feature.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/platform.d.ts +11 -8
- package/dist/src/platform.d.ts.map +1 -1
- package/dist/src/platform.js +64 -15
- package/dist/src/platform.js.map +1 -1
- package/dist/src/services/climate-control.service.d.ts +8 -2
- package/dist/src/services/climate-control.service.d.ts.map +1 -1
- package/dist/src/services/climate-control.service.js +59 -58
- package/dist/src/services/climate-control.service.js.map +1 -1
- package/dist/src/services/hot-water-tank.service.d.ts +6 -2
- package/dist/src/services/hot-water-tank.service.d.ts.map +1 -1
- package/dist/src/services/hot-water-tank.service.js +33 -31
- package/dist/src/services/hot-water-tank.service.js.map +1 -1
- package/dist/src/types/daikin-enums.js +12 -12
- package/dist/src/types/daikin-enums.js.map +1 -1
- package/dist/src/types/device-capabilities.d.ts +1 -1
- package/dist/src/types/device-capabilities.d.ts.map +1 -1
- package/dist/src/utils/log-context.d.ts +23 -23
- package/dist/src/utils/log-context.d.ts.map +1 -1
- package/dist/src/utils/log-context.js +28 -28
- package/dist/src/utils/log-context.js.map +1 -1
- package/dist/src/utils/strings.d.ts.map +1 -1
- package/dist/src/utils/strings.js.map +1 -1
- package/dist/src/utils/update-mapper.d.ts +16 -16
- package/dist/src/utils/update-mapper.d.ts.map +1 -1
- package/dist/src/utils/update-mapper.js +14 -14
- package/dist/src/utils/update-mapper.js.map +1 -1
- package/homebridge-ui/public/index.html +2 -2
- package/homebridge-ui/public/script.js +957 -898
- package/homebridge-ui/server.js +746 -678
- package/package.json +29 -27
- package/.claude/settings.json +0 -3
- package/.claude/settings.local.json +0 -29
- package/CHANGELOG.md +0 -103
- package/CLAUDE.md +0 -269
- package/config.md +0 -2
- package/dist/src/api/daikin-device-tracker.d.ts +0 -97
- package/dist/src/api/daikin-device-tracker.d.ts.map +0 -1
- package/dist/src/api/daikin-device-tracker.js +0 -136
- package/dist/src/api/daikin-device-tracker.js.map +0 -1
- package/dist/src/api/http-interceptor.d.ts +0 -99
- package/dist/src/api/http-interceptor.d.ts.map +0 -1
- package/dist/src/api/http-interceptor.js +0 -177
- package/dist/src/api/http-interceptor.js.map +0 -1
- package/dist/src/di/service-container.d.ts +0 -92
- package/dist/src/di/service-container.d.ts.map +0 -1
- package/dist/src/di/service-container.js +0 -156
- package/dist/src/di/service-container.js.map +0 -1
- package/dist/src/features/feature-registry.d.ts +0 -100
- package/dist/src/features/feature-registry.d.ts.map +0 -1
- package/dist/src/features/feature-registry.js +0 -142
- package/dist/src/features/feature-registry.js.map +0 -1
- package/dist/src/services/service-factory.d.ts +0 -46
- package/dist/src/services/service-factory.d.ts.map +0 -1
- package/dist/src/services/service-factory.js +0 -72
- package/dist/src/services/service-factory.js.map +0 -1
- package/dist/src/utils/error-handler.d.ts +0 -101
- package/dist/src/utils/error-handler.d.ts.map +0 -1
- package/dist/src/utils/error-handler.js +0 -251
- package/dist/src/utils/error-handler.js.map +0 -1
- package/dist/src/utils/retry.d.ts +0 -42
- package/dist/src/utils/retry.d.ts.map +0 -1
- package/dist/src/utils/retry.js +0 -70
- package/dist/src/utils/retry.js.map +0 -1
- package/docs/ARCHITECTURE.md +0 -645
- package/docs/IMPLEMENTATION_GUIDE.md +0 -899
- package/docs/IMPROVEMENTS_SUMMARY.md +0 -415
- package/docs/NEXT_STEPS.md +0 -368
- package/docs/Screenshot 2024-07-04 at 18.41.28.png +0 -0
- package/docs/TROUBLESHOOTING.md +0 -475
- package/docs/api-response-for-BRP069A8x.json +0 -520
- package/docs/api-response-for-BRP069C4x-2.json +0 -881
- package/docs/api-response-for-BRP069C4x.json +0 -916
- package/docs/api-response-for-altherma.json +0 -759
- package/docs/api-response-for-altherma2.json +0 -2735
- package/docs/api-response-with-multiple-devices-incl-heatpump.json +0 -2544
- package/docs/cr-insance-altherma-id-0.json +0 -834
- package/docs/mock-air-to-air-dx23.json +0 -759
- package/docs/mock-air-to-air-dx4.json +0 -1134
- package/docs/mock-airpurifier-with-humidifier.json +0 -732
- package/docs/mock-airpurifier.json +0 -450
- package/docs/mock-altherma-air-to-water-lan.json +0 -845
- package/docs/mock-altherma-air-to-water-wlan.json +0 -845
- package/docs/mock-d2cnd-gas-boiler.json +0 -649
- package/docs/setpointmode-vs-controlmode-vs-setpoints-vs-sensorydata.txt +0 -6
- package/images/fan-speed.jpeg +0 -0
- package/images/homekit-controls.jpeg +0 -0
- package/images/homekit-settings.jpeg +0 -0
- package/images/swing-mode.png +0 -0
- package/jest.config.ts +0 -13
- package/test/fixtures/altherma-crSense-2.ts +0 -834
- package/test/fixtures/altherma-fraction.ts +0 -718
- package/test/fixtures/altherma-heat-pump-2.ts +0 -479
- package/test/fixtures/altherma-heat-pump.ts +0 -757
- package/test/fixtures/altherma-miladcerkic-off.ts +0 -524
- package/test/fixtures/altherma-miladcerkic.ts +0 -524
- package/test/fixtures/altherma-v1ckoeln.ts +0 -644
- package/test/fixtures/altherma-with-embedded-id-zero.ts +0 -834
- package/test/fixtures/dx23-airco-2.ts +0 -343
- package/test/fixtures/dx23-airco.ts +0 -518
- package/test/fixtures/dx4-airco.ts +0 -914
- package/test/fixtures/unknown-jan.ts +0 -488
- package/test/fixtures/unknown-kitchen-guests.ts +0 -488
- package/test/helpers/test-isolation.ts +0 -228
- package/test/integration/air-conditioning.test.ts +0 -410
- package/test/integration/altherma.test.ts +0 -289
- package/test/integration/platform.test.ts +0 -118
- package/test/mocks/index.ts +0 -27
- package/test/test-gigya-auth.js +0 -443
- package/test/test-mobile-oauth.js +0 -175
- package/test/test-websocket-mobile.js +0 -123
- package/test/test-websocket.js +0 -116
- package/test/unit/api/__snapshots__/daikinCloud.test.ts.snap +0 -1320
- package/test/unit/api/daikin-api.test.ts +0 -384
- package/test/unit/api/daikin-oauth.test.ts +0 -214
- package/test/unit/api/daikinCloud.test.ts +0 -12
- package/test/unit/config/config-manager.test.ts +0 -271
- package/test/unit/device/daikin-device.test.ts +0 -79
- package/test/unit/services/hot-water-tank.service.test.ts +0 -123
- package/test/unit/utils/error-handler.test.ts +0 -274
- package/test/unit/utils/log-context.test.ts +0 -271
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ErrorHandler Tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {ErrorHandler, ErrorCategory, ErrorSeverity} from '../../../src/utils/error-handler';
|
|
6
|
-
import {Logger} from 'homebridge';
|
|
7
|
-
|
|
8
|
-
describe('ErrorHandler', () => {
|
|
9
|
-
let mockLogger: Logger;
|
|
10
|
-
let handler: ErrorHandler;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
mockLogger = {
|
|
14
|
-
debug: jest.fn(),
|
|
15
|
-
info: jest.fn(),
|
|
16
|
-
warn: jest.fn(),
|
|
17
|
-
error: jest.fn(),
|
|
18
|
-
log: jest.fn(),
|
|
19
|
-
} as unknown as Logger;
|
|
20
|
-
|
|
21
|
-
handler = new ErrorHandler(mockLogger, 'TestContext');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('categorize', () => {
|
|
25
|
-
it('should categorize authentication errors', () => {
|
|
26
|
-
const error = new Error('Unauthorized token expired');
|
|
27
|
-
expect(handler.categorize(error)).toBe(ErrorCategory.AUTHENTICATION);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('should categorize rate limit errors', () => {
|
|
31
|
-
const error = new Error('Rate limit exceeded');
|
|
32
|
-
expect(handler.categorize(error)).toBe(ErrorCategory.RATE_LIMIT);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should categorize network errors', () => {
|
|
36
|
-
const error = new Error('ECONNRESET connection failed');
|
|
37
|
-
expect(handler.categorize(error)).toBe(ErrorCategory.NETWORK);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should categorize websocket errors', () => {
|
|
41
|
-
const error = new Error('ws: connection closed');
|
|
42
|
-
expect(handler.categorize(error)).toBe(ErrorCategory.WEBSOCKET);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should categorize validation errors', () => {
|
|
46
|
-
const error = new Error('Invalid configuration value');
|
|
47
|
-
expect(handler.categorize(error)).toBe(ErrorCategory.VALIDATION);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should categorize device errors', () => {
|
|
51
|
-
const error = new Error('Device not responding');
|
|
52
|
-
expect(handler.categorize(error)).toBe(ErrorCategory.DEVICE);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should categorize unknown errors', () => {
|
|
56
|
-
const error = new Error('Something went wrong');
|
|
57
|
-
expect(handler.categorize(error)).toBe(ErrorCategory.UNKNOWN);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('determineSeverity', () => {
|
|
62
|
-
it('should mark rate limit as warning', () => {
|
|
63
|
-
const error = new Error('Rate limit');
|
|
64
|
-
const severity = handler.determineSeverity(error, ErrorCategory.RATE_LIMIT);
|
|
65
|
-
expect(severity).toBe(ErrorSeverity.WARNING);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('should mark authentication as fatal', () => {
|
|
69
|
-
const error = new Error('Auth failed');
|
|
70
|
-
const severity = handler.determineSeverity(error, ErrorCategory.AUTHENTICATION);
|
|
71
|
-
expect(severity).toBe(ErrorSeverity.FATAL);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should mark configuration as fatal', () => {
|
|
75
|
-
const error = new Error('Config error');
|
|
76
|
-
const severity = handler.determineSeverity(error, ErrorCategory.CONFIGURATION);
|
|
77
|
-
expect(severity).toBe(ErrorSeverity.FATAL);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should mark network as warning', () => {
|
|
81
|
-
const error = new Error('Network error');
|
|
82
|
-
const severity = handler.determineSeverity(error, ErrorCategory.NETWORK);
|
|
83
|
-
expect(severity).toBe(ErrorSeverity.WARNING);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should mark device as error', () => {
|
|
87
|
-
const error = new Error('Device error');
|
|
88
|
-
const severity = handler.determineSeverity(error, ErrorCategory.DEVICE);
|
|
89
|
-
expect(severity).toBe(ErrorSeverity.ERROR);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe('isRetryable', () => {
|
|
94
|
-
it('should mark network errors as retryable', () => {
|
|
95
|
-
const error = new Error('ECONNRESET');
|
|
96
|
-
expect(handler.isRetryable(error)).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should mark timeout errors as retryable', () => {
|
|
100
|
-
const error = new Error('ETIMEDOUT');
|
|
101
|
-
expect(handler.isRetryable(error)).toBe(true);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should mark gateway errors as retryable', () => {
|
|
105
|
-
const error = new Error('Bad gateway');
|
|
106
|
-
expect(handler.isRetryable(error)).toBe(true);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should mark rate limit as retryable', () => {
|
|
110
|
-
const error = new Error('Rate limit exceeded');
|
|
111
|
-
expect(handler.isRetryable(error)).toBe(true);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should not mark validation errors as retryable', () => {
|
|
115
|
-
const error = new Error('Invalid input');
|
|
116
|
-
expect(handler.isRetryable(error)).toBe(false);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('handle', () => {
|
|
121
|
-
it('should handle error and log with context', () => {
|
|
122
|
-
const error = new Error('Test error');
|
|
123
|
-
const context = {
|
|
124
|
-
category: ErrorCategory.API,
|
|
125
|
-
operation: 'testOperation',
|
|
126
|
-
deviceId: 'device123',
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const handled = handler.handle(error, context);
|
|
130
|
-
|
|
131
|
-
expect(handled.message).toBe('Test error');
|
|
132
|
-
expect(handled.context.category).toBe(ErrorCategory.API);
|
|
133
|
-
expect(handled.context.operation).toBe('testOperation');
|
|
134
|
-
expect(handled.context.deviceId).toBe('device123');
|
|
135
|
-
expect(mockLogger.error).toHaveBeenCalled();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should auto-categorize when category not provided', () => {
|
|
139
|
-
const error = new Error('Unauthorized');
|
|
140
|
-
const handled = handler.handle(error);
|
|
141
|
-
|
|
142
|
-
expect(handled.context.category).toBe(ErrorCategory.AUTHENTICATION);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should auto-determine severity', () => {
|
|
146
|
-
const error = new Error('Rate limit');
|
|
147
|
-
const handled = handler.handle(error);
|
|
148
|
-
|
|
149
|
-
expect(handled.context.severity).toBe(ErrorSeverity.WARNING);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should detect retryability', () => {
|
|
153
|
-
const error = new Error('ECONNRESET');
|
|
154
|
-
const handled = handler.handle(error);
|
|
155
|
-
|
|
156
|
-
expect(handled.context.retryable).toBe(true);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should record error in history', () => {
|
|
160
|
-
const error = new Error('Test error');
|
|
161
|
-
handler.handle(error);
|
|
162
|
-
|
|
163
|
-
const history = handler.getRecentErrors(1);
|
|
164
|
-
expect(history).toHaveLength(1);
|
|
165
|
-
expect(history[0].message).toBe('Test error');
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe('handleWithMessage', () => {
|
|
170
|
-
it('should return user-friendly message for authentication', () => {
|
|
171
|
-
const error = new Error('Token expired');
|
|
172
|
-
const message = handler.handleWithMessage(error, {
|
|
173
|
-
category: ErrorCategory.AUTHENTICATION,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
expect(message).toContain('Authentication failed');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should return user-friendly message for rate limit', () => {
|
|
180
|
-
const error = new Error('Too many requests');
|
|
181
|
-
const message = handler.handleWithMessage(error, {
|
|
182
|
-
category: ErrorCategory.RATE_LIMIT,
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
expect(message).toContain('rate limit');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should return user-friendly message for network', () => {
|
|
189
|
-
const error = new Error('Connection failed');
|
|
190
|
-
const message = handler.handleWithMessage(error, {
|
|
191
|
-
category: ErrorCategory.NETWORK,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
expect(message).toContain('Network connection');
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe('getRecentErrors', () => {
|
|
199
|
-
it('should return limited number of recent errors', () => {
|
|
200
|
-
for (let i = 0; i < 10; i++) {
|
|
201
|
-
handler.handle(new Error(`Error ${i}`));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const recent = handler.getRecentErrors(5);
|
|
205
|
-
expect(recent).toHaveLength(5);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('should return all errors when no limit specified', () => {
|
|
209
|
-
for (let i = 0; i < 5; i++) {
|
|
210
|
-
handler.handle(new Error(`Error ${i}`));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const all = handler.getRecentErrors();
|
|
214
|
-
expect(all).toHaveLength(5);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe('getErrorsByCategory', () => {
|
|
219
|
-
it('should filter errors by category', () => {
|
|
220
|
-
handler.handle(new Error('Auth error'), {category: ErrorCategory.AUTHENTICATION});
|
|
221
|
-
handler.handle(new Error('Network error'), {category: ErrorCategory.NETWORK});
|
|
222
|
-
handler.handle(new Error('Another auth error'), {category: ErrorCategory.AUTHENTICATION});
|
|
223
|
-
|
|
224
|
-
const authErrors = handler.getErrorsByCategory(ErrorCategory.AUTHENTICATION);
|
|
225
|
-
expect(authErrors).toHaveLength(2);
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
describe('clearHistory', () => {
|
|
230
|
-
it('should clear all error history', () => {
|
|
231
|
-
handler.handle(new Error('Error 1'));
|
|
232
|
-
handler.handle(new Error('Error 2'));
|
|
233
|
-
expect(handler.getRecentErrors()).toHaveLength(2);
|
|
234
|
-
|
|
235
|
-
handler.clearHistory();
|
|
236
|
-
expect(handler.getRecentErrors()).toHaveLength(0);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
describe('logging', () => {
|
|
241
|
-
it('should log fatal errors with stack trace', () => {
|
|
242
|
-
const error = new Error('Fatal error');
|
|
243
|
-
error.stack = 'Stack trace here';
|
|
244
|
-
|
|
245
|
-
handler.handle(error, {
|
|
246
|
-
category: ErrorCategory.AUTHENTICATION,
|
|
247
|
-
severity: ErrorSeverity.FATAL,
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
expect(mockLogger.error).toHaveBeenCalledTimes(2); // Message + stack
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should log warnings with warn level', () => {
|
|
254
|
-
handler.handle(new Error('Warning'), {
|
|
255
|
-
severity: ErrorSeverity.WARNING,
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
expect(mockLogger.warn).toHaveBeenCalled();
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('should include context in log messages', () => {
|
|
262
|
-
handler.handle(new Error('Test'), {
|
|
263
|
-
category: ErrorCategory.DEVICE,
|
|
264
|
-
deviceId: 'device123',
|
|
265
|
-
operation: 'update',
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
const logCall = (mockLogger.error as jest.Mock).mock.calls[0][0];
|
|
269
|
-
expect(logCall).toContain('DEVICE');
|
|
270
|
-
expect(logCall).toContain('device123');
|
|
271
|
-
expect(logCall).toContain('update');
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
});
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* StructuredLogger Tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
StructuredLogger,
|
|
7
|
-
LogLevel,
|
|
8
|
-
LogCategory,
|
|
9
|
-
createLogger,
|
|
10
|
-
createCategoryLogger,
|
|
11
|
-
createDeviceLogger,
|
|
12
|
-
} from '../../../src/utils/log-context';
|
|
13
|
-
import {Logger} from 'homebridge';
|
|
14
|
-
|
|
15
|
-
describe('StructuredLogger', () => {
|
|
16
|
-
let mockLogger: Logger;
|
|
17
|
-
let logger: StructuredLogger;
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
mockLogger = {
|
|
21
|
-
debug: jest.fn(),
|
|
22
|
-
info: jest.fn(),
|
|
23
|
-
warn: jest.fn(),
|
|
24
|
-
error: jest.fn(),
|
|
25
|
-
log: jest.fn(),
|
|
26
|
-
} as unknown as Logger;
|
|
27
|
-
|
|
28
|
-
logger = new StructuredLogger(mockLogger);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('basic logging', () => {
|
|
32
|
-
it('should log debug messages', () => {
|
|
33
|
-
logger.debug('Debug message');
|
|
34
|
-
expect(mockLogger.debug).toHaveBeenCalledWith('Debug message');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should log info messages', () => {
|
|
38
|
-
logger.info('Info message');
|
|
39
|
-
expect(mockLogger.info).toHaveBeenCalledWith('Info message');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should log warn messages', () => {
|
|
43
|
-
logger.warn('Warning message');
|
|
44
|
-
expect(mockLogger.warn).toHaveBeenCalledWith('Warning message');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should log error messages', () => {
|
|
48
|
-
logger.error('Error message');
|
|
49
|
-
expect(mockLogger.error).toHaveBeenCalledWith('Error message');
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('context formatting', () => {
|
|
54
|
-
it('should include category in log message', () => {
|
|
55
|
-
logger.log(LogLevel.INFO, 'Test', {category: LogCategory.API});
|
|
56
|
-
expect(mockLogger.info).toHaveBeenCalledWith('[API] Test');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should include device name in log message', () => {
|
|
60
|
-
logger.log(LogLevel.INFO, 'Test', {deviceName: 'Living Room'});
|
|
61
|
-
expect(mockLogger.info).toHaveBeenCalledWith('[Living Room] Test');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should include device ID when no name', () => {
|
|
65
|
-
logger.log(LogLevel.INFO, 'Test', {deviceId: 'device123'});
|
|
66
|
-
expect(mockLogger.info).toHaveBeenCalledWith('[Device:device123] Test');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should include operation in log message', () => {
|
|
70
|
-
logger.log(LogLevel.INFO, 'Test', {operation: 'update'});
|
|
71
|
-
expect(mockLogger.info).toHaveBeenCalledWith('(update) Test');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should include embedded ID', () => {
|
|
75
|
-
logger.log(LogLevel.INFO, 'Test', {embeddedId: 'climateControl'});
|
|
76
|
-
expect(mockLogger.info).toHaveBeenCalledWith('[climateControl] Test');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should include feature name', () => {
|
|
80
|
-
logger.log(LogLevel.INFO, 'Test', {feature: 'PowerfulMode'});
|
|
81
|
-
expect(mockLogger.info).toHaveBeenCalledWith('[Feature:PowerfulMode] Test');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should combine multiple context fields', () => {
|
|
85
|
-
logger.log(LogLevel.INFO, 'Test', {
|
|
86
|
-
category: LogCategory.DEVICE,
|
|
87
|
-
deviceName: 'Kitchen',
|
|
88
|
-
operation: 'setState',
|
|
89
|
-
});
|
|
90
|
-
expect(mockLogger.info).toHaveBeenCalledWith('[Device] [Kitchen] (setState) Test');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should append metadata as JSON', () => {
|
|
94
|
-
logger.log(LogLevel.INFO, 'Test', {
|
|
95
|
-
metadata: {temp: 25, mode: 'cool'},
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const logCall = (mockLogger.info as jest.Mock).mock.calls[0][0];
|
|
99
|
-
expect(logCall).toContain('{"temp":25,"mode":"cool"}');
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('default context', () => {
|
|
104
|
-
it('should merge default context with call context', () => {
|
|
105
|
-
const loggerWithDefaults = new StructuredLogger(mockLogger, {
|
|
106
|
-
category: LogCategory.PLATFORM,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
loggerWithDefaults.log(LogLevel.INFO, 'Test', {deviceId: 'device123'});
|
|
110
|
-
|
|
111
|
-
const logCall = (mockLogger.info as jest.Mock).mock.calls[0][0];
|
|
112
|
-
expect(logCall).toContain('[Platform]');
|
|
113
|
-
expect(logCall).toContain('[Device:device123]');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should override default context with call context', () => {
|
|
117
|
-
const loggerWithDefaults = new StructuredLogger(mockLogger, {
|
|
118
|
-
category: LogCategory.PLATFORM,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
loggerWithDefaults.log(LogLevel.INFO, 'Test', {
|
|
122
|
-
category: LogCategory.API,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const logCall = (mockLogger.info as jest.Mock).mock.calls[0][0];
|
|
126
|
-
expect(logCall).toContain('[API]');
|
|
127
|
-
expect(logCall).not.toContain('[Platform]');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should merge metadata objects', () => {
|
|
131
|
-
const loggerWithDefaults = new StructuredLogger(mockLogger, {
|
|
132
|
-
metadata: {defaultKey: 'defaultValue'},
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
loggerWithDefaults.log(LogLevel.INFO, 'Test', {
|
|
136
|
-
metadata: {callKey: 'callValue'},
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const logCall = (mockLogger.info as jest.Mock).mock.calls[0][0];
|
|
140
|
-
expect(logCall).toContain('defaultKey');
|
|
141
|
-
expect(logCall).toContain('callKey');
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('child logger', () => {
|
|
146
|
-
it('should create child logger with additional context', () => {
|
|
147
|
-
const child = logger.child({category: LogCategory.DEVICE});
|
|
148
|
-
child.info('Test message');
|
|
149
|
-
|
|
150
|
-
const logCall = (mockLogger.info as jest.Mock).mock.calls[0][0];
|
|
151
|
-
expect(logCall).toContain('[Device]');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should inherit parent context', () => {
|
|
155
|
-
const parent = new StructuredLogger(mockLogger, {
|
|
156
|
-
category: LogCategory.PLATFORM,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const child = parent.child({deviceName: 'Kitchen'});
|
|
160
|
-
child.info('Test');
|
|
161
|
-
|
|
162
|
-
const logCall = (mockLogger.info as jest.Mock).mock.calls[0][0];
|
|
163
|
-
expect(logCall).toContain('[Platform]');
|
|
164
|
-
expect(logCall).toContain('[Kitchen]');
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('log history', () => {
|
|
169
|
-
it('should record log entries in history', () => {
|
|
170
|
-
logger.info('Message 1');
|
|
171
|
-
logger.warn('Message 2');
|
|
172
|
-
logger.error('Message 3');
|
|
173
|
-
|
|
174
|
-
const history = logger.getHistory();
|
|
175
|
-
expect(history).toHaveLength(3);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('should return limited history', () => {
|
|
179
|
-
for (let i = 0; i < 10; i++) {
|
|
180
|
-
logger.info(`Message ${i}`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const history = logger.getHistory(5);
|
|
184
|
-
expect(history).toHaveLength(5);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('should include context in history entries', () => {
|
|
188
|
-
logger.info('Test', {category: LogCategory.API, deviceId: 'device123'});
|
|
189
|
-
|
|
190
|
-
const history = logger.getHistory();
|
|
191
|
-
expect(history[0].message).toBe('Test');
|
|
192
|
-
expect(history[0].context?.category).toBe(LogCategory.API);
|
|
193
|
-
expect(history[0].context?.deviceId).toBe('device123');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('should include timestamp in history entries', () => {
|
|
197
|
-
const before = new Date();
|
|
198
|
-
logger.info('Test');
|
|
199
|
-
const after = new Date();
|
|
200
|
-
|
|
201
|
-
const history = logger.getHistory();
|
|
202
|
-
expect(history[0].timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
203
|
-
expect(history[0].timestamp.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should clear history', () => {
|
|
207
|
-
logger.info('Message 1');
|
|
208
|
-
logger.info('Message 2');
|
|
209
|
-
expect(logger.getHistory()).toHaveLength(2);
|
|
210
|
-
|
|
211
|
-
logger.clearHistory();
|
|
212
|
-
expect(logger.getHistory()).toHaveLength(0);
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe('helper functions', () => {
|
|
217
|
-
it('createLogger should create StructuredLogger', () => {
|
|
218
|
-
const newLogger = createLogger(mockLogger);
|
|
219
|
-
expect(newLogger).toBeInstanceOf(StructuredLogger);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('createLogger should accept default context', () => {
|
|
223
|
-
const newLogger = createLogger(mockLogger, {category: LogCategory.API});
|
|
224
|
-
newLogger.info('Test');
|
|
225
|
-
|
|
226
|
-
const logCall = (mockLogger.info as jest.Mock).mock.calls[0][0];
|
|
227
|
-
expect(logCall).toContain('[API]');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('createCategoryLogger should create logger with category', () => {
|
|
231
|
-
const newLogger = createCategoryLogger(mockLogger, LogCategory.WEBSOCKET);
|
|
232
|
-
newLogger.info('Test');
|
|
233
|
-
|
|
234
|
-
const logCall = (mockLogger.info as jest.Mock).mock.calls[0][0];
|
|
235
|
-
expect(logCall).toContain('[WebSocket]');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('createDeviceLogger should create logger with device context', () => {
|
|
239
|
-
const newLogger = createDeviceLogger(mockLogger, 'device123', 'Kitchen AC');
|
|
240
|
-
newLogger.info('Test');
|
|
241
|
-
|
|
242
|
-
const logCall = (mockLogger.info as jest.Mock).mock.calls[0][0];
|
|
243
|
-
expect(logCall).toContain('[Device]');
|
|
244
|
-
expect(logCall).toContain('[Kitchen AC]');
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe('edge cases', () => {
|
|
249
|
-
it('should handle empty message', () => {
|
|
250
|
-
logger.info('');
|
|
251
|
-
expect(mockLogger.info).toHaveBeenCalled();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('should handle undefined context', () => {
|
|
255
|
-
logger.info('Test', undefined);
|
|
256
|
-
expect(mockLogger.info).toHaveBeenCalledWith('Test');
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('should handle empty metadata', () => {
|
|
260
|
-
logger.info('Test', {metadata: {}});
|
|
261
|
-
|
|
262
|
-
const logCall = (mockLogger.info as jest.Mock).mock.calls[0][0];
|
|
263
|
-
expect(logCall).toBe('Test');
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('should handle special characters in messages', () => {
|
|
267
|
-
logger.info('Test [brackets] (parens) {braces}');
|
|
268
|
-
expect(mockLogger.info).toHaveBeenCalled();
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
});
|