@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,899 +0,0 @@
|
|
|
1
|
-
# Implementation Guide for Remaining Improvements
|
|
2
|
-
|
|
3
|
-
This document provides detailed implementation guidance for the remaining improvements identified in the code analysis. Use this as a roadmap to complete the enhancement phases.
|
|
4
|
-
|
|
5
|
-
## ✅ Completed Improvements
|
|
6
|
-
|
|
7
|
-
### Phase 1: Foundation & Type Safety
|
|
8
|
-
|
|
9
|
-
#### 1.1 Enable Strict TypeScript Checking ✓
|
|
10
|
-
**Status**: Completed
|
|
11
|
-
**Changes Made:**
|
|
12
|
-
- Enabled `noImplicitAny: true` in [tsconfig.json](tsconfig.json)
|
|
13
|
-
- Fixed 2 implicit `any` type errors in [daikin-cloud.repository.ts](src/api/daikin-cloud.repository.ts)
|
|
14
|
-
- Build passes with strict type checking
|
|
15
|
-
|
|
16
|
-
#### 1.2 Add Data Validation Layer with Zod ✓
|
|
17
|
-
**Status**: Completed
|
|
18
|
-
**Changes Made:**
|
|
19
|
-
- Installed Zod v3.23.8 (compatible with TypeScript 4.4.4)
|
|
20
|
-
- Created [daikin-schemas.ts](src/api/daikin-schemas.ts) with validation schemas for:
|
|
21
|
-
- TokenSet, RateLimitStatus
|
|
22
|
-
- ManagementPoint, GatewayDevice
|
|
23
|
-
- WebSocketDeviceUpdate
|
|
24
|
-
- Configuration validation (DaikinClientConfig, MobileClientConfig, DaikinControllerConfig)
|
|
25
|
-
- Added `validateWithZod()` method to ConfigManager
|
|
26
|
-
- Helper functions: `validateData()` and `safeValidateData()`
|
|
27
|
-
|
|
28
|
-
**Next Steps:**
|
|
29
|
-
- Use Zod schemas in API response parsing
|
|
30
|
-
- Add validation to WebSocket message handling
|
|
31
|
-
- Validate device data before processing
|
|
32
|
-
|
|
33
|
-
#### 1.3 Consolidate WebSocket Update Logic ✓
|
|
34
|
-
**Status**: Completed
|
|
35
|
-
**Changes Made:**
|
|
36
|
-
- Refactored [platform.ts](src/platform.ts) to use [UpdateMapper](src/utils/update-mapper.ts)
|
|
37
|
-
- Removed 180+ lines of duplicated code
|
|
38
|
-
- Single source of truth for WebSocket update handling
|
|
39
|
-
- Improved maintainability and testability
|
|
40
|
-
|
|
41
|
-
#### 1.4 Security Hardening ✓
|
|
42
|
-
**Status**: Completed
|
|
43
|
-
**Changes Made:**
|
|
44
|
-
- Verified token files already use mode 0o600 (secure permissions)
|
|
45
|
-
- Updated GitHub Actions to v4:
|
|
46
|
-
- [.github/workflows/build.yml](.github/workflows/build.yml): actions/checkout@v4, actions/setup-node@v4
|
|
47
|
-
- [.github/workflows/npm-publish.yml](.github/workflows/npm-publish.yml): actions/checkout@v4, actions/setup-node@v4
|
|
48
|
-
|
|
49
|
-
#### 1.5 Create ARCHITECTURE.md ✓
|
|
50
|
-
**Status**: Completed
|
|
51
|
-
**Changes Made:**
|
|
52
|
-
- Created comprehensive [ARCHITECTURE.md](docs/ARCHITECTURE.md) with:
|
|
53
|
-
- System architecture diagram
|
|
54
|
-
- Component documentation
|
|
55
|
-
- Data flow diagrams
|
|
56
|
-
- Configuration schema
|
|
57
|
-
- Security considerations
|
|
58
|
-
- Extension guides
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## 📋 Remaining Improvements
|
|
63
|
-
|
|
64
|
-
### Phase 1 Remaining Tasks
|
|
65
|
-
|
|
66
|
-
#### 1.6 Add Integration Tests for OAuth Flows
|
|
67
|
-
|
|
68
|
-
**Priority**: High
|
|
69
|
-
**Estimated Effort**: 4-6 hours
|
|
70
|
-
**Files to Create/Modify:**
|
|
71
|
-
- `test/integration/oauth-developer-portal.test.ts` (new)
|
|
72
|
-
- `test/integration/oauth-mobile-app.test.ts` (new)
|
|
73
|
-
- `test/mocks/oauth-mock-server.ts` (new)
|
|
74
|
-
|
|
75
|
-
**Implementation Steps:**
|
|
76
|
-
|
|
77
|
-
1. **Create OAuth Mock Server**
|
|
78
|
-
```typescript
|
|
79
|
-
// test/mocks/oauth-mock-server.ts
|
|
80
|
-
import express from 'express';
|
|
81
|
-
|
|
82
|
-
export class OAuthMockServer {
|
|
83
|
-
private app: express.Application;
|
|
84
|
-
private server: any;
|
|
85
|
-
|
|
86
|
-
constructor() {
|
|
87
|
-
this.app = express();
|
|
88
|
-
this.setupRoutes();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private setupRoutes() {
|
|
92
|
-
// Mock authorization endpoint
|
|
93
|
-
this.app.get('/v1/oidc/authorize', (req, res) => {
|
|
94
|
-
const {redirect_uri, state} = req.query;
|
|
95
|
-
res.redirect(`${redirect_uri}?code=mock_code&state=${state}`);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Mock token endpoint
|
|
99
|
-
this.app.post('/v1/oidc/token', (req, res) => {
|
|
100
|
-
res.json({
|
|
101
|
-
access_token: 'mock_access_token',
|
|
102
|
-
refresh_token: 'mock_refresh_token',
|
|
103
|
-
token_type: 'Bearer',
|
|
104
|
-
expires_in: 3600,
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Mock Gigya login
|
|
109
|
-
this.app.post('/accounts.login', (req, res) => {
|
|
110
|
-
res.json({
|
|
111
|
-
id_token: 'mock_gigya_token',
|
|
112
|
-
sessionInfo: {sessionToken: 'mock_session'},
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
start(port: number): Promise<void> {
|
|
118
|
-
return new Promise((resolve) => {
|
|
119
|
-
this.server = this.app.listen(port, () => resolve());
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
stop(): Promise<void> {
|
|
124
|
-
return new Promise((resolve) => {
|
|
125
|
-
this.server.close(() => resolve());
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
2. **Create Developer Portal OAuth Tests**
|
|
132
|
-
```typescript
|
|
133
|
-
// test/integration/oauth-developer-portal.test.ts
|
|
134
|
-
import {DaikinOAuth} from '../../src/api/daikin-oauth';
|
|
135
|
-
import {OAuthMockServer} from '../mocks/oauth-mock-server';
|
|
136
|
-
import fs from 'fs';
|
|
137
|
-
import path from 'path';
|
|
138
|
-
|
|
139
|
-
describe('Developer Portal OAuth Flow', () => {
|
|
140
|
-
let mockServer: OAuthMockServer;
|
|
141
|
-
let tokenFilePath: string;
|
|
142
|
-
|
|
143
|
-
beforeAll(async () => {
|
|
144
|
-
mockServer = new OAuthMockServer();
|
|
145
|
-
await mockServer.start(8585);
|
|
146
|
-
tokenFilePath = path.join(__dirname, '.test-tokenset');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
afterAll(async () => {
|
|
150
|
-
await mockServer.stop();
|
|
151
|
-
if (fs.existsSync(tokenFilePath)) {
|
|
152
|
-
fs.unlinkSync(tokenFilePath);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test('should complete authorization code flow', async () => {
|
|
157
|
-
const oauth = new DaikinOAuth({
|
|
158
|
-
clientId: 'test_client',
|
|
159
|
-
clientSecret: 'test_secret',
|
|
160
|
-
callbackServerExternalAddress: 'localhost',
|
|
161
|
-
callbackServerPort: 8586,
|
|
162
|
-
tokenFilePath,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Test authorization URL generation
|
|
166
|
-
const authUrl = oauth.buildAuthUrl();
|
|
167
|
-
expect(authUrl).toContain('response_type=code');
|
|
168
|
-
expect(authUrl).toContain('client_id=test_client');
|
|
169
|
-
|
|
170
|
-
// Test token exchange
|
|
171
|
-
const tokenSet = await oauth.exchangeCode('mock_code');
|
|
172
|
-
expect(tokenSet.access_token).toBe('mock_access_token');
|
|
173
|
-
expect(tokenSet.refresh_token).toBe('mock_refresh_token');
|
|
174
|
-
|
|
175
|
-
// Verify token was saved
|
|
176
|
-
expect(fs.existsSync(tokenFilePath)).toBe(true);
|
|
177
|
-
const savedToken = JSON.parse(fs.readFileSync(tokenFilePath, 'utf8'));
|
|
178
|
-
expect(savedToken.access_token).toBe('mock_access_token');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test('should refresh expired token', async () => {
|
|
182
|
-
// Implementation
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test('should handle authentication errors', async () => {
|
|
186
|
-
// Implementation
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
3. **Create Mobile App OAuth Tests**
|
|
192
|
-
```typescript
|
|
193
|
-
// test/integration/oauth-mobile-app.test.ts
|
|
194
|
-
import {DaikinMobileOAuth} from '../../src/api/daikin-mobile-oauth';
|
|
195
|
-
|
|
196
|
-
describe('Mobile App OAuth Flow', () => {
|
|
197
|
-
test('should login with email/password', async () => {
|
|
198
|
-
// Implementation
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test('should exchange Gigya token for OIDC token', async () => {
|
|
202
|
-
// Implementation
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test('should handle invalid credentials', async () => {
|
|
206
|
-
// Implementation
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
4. **Add to package.json**
|
|
212
|
-
```json
|
|
213
|
-
{
|
|
214
|
-
"devDependencies": {
|
|
215
|
-
"express": "^4.18.2",
|
|
216
|
-
"@types/express": "^4.17.17"
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
**Testing:**
|
|
222
|
-
```bash
|
|
223
|
-
npm install
|
|
224
|
-
npm run test -- test/integration/oauth-developer-portal.test.ts
|
|
225
|
-
npm run test -- test/integration/oauth-mobile-app.test.ts
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
### Phase 2: Error Handling & Resilience
|
|
231
|
-
|
|
232
|
-
#### 2.1 Implement Error Recovery in Services
|
|
233
|
-
|
|
234
|
-
**Priority**: High
|
|
235
|
-
**Estimated Effort**: 3-4 hours
|
|
236
|
-
**Files to Modify:**
|
|
237
|
-
- [src/services/climate-control.service.ts](src/services/climate-control.service.ts)
|
|
238
|
-
- [src/services/hot-water-tank.service.ts](src/services/hot-water-tank.service.ts)
|
|
239
|
-
|
|
240
|
-
**Implementation Steps:**
|
|
241
|
-
|
|
242
|
-
1. **Create Retry Helper**
|
|
243
|
-
```typescript
|
|
244
|
-
// src/utils/retry.ts
|
|
245
|
-
export async function retryWithBackoff<T>(
|
|
246
|
-
fn: () => Promise<T>,
|
|
247
|
-
options: {
|
|
248
|
-
maxRetries?: number;
|
|
249
|
-
initialDelay?: number;
|
|
250
|
-
maxDelay?: number;
|
|
251
|
-
onRetry?: (attempt: number, error: Error) => void;
|
|
252
|
-
} = {},
|
|
253
|
-
): Promise<T> {
|
|
254
|
-
const {
|
|
255
|
-
maxRetries = 3,
|
|
256
|
-
initialDelay = 1000,
|
|
257
|
-
maxDelay = 10000,
|
|
258
|
-
onRetry,
|
|
259
|
-
} = options;
|
|
260
|
-
|
|
261
|
-
let lastError: Error;
|
|
262
|
-
|
|
263
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
264
|
-
try {
|
|
265
|
-
return await fn();
|
|
266
|
-
} catch (error) {
|
|
267
|
-
lastError = error as Error;
|
|
268
|
-
|
|
269
|
-
if (attempt < maxRetries) {
|
|
270
|
-
const delay = Math.min(
|
|
271
|
-
initialDelay * Math.pow(2, attempt),
|
|
272
|
-
maxDelay,
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
onRetry?.(attempt + 1, lastError);
|
|
276
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
throw lastError!;
|
|
282
|
-
}
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
2. **Update Climate Control Service**
|
|
286
|
-
```typescript
|
|
287
|
-
// In climate-control.service.ts
|
|
288
|
-
import {retryWithBackoff} from '../utils/retry';
|
|
289
|
-
import {ErrorHandler, ErrorSeverity} from '../utils/error-handler';
|
|
290
|
-
|
|
291
|
-
async handleTargetStateSet(value: CharacteristicValue) {
|
|
292
|
-
try {
|
|
293
|
-
const targetState = value as number;
|
|
294
|
-
|
|
295
|
-
await retryWithBackoff(
|
|
296
|
-
async () => {
|
|
297
|
-
const operationMode = this.mapTargetStateToOperationMode(targetState);
|
|
298
|
-
await this.controller.setDeviceData(/* ... */);
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
maxRetries: 3,
|
|
302
|
-
onRetry: (attempt, error) => {
|
|
303
|
-
this.platform.log.warn(
|
|
304
|
-
`[Service] Retry attempt ${attempt} for target state: ${error.message}`,
|
|
305
|
-
);
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
// Success - schedule force update
|
|
311
|
-
this.scheduleForceUpdate();
|
|
312
|
-
|
|
313
|
-
} catch (error) {
|
|
314
|
-
const errorInfo = ErrorHandler.categorizeError(error);
|
|
315
|
-
ErrorHandler.logError(
|
|
316
|
-
this.platform.log,
|
|
317
|
-
'Failed to set target state after retries',
|
|
318
|
-
error,
|
|
319
|
-
errorInfo.severity,
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
// Update characteristic to reflect failure
|
|
323
|
-
this.service.updateCharacteristic(
|
|
324
|
-
this.platform.Characteristic.TargetHeaterCoolerState,
|
|
325
|
-
this.getCurrentTargetState(), // Revert to last known good state
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
throw new this.platform.api.hap.HapStatusError(
|
|
329
|
-
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
3. **Add State Tracking**
|
|
336
|
-
```typescript
|
|
337
|
-
private lastKnownGoodState: {
|
|
338
|
-
active?: number;
|
|
339
|
-
targetState?: number;
|
|
340
|
-
coolingThreshold?: number;
|
|
341
|
-
heatingThreshold?: number;
|
|
342
|
-
} = {};
|
|
343
|
-
|
|
344
|
-
private updateLastKnownGoodState(characteristic: string, value: number) {
|
|
345
|
-
this.lastKnownGoodState[characteristic] = value;
|
|
346
|
-
}
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
#### 2.2 Add WebSocket Resilience Tests
|
|
350
|
-
|
|
351
|
-
**Priority**: Medium
|
|
352
|
-
**Estimated Effort**: 3-4 hours
|
|
353
|
-
**Files to Create:**
|
|
354
|
-
- `test/unit/api/daikin-websocket.test.ts` (new)
|
|
355
|
-
|
|
356
|
-
**Implementation:**
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
// test/unit/api/daikin-websocket.test.ts
|
|
360
|
-
import {DaikinWebSocket} from '../../../src/api/daikin-websocket';
|
|
361
|
-
import {EventEmitter} from 'events';
|
|
362
|
-
import WS from 'ws';
|
|
363
|
-
|
|
364
|
-
describe('DaikinWebSocket', () => {
|
|
365
|
-
let mockServer: WS.Server;
|
|
366
|
-
|
|
367
|
-
beforeEach(() => {
|
|
368
|
-
mockServer = new WS.Server({port: 8587});
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
afterEach(() => {
|
|
372
|
-
mockServer.close();
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
test('should connect and handle messages', async () => {
|
|
376
|
-
const websocket = new DaikinWebSocket(/* ... */);
|
|
377
|
-
|
|
378
|
-
await websocket.connect();
|
|
379
|
-
|
|
380
|
-
// Simulate server message
|
|
381
|
-
mockServer.clients.forEach(client => {
|
|
382
|
-
client.send(JSON.stringify({
|
|
383
|
-
deviceId: 'test-device',
|
|
384
|
-
embeddedId: '0',
|
|
385
|
-
characteristicName: 'onOffMode',
|
|
386
|
-
data: {value: 'on'},
|
|
387
|
-
}));
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
// Assert event was emitted
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
test('should reconnect after disconnect', async () => {
|
|
394
|
-
const websocket = new DaikinWebSocket(/* ... */);
|
|
395
|
-
await websocket.connect();
|
|
396
|
-
|
|
397
|
-
// Simulate disconnect
|
|
398
|
-
mockServer.close();
|
|
399
|
-
|
|
400
|
-
// Wait for reconnection attempt
|
|
401
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
402
|
-
|
|
403
|
-
// Verify reconnection logic was triggered
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
test('should handle malformed messages gracefully', async () => {
|
|
407
|
-
const websocket = new DaikinWebSocket(/* ... */);
|
|
408
|
-
await websocket.connect();
|
|
409
|
-
|
|
410
|
-
mockServer.clients.forEach(client => {
|
|
411
|
-
client.send('invalid json');
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
// Should not crash
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
test('should respect exponential backoff on reconnect', async () => {
|
|
418
|
-
// Test backoff delays: 1s, 2s, 4s, 8s, ...
|
|
419
|
-
});
|
|
420
|
-
});
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
#### 2.3 Create Status UI for Monitoring
|
|
424
|
-
|
|
425
|
-
**Priority**: Medium
|
|
426
|
-
**Estimated Effort**: 6-8 hours
|
|
427
|
-
**Files to Create/Modify:**
|
|
428
|
-
- `homebridge-ui/public/status.html` (new)
|
|
429
|
-
- `homebridge-ui/server.js` (modify)
|
|
430
|
-
|
|
431
|
-
**Implementation:**
|
|
432
|
-
|
|
433
|
-
1. **Create Status Endpoint**
|
|
434
|
-
```javascript
|
|
435
|
-
// homebridge-ui/server.js
|
|
436
|
-
app.get('/status', async (req, res) => {
|
|
437
|
-
try {
|
|
438
|
-
const status = {
|
|
439
|
-
authMode: config.authMode || 'developer_portal',
|
|
440
|
-
authenticated: false,
|
|
441
|
-
apiUsage: {
|
|
442
|
-
limitDay: 0,
|
|
443
|
-
remainingDay: 0,
|
|
444
|
-
limitMinute: 0,
|
|
445
|
-
remainingMinute: 0,
|
|
446
|
-
},
|
|
447
|
-
websocket: {
|
|
448
|
-
connected: false,
|
|
449
|
-
lastConnected: null,
|
|
450
|
-
},
|
|
451
|
-
devices: [],
|
|
452
|
-
lastSync: null,
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
// Read status from plugin
|
|
456
|
-
// (Plugin would need to expose status via file or API)
|
|
457
|
-
|
|
458
|
-
res.json(status);
|
|
459
|
-
} catch (error) {
|
|
460
|
-
res.status(500).json({error: error.message});
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
2. **Create Status UI**
|
|
466
|
-
```html
|
|
467
|
-
<!-- homebridge-ui/public/status.html -->
|
|
468
|
-
<div class="card">
|
|
469
|
-
<h3>Authentication Status</h3>
|
|
470
|
-
<div id="auth-status">
|
|
471
|
-
<span class="badge badge-success">Connected</span>
|
|
472
|
-
<span>Mode: <strong id="auth-mode">Mobile App</strong></span>
|
|
473
|
-
</div>
|
|
474
|
-
</div>
|
|
475
|
-
|
|
476
|
-
<div class="card">
|
|
477
|
-
<h3>API Rate Limit</h3>
|
|
478
|
-
<div class="progress">
|
|
479
|
-
<div id="rate-limit-bar" class="progress-bar" style="width: 75%">
|
|
480
|
-
<span id="rate-limit-text">75% remaining (3750/5000)</span>
|
|
481
|
-
</div>
|
|
482
|
-
</div>
|
|
483
|
-
<small>Resets: <span id="rate-limit-reset">23:59 UTC</span></small>
|
|
484
|
-
</div>
|
|
485
|
-
|
|
486
|
-
<div class="card">
|
|
487
|
-
<h3>WebSocket Connection</h3>
|
|
488
|
-
<div id="websocket-status">
|
|
489
|
-
<span class="badge badge-success">Connected</span>
|
|
490
|
-
<span>Last message: <span id="ws-last-message">2 minutes ago</span></span>
|
|
491
|
-
</div>
|
|
492
|
-
</div>
|
|
493
|
-
|
|
494
|
-
<div class="card">
|
|
495
|
-
<h3>Devices</h3>
|
|
496
|
-
<table class="table">
|
|
497
|
-
<thead>
|
|
498
|
-
<tr>
|
|
499
|
-
<th>Device</th>
|
|
500
|
-
<th>Status</th>
|
|
501
|
-
<th>Last Sync</th>
|
|
502
|
-
<th>Errors</th>
|
|
503
|
-
</tr>
|
|
504
|
-
</thead>
|
|
505
|
-
<tbody id="device-list">
|
|
506
|
-
<!-- Populated by JavaScript -->
|
|
507
|
-
</tbody>
|
|
508
|
-
</table>
|
|
509
|
-
</div>
|
|
510
|
-
|
|
511
|
-
<script>
|
|
512
|
-
async function loadStatus() {
|
|
513
|
-
const response = await fetch('/status');
|
|
514
|
-
const status = await response.json();
|
|
515
|
-
|
|
516
|
-
// Update UI with status
|
|
517
|
-
document.getElementById('auth-mode').textContent = status.authMode;
|
|
518
|
-
// ... etc
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
setInterval(loadStatus, 5000); // Refresh every 5 seconds
|
|
522
|
-
loadStatus();
|
|
523
|
-
</script>
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
|
|
528
|
-
### Phase 3: Documentation & Performance
|
|
529
|
-
|
|
530
|
-
#### 3.1 Add Comprehensive JSDoc Documentation
|
|
531
|
-
|
|
532
|
-
**Priority**: Medium
|
|
533
|
-
**Estimated Effort**: 4-6 hours
|
|
534
|
-
**Files to Modify**: All core classes
|
|
535
|
-
|
|
536
|
-
**Implementation Template:**
|
|
537
|
-
|
|
538
|
-
```typescript
|
|
539
|
-
/**
|
|
540
|
-
* Daikin Cloud Controller
|
|
541
|
-
*
|
|
542
|
-
* Manages authentication, API communication, and device data for Daikin Cloud integration.
|
|
543
|
-
* Supports both Developer Portal (OAuth 2.0) and Mobile App (Gigya) authentication modes.
|
|
544
|
-
*
|
|
545
|
-
* @example
|
|
546
|
-
* ```typescript
|
|
547
|
-
* const controller = new DaikinCloudController({
|
|
548
|
-
* authMode: 'mobile_app',
|
|
549
|
-
* email: 'user@example.com',
|
|
550
|
-
* password: 'password',
|
|
551
|
-
* tokenFilePath: '/path/to/tokens',
|
|
552
|
-
* });
|
|
553
|
-
*
|
|
554
|
-
* await controller.authenticate();
|
|
555
|
-
* await controller.updateAllDeviceData();
|
|
556
|
-
* const devices = controller.getDevices();
|
|
557
|
-
* ```
|
|
558
|
-
*
|
|
559
|
-
* @see {@link DaikinControllerConfig} for configuration options
|
|
560
|
-
* @see {@link DaikinCloudDevice} for device data structure
|
|
561
|
-
*/
|
|
562
|
-
export class DaikinCloudController extends EventEmitter {
|
|
563
|
-
/**
|
|
564
|
-
* Creates a new Daikin Cloud Controller
|
|
565
|
-
*
|
|
566
|
-
* @param config - Controller configuration
|
|
567
|
-
* @param logger - Optional logger instance for debugging
|
|
568
|
-
* @throws {Error} If configuration is invalid
|
|
569
|
-
*/
|
|
570
|
-
constructor(config: DaikinControllerConfig, logger?: Logger) {
|
|
571
|
-
// ...
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Authenticate with Daikin Cloud
|
|
576
|
-
*
|
|
577
|
-
* Initiates authentication flow based on configured auth mode:
|
|
578
|
-
* - Developer Portal: Starts OAuth callback server and returns authorization URL
|
|
579
|
-
* - Mobile App: Automatically completes login and token exchange
|
|
580
|
-
*
|
|
581
|
-
* @returns Promise that resolves when authentication is complete
|
|
582
|
-
* @throws {Error} If authentication fails or credentials are invalid
|
|
583
|
-
* @emits token_update When tokens are refreshed
|
|
584
|
-
* @emits error On authentication failure
|
|
585
|
-
*
|
|
586
|
-
* @example
|
|
587
|
-
* ```typescript
|
|
588
|
-
* try {
|
|
589
|
-
* await controller.authenticate();
|
|
590
|
-
* console.log('Authentication successful');
|
|
591
|
-
* } catch (error) {
|
|
592
|
-
* console.error('Authentication failed:', error);
|
|
593
|
-
* }
|
|
594
|
-
* ```
|
|
595
|
-
*/
|
|
596
|
-
async authenticate(): Promise<void> {
|
|
597
|
-
// ...
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
**Priority Files for JSDoc:**
|
|
603
|
-
1. [src/api/daikin-controller.ts](src/api/daikin-controller.ts)
|
|
604
|
-
2. [src/api/daikin-api.ts](src/api/daikin-api.ts)
|
|
605
|
-
3. [src/api/daikin-websocket.ts](src/api/daikin-websocket.ts)
|
|
606
|
-
4. [src/platform.ts](src/platform.ts)
|
|
607
|
-
5. [src/accessories/base-accessory.ts](src/accessories/base-accessory.ts)
|
|
608
|
-
|
|
609
|
-
#### 3.2 Implement Differential Device Updates
|
|
610
|
-
|
|
611
|
-
**Priority**: Low-Medium
|
|
612
|
-
**Estimated Effort**: 4-5 hours
|
|
613
|
-
**Files to Modify:**
|
|
614
|
-
- [src/api/daikin-device.ts](src/api/daikin-device.ts)
|
|
615
|
-
- [src/platform.ts](src/platform.ts)
|
|
616
|
-
|
|
617
|
-
**Implementation:**
|
|
618
|
-
|
|
619
|
-
```typescript
|
|
620
|
-
// In daikin-device.ts
|
|
621
|
-
export class DaikinCloudDevice {
|
|
622
|
-
private lastUpdate: Date = new Date();
|
|
623
|
-
private dataHash: string = '';
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* Check if device data has changed since last update
|
|
627
|
-
*/
|
|
628
|
-
hasChanges(newData: GatewayDevice): boolean {
|
|
629
|
-
const newHash = this.computeHash(newData);
|
|
630
|
-
return this.dataHash !== newHash;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Compute hash of device data for change detection
|
|
635
|
-
*/
|
|
636
|
-
private computeHash(data: GatewayDevice): string {
|
|
637
|
-
// Use fast-json-stable-stringify or similar
|
|
638
|
-
return crypto
|
|
639
|
-
.createHash('md5')
|
|
640
|
-
.update(JSON.stringify(data))
|
|
641
|
-
.digest('hex');
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Update device data and track last update time
|
|
646
|
-
*/
|
|
647
|
-
updateData(updates: Partial<GatewayDevice>): void {
|
|
648
|
-
this.deviceData = {...this.deviceData, ...updates};
|
|
649
|
-
this.lastUpdate = new Date();
|
|
650
|
-
this.dataHash = this.computeHash(this.deviceData);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* Get time since last update in milliseconds
|
|
655
|
-
*/
|
|
656
|
-
getTimeSinceLastUpdate(): number {
|
|
657
|
-
return Date.now() - this.lastUpdate.getTime();
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
```typescript
|
|
663
|
-
// In platform.ts
|
|
664
|
-
private async updateDevices() {
|
|
665
|
-
if (!this.controller) return;
|
|
666
|
-
|
|
667
|
-
try {
|
|
668
|
-
await this.controller.updateAllDeviceData();
|
|
669
|
-
|
|
670
|
-
const devices = this.controller.getDevices();
|
|
671
|
-
let changedCount = 0;
|
|
672
|
-
|
|
673
|
-
devices.forEach(device => {
|
|
674
|
-
if (device.hasChanges(/* new data */)) {
|
|
675
|
-
changedCount++;
|
|
676
|
-
// Only update accessories for changed devices
|
|
677
|
-
this.updateAccessoriesForDevice(device);
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
this.log.debug(
|
|
682
|
-
`[API Syncing] Updated ${changedCount}/${devices.length} devices with changes`,
|
|
683
|
-
);
|
|
684
|
-
} catch (error) {
|
|
685
|
-
this.log.error(`[API Syncing] Failed to update devices: ${error}`);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
```
|
|
689
|
-
|
|
690
|
-
#### 3.3 Add Device-Level Error Tracking
|
|
691
|
-
|
|
692
|
-
**Priority**: Low
|
|
693
|
-
**Estimated Effort**: 3-4 hours
|
|
694
|
-
**Files to Modify:**
|
|
695
|
-
- [src/api/daikin-device.ts](src/api/daikin-device.ts)
|
|
696
|
-
- [src/accessories/base-accessory.ts](src/accessories/base-accessory.ts)
|
|
697
|
-
|
|
698
|
-
**Implementation:**
|
|
699
|
-
|
|
700
|
-
```typescript
|
|
701
|
-
// In daikin-device.ts
|
|
702
|
-
export interface DeviceError {
|
|
703
|
-
timestamp: Date;
|
|
704
|
-
severity: 'error' | 'warning';
|
|
705
|
-
message: string;
|
|
706
|
-
operation: string;
|
|
707
|
-
retryCount: number;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
export class DaikinCloudDevice {
|
|
711
|
-
private errors: DeviceError[] = [];
|
|
712
|
-
private maxErrors = 10; // Keep last 10 errors
|
|
713
|
-
|
|
714
|
-
addError(error: DeviceError): void {
|
|
715
|
-
this.errors.unshift(error);
|
|
716
|
-
if (this.errors.length > this.maxErrors) {
|
|
717
|
-
this.errors.pop();
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
getErrors(): DeviceError[] {
|
|
722
|
-
return [...this.errors];
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
getRecentErrors(since: Date): DeviceError[] {
|
|
726
|
-
return this.errors.filter(e => e.timestamp >= since);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
clearErrors(): void {
|
|
730
|
-
this.errors = [];
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
hasRecentErrors(): boolean {
|
|
734
|
-
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
735
|
-
return this.getRecentErrors(fiveMinutesAgo).length > 0;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
```typescript
|
|
741
|
-
// In base-accessory.ts
|
|
742
|
-
protected handleError(
|
|
743
|
-
operation: string,
|
|
744
|
-
error: Error,
|
|
745
|
-
retryCount: number = 0,
|
|
746
|
-
): void {
|
|
747
|
-
this.accessory.context.device.addError({
|
|
748
|
-
timestamp: new Date(),
|
|
749
|
-
severity: retryCount >= 3 ? 'error' : 'warning',
|
|
750
|
-
message: error.message,
|
|
751
|
-
operation,
|
|
752
|
-
retryCount,
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
// Log error with device context
|
|
756
|
-
this.platform.log.error(
|
|
757
|
-
`[${this.accessory.displayName}] ${operation} failed: ${error.message}`,
|
|
758
|
-
);
|
|
759
|
-
|
|
760
|
-
// Optionally update a status characteristic
|
|
761
|
-
// this.updateErrorStatusCharacteristic();
|
|
762
|
-
}
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
---
|
|
766
|
-
|
|
767
|
-
## Testing & Verification
|
|
768
|
-
|
|
769
|
-
### Running Tests
|
|
770
|
-
|
|
771
|
-
```bash
|
|
772
|
-
# Run all tests
|
|
773
|
-
npm test
|
|
774
|
-
|
|
775
|
-
# Run with coverage
|
|
776
|
-
npm test -- --coverage
|
|
777
|
-
|
|
778
|
-
# Run specific test file
|
|
779
|
-
npm test -- test/unit/api/daikin-api.test.ts
|
|
780
|
-
|
|
781
|
-
# Watch mode for development
|
|
782
|
-
npm test -- --watch
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
### Build & Lint
|
|
786
|
-
|
|
787
|
-
```bash
|
|
788
|
-
# Build TypeScript
|
|
789
|
-
npm run build
|
|
790
|
-
|
|
791
|
-
# Lint code
|
|
792
|
-
npm run lint
|
|
793
|
-
|
|
794
|
-
# Fix linting issues
|
|
795
|
-
npm run lint -- --fix
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
### Pre-Commit Checklist
|
|
799
|
-
|
|
800
|
-
- [ ] All tests pass
|
|
801
|
-
- [ ] Coverage > 70% for modified files
|
|
802
|
-
- [ ] No linting errors
|
|
803
|
-
- [ ] Build completes successfully
|
|
804
|
-
- [ ] Documentation updated (JSDoc, README)
|
|
805
|
-
- [ ] CHANGELOG.md updated
|
|
806
|
-
|
|
807
|
-
---
|
|
808
|
-
|
|
809
|
-
## Performance Benchmarks
|
|
810
|
-
|
|
811
|
-
### Before Improvements
|
|
812
|
-
- Test coverage: 62%
|
|
813
|
-
- Build time: ~5s
|
|
814
|
-
- Unchecked type assertions: 27
|
|
815
|
-
- Duplicated code: ~200 lines
|
|
816
|
-
- GitHub Actions: Using deprecated v2
|
|
817
|
-
|
|
818
|
-
### After Phase 1
|
|
819
|
-
- Test coverage: 62% (unchanged, tests pending)
|
|
820
|
-
- Build time: ~5s
|
|
821
|
-
- Unchecked type assertions: 2 (explicit `any`)
|
|
822
|
-
- Duplicated code: 0 (consolidated)
|
|
823
|
-
- GitHub Actions: Using v4
|
|
824
|
-
- Validation: Zod schemas added
|
|
825
|
-
|
|
826
|
-
### Target After All Phases
|
|
827
|
-
- Test coverage: > 80%
|
|
828
|
-
- Build time: < 6s
|
|
829
|
-
- Unchecked type assertions: 0
|
|
830
|
-
- Duplicated code: 0
|
|
831
|
-
- Documentation: 100% public APIs
|
|
832
|
-
- Performance: Differential updates reduce CPU usage
|
|
833
|
-
|
|
834
|
-
---
|
|
835
|
-
|
|
836
|
-
## Migration Notes
|
|
837
|
-
|
|
838
|
-
### Breaking Changes
|
|
839
|
-
None of the completed improvements introduce breaking changes.
|
|
840
|
-
|
|
841
|
-
### Configuration Changes
|
|
842
|
-
No configuration changes required. Zod validation is backward-compatible.
|
|
843
|
-
|
|
844
|
-
### Token File Migration
|
|
845
|
-
No migration needed. Token file format unchanged, permissions already secure.
|
|
846
|
-
|
|
847
|
-
---
|
|
848
|
-
|
|
849
|
-
## Troubleshooting Implementation Issues
|
|
850
|
-
|
|
851
|
-
### TypeScript Strict Mode Errors
|
|
852
|
-
If strict mode reveals new errors:
|
|
853
|
-
1. Add explicit types instead of using `any`
|
|
854
|
-
2. Use type guards for runtime validation
|
|
855
|
-
3. Use Zod schemas for external data
|
|
856
|
-
|
|
857
|
-
### Zod Version Compatibility
|
|
858
|
-
Zod v3.23.8 is compatible with TypeScript 4.4.4. Do not upgrade to Zod v4 without upgrading TypeScript to 5.0+.
|
|
859
|
-
|
|
860
|
-
### Test Failures After Refactoring
|
|
861
|
-
If tests fail after consolidating WebSocket logic:
|
|
862
|
-
1. Update test mocks to use UpdateMapper
|
|
863
|
-
2. Verify characteristic mappings match original logic
|
|
864
|
-
3. Check event emission in UpdateMapper tests
|
|
865
|
-
|
|
866
|
-
---
|
|
867
|
-
|
|
868
|
-
## Resources
|
|
869
|
-
|
|
870
|
-
- [Zod Documentation](https://zod.dev/)
|
|
871
|
-
- [Jest Testing Guide](https://jestjs.io/docs/getting-started)
|
|
872
|
-
- [Homebridge Plugin Development](https://developers.homebridge.io/)
|
|
873
|
-
- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
|
|
874
|
-
- [GitHub Actions](https://docs.github.com/en/actions)
|
|
875
|
-
|
|
876
|
-
---
|
|
877
|
-
|
|
878
|
-
## Contributing
|
|
879
|
-
|
|
880
|
-
When implementing improvements:
|
|
881
|
-
1. Create feature branch: `feature/improvement-name`
|
|
882
|
-
2. Implement changes with tests
|
|
883
|
-
3. Update documentation
|
|
884
|
-
4. Submit PR with description linking to this guide
|
|
885
|
-
5. Ensure CI passes
|
|
886
|
-
|
|
887
|
-
---
|
|
888
|
-
|
|
889
|
-
## Next Steps
|
|
890
|
-
|
|
891
|
-
1. **Immediate**: Run test suite to verify completed changes
|
|
892
|
-
2. **Short-term**: Implement OAuth integration tests (Phase 1.6)
|
|
893
|
-
3. **Medium-term**: Add error recovery to services (Phase 2.1)
|
|
894
|
-
4. **Long-term**: Complete JSDoc documentation (Phase 3.1)
|
|
895
|
-
|
|
896
|
-
---
|
|
897
|
-
|
|
898
|
-
*Last Updated: 2026-01-10*
|
|
899
|
-
*Maintained by: Claude Code*
|