@homebridge-plugins/homebridge-eufy-security 4.6.0-beta.28 → 4.6.0-beta.29
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/.claude/skills/new-device-support/SKILL.md +95 -15
- package/.claude/skills/new-device-support/check-device.mjs +363 -0
- package/.claude/skills/new-device-support/map-properties.mjs +99 -10
- package/.claude/skills/new-device-support/verify-device.mjs +272 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: new-device-support
|
|
3
|
-
description: Full workflow to add support for a new Eufy Security device type across eufy-security-client and homebridge-eufy-security. Covers exploration, implementation, build verification, and git/PR creation.
|
|
3
|
+
description: Full workflow to add support for a new Eufy Security device type (cameras, locks, sensors, and other devices) across eufy-security-client and homebridge-eufy-security. Covers exploration, implementation, build verification, and git/PR creation.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Add New Eufy Security Device Support
|
|
@@ -11,13 +11,28 @@ Use `$ARGUMENTS` for the issue URL or device details.
|
|
|
11
11
|
|
|
12
12
|
## Phase 1 — Gather Information
|
|
13
13
|
|
|
14
|
-
1. **Fetch the GitHub issue** (if URL provided) to extract: device name, model number (e.g. T86P2), device type number, raw properties JSON, firmware version, and any reference PRs.
|
|
15
|
-
2. **
|
|
14
|
+
1. **Fetch the GitHub issue** (if URL provided) to extract: device name, model number (e.g. T86P2), device type number, raw properties JSON, firmware version, and any reference PRs. Note: users on recent eufy-security-client versions see a structured "unknown device" debug message that includes raw properties in a format directly usable with the mapping script.
|
|
15
|
+
2. **Check for existing upstream work** — before starting, search for PRs that already add this device:
|
|
16
16
|
```bash
|
|
17
|
-
|
|
17
|
+
gh pr list --repo bropat/eufy-security-client --state all --search "<model> OR <device-type-number>" --limit 10
|
|
18
18
|
```
|
|
19
|
-
|
|
20
|
-
3. **
|
|
19
|
+
If a PR exists: review its diff to see what's already implemented, what's missing (e.g. missing GenericTypeProperty label, incomplete DeviceCommands), and whether it's merged, open, or stale. If merged, the device may only need homebridge-eufy-security side changes. If open but incomplete, coordinate with the PR author or build on their work. If the PR has review comments, check for flagged issues.
|
|
20
|
+
3. **Pre-flight checks** — before proceeding, confirm you have:
|
|
21
|
+
- Device type number (required — cannot proceed without it)
|
|
22
|
+
- Raw device properties JSON (required for accurate property mapping — if missing, ask the user to enable debug logging and re-export diagnostics)
|
|
23
|
+
- Model number / display name (needed for enum naming and documentation)
|
|
24
|
+
- If the device type number is completely unknown (not in any existing code), investigate: check the model number prefix pattern, look at raw properties to infer capabilities (camera properties? lock properties? sensor properties?), and check Eufy's product pages for the model.
|
|
25
|
+
4. **Run the pre-implementation audit**: Before writing any code, run the audit script to see what already exists:
|
|
26
|
+
```bash
|
|
27
|
+
node homebridge-eufy-security/.claude/skills/new-device-support/check-device.mjs <type-number> [--pr-search <model>]
|
|
28
|
+
```
|
|
29
|
+
This checks all 6 registration points in types.ts, classification methods in device.ts, device-images.js, finds the closest existing devices by property overlap, and searches upstream PRs. Use its output to understand the starting point.
|
|
30
|
+
5. **Run the property mapping script**: Save the raw properties JSON to a temp file and run:
|
|
31
|
+
```bash
|
|
32
|
+
node homebridge-eufy-security/.claude/skills/new-device-support/map-properties.mjs /tmp/<device>-raw-props.json --closest
|
|
33
|
+
```
|
|
34
|
+
This maps each raw `param_type` to its `CommandType`/`ParamType` enum name, matching property constants, and which existing device types use them. It also outputs a suggested DeviceProperties block. The `--closest` flag ranks existing devices by property overlap to identify the best base device.
|
|
35
|
+
6. **Ask the user** two questions:
|
|
21
36
|
- Image naming convention (check if images already exist or need renaming)
|
|
22
37
|
- Enum name for the DeviceType (e.g. `CAMERA_4G_S330`)
|
|
23
38
|
|
|
@@ -25,12 +40,32 @@ Use `$ARGUMENTS` for the issue URL or device details.
|
|
|
25
40
|
|
|
26
41
|
Create a detailed plan covering all files that need changes. The plan must be based on the actual raw device properties — never guess which properties a device supports.
|
|
27
42
|
|
|
43
|
+
### Determine device category
|
|
44
|
+
|
|
45
|
+
Before planning file changes, identify the device category — this determines which classification methods and accessory classes apply:
|
|
46
|
+
|
|
47
|
+
- **Camera** (including doorbells, floodlights, indoor cameras, solo cameras, 4G cameras): uses `CameraAccessory` in the plugin. Classification methods: `isCamera()`, and optionally `isDoorbell()`, `isFloodLight()`, `isIndoorCamera()`, `isPanAndTiltCamera()`, `isOutdoorPanAndTiltCamera()`, `isSoloCameras()`, etc. Note: `isSoloCameras()` is a composite method that includes outdoor PTZ, wall light cams, and other standalone camera types.
|
|
48
|
+
- **WallLightCam**: uses `CameraAccessory`. Classification: `isWallLightCam()`. This category is heavily referenced in `station.ts` (50+ references) for livestream, talkback, property handling, and command routing — expect substantial `station.ts` changes.
|
|
49
|
+
- **GarageCamera**: uses `CameraAccessory`. Classification: `isGarageCamera()`. Has dedicated property variants (e.g. `DeviceWatermarkGarageCameraProperty`, `DeviceMotionDetectionSensitivityGarageCameraProperty`). Also has `isGarageCameraBySn()` for serial number matching.
|
|
50
|
+
- **Lock** (BLE, WiFi, WiFi Video variants): uses `LockAccessory`. Classification methods: `isLock()`, `isLockWifi()`, `isLockBle()`, `isLockWifiVideo()`, etc. Locks have many specialized type guard methods (e.g. `isLockWifiT8531()`, `isLockWifiR10()`) — check existing patterns carefully. Locks may require changes in **three additional layers** beyond this skill's primary scope — flag these to the user:
|
|
51
|
+
- `src/mqtt/` — MQTT protocol for lock communication
|
|
52
|
+
- `src/p2p/session.ts` — P2P session initialization uses `Device.isLockWifi()` and `Device.isLockWifiNoFinger()` for lock sequence generation
|
|
53
|
+
- `src/http/station.ts` — lock-specific routing at station level
|
|
54
|
+
- **Sensor** (entry sensor, motion sensor, PIR sensor): uses `EntrySensorAccessory` or `MotionSensorAccessory`. Classification: `isSensor()`, `isEntrySensor()`, `isMotionSensor()`. Note: `isSensor()` is static-only — no public instance method exists.
|
|
55
|
+
- **SmartDrop**: uses `SmartDropAccessory`. Classification: `isSmartDrop()`. Also has `isSmartDropBySn()` for serial number matching.
|
|
56
|
+
- **SmartSafe**: no HomeKit accessory currently. Classification: `isSmartSafe()`.
|
|
57
|
+
- **Tracker/SmartTrack**: no HomeKit accessory currently. Classification: `isSmartTrack()`.
|
|
58
|
+
- **Keypad**: no HomeKit accessory currently. Classification: `isKeyPad()`.
|
|
59
|
+
- **WaterFreezeSensor**: no HomeKit accessory currently. Classification: check for `WATER_FREEZE_SENSOR_*` in DeviceType enum.
|
|
60
|
+
- **Siren**: no HomeKit accessory currently. Classification: check for `SIREN_SENSOR_*` in DeviceType enum.
|
|
61
|
+
- **New category**: if the device doesn't fit any existing category, a new accessory class is needed in `src/accessories/`, a new classification method, and a new `if` block in `register_device()`. Flag this to the user — it's a significantly larger task.
|
|
62
|
+
|
|
28
63
|
### Files to modify in eufy-security-client
|
|
29
64
|
|
|
30
65
|
#### `src/http/types.ts` — 6 locations:
|
|
31
66
|
|
|
32
67
|
1. **DeviceType enum**: Add `ENUM_NAME = <number>, //<model>` in numeric order
|
|
33
|
-
2. **GenericTypeProperty states**: Add `<number>: "<Display Name> (<Model>)"` in numeric order
|
|
68
|
+
2. **GenericTypeProperty `states` field**: Add `<number>: "<Display Name> (<Model>)"` inside the `states` object in numeric order. **WARNING**: This is one of the most commonly forgotten steps. Missing this label causes the device to display as a raw number in downstream UIs (see upstream PR #828 where 5 device types had missing labels).
|
|
34
69
|
3. **DeviceProperties**: Add `[DeviceType.ENUM_NAME]` block. Always starts with `...GenericDeviceProperties`. Map each raw param_type to its corresponding `PropertyName.*` property constant. Base on the closest existing device but only include properties that match the raw data.
|
|
35
70
|
4. **StationProperties**: Add `[DeviceType.ENUM_NAME]` block if device can act as its own station (solo cameras, integrated devices). Use `...BaseStationProperties` plus station-specific properties.
|
|
36
71
|
5. **DeviceCommands**: Add `[DeviceType.ENUM_NAME]` array. Commands depend on device capabilities (livestream, talkback, pan/tilt, download, snooze, preset positions, calibrate, alarm).
|
|
@@ -38,14 +73,24 @@ Create a detailed plan covering all files that need changes. The plan must be ba
|
|
|
38
73
|
|
|
39
74
|
#### `src/http/device.ts` — Classification methods:
|
|
40
75
|
|
|
41
|
-
Add the new device type to all applicable static classification methods.
|
|
76
|
+
Add the new device type to all applicable static classification methods. There are two kinds:
|
|
77
|
+
|
|
78
|
+
**Broad classification methods** (add device to these as applicable):
|
|
42
79
|
- `isCamera()` — if it's a camera/doorbell/floodlight
|
|
43
|
-
- `hasBattery()` — if battery-powered
|
|
80
|
+
- `hasBattery()` — if battery-powered (critical — omitting this means no battery service in HomeKit)
|
|
44
81
|
- `isPanAndTiltCamera()` — if has PTZ
|
|
45
|
-
- `isOutdoorPanAndTiltCamera()` — if outdoor PTZ (
|
|
46
|
-
- `
|
|
82
|
+
- `isOutdoorPanAndTiltCamera()` — if outdoor PTZ (included by `isSoloCameras()`)
|
|
83
|
+
- `isSoloCameras()` — composite method aggregating solo/standalone camera types
|
|
84
|
+
- `isFloodLight()`, `isIndoorCamera()`, `isDoorbell()`, `isWallLightCam()`, `isGarageCamera()` — as applicable
|
|
85
|
+
- `isLock()`, `isLockWifi()`, `isLockBle()`, `isLockWifiNoFinger()` — if it's a lock variant
|
|
86
|
+
- `isSensor()`, `isEntrySensor()`, `isMotionSensor()` — if it's a sensor
|
|
87
|
+
- `isSmartDrop()`, `isSmartSafe()`, `isSmartTrack()`, `isKeyPad()` — as applicable
|
|
88
|
+
|
|
89
|
+
Note: Not all broad methods have public instance counterparts (e.g. `isSensor()` is static-only). Don't create instance methods where none exist for the pattern.
|
|
90
|
+
|
|
91
|
+
**`isSupported()` check**: After adding to `DeviceProperties` map, `Device.isSupported(type)` automatically returns `true` (it checks `DeviceProperties[type] !== undefined`). This is the foundation of device registration — if `DeviceProperties` entry is missing, the device is silently unsupported regardless of all other registrations.
|
|
47
92
|
|
|
48
|
-
Add a **new
|
|
93
|
+
Add a **new dedicated type guard method** (static + instance pair):
|
|
49
94
|
```typescript
|
|
50
95
|
static isNewDevice(type: number): boolean {
|
|
51
96
|
//<Model>
|
|
@@ -57,9 +102,12 @@ public isNewDevice(): boolean {
|
|
|
57
102
|
}
|
|
58
103
|
```
|
|
59
104
|
|
|
60
|
-
Update serial number checks if applicable:
|
|
105
|
+
Update serial number checks if applicable (all are static-only, no instance methods):
|
|
61
106
|
- `isIntegratedDeviceBySn()` — add `sn.startsWith("<model>")` if the device is integrated/standalone
|
|
62
107
|
- `isSoloCameraBySn()` — add `sn.startsWith("<model>")` if it's a solo camera
|
|
108
|
+
- `isSmartDropBySn()` — add if it's a SmartDrop variant
|
|
109
|
+
- `isGarageCameraBySn()` — add if it's a garage camera variant
|
|
110
|
+
- `isFloodlightBySn()` — add if it's a floodlight variant
|
|
63
111
|
|
|
64
112
|
#### `src/http/station.ts`:
|
|
65
113
|
|
|
@@ -67,7 +115,7 @@ Update serial number checks if applicable:
|
|
|
67
115
|
|
|
68
116
|
#### `src/push/service.ts`:
|
|
69
117
|
|
|
70
|
-
- If the device is 4G LTE or needs special push notification handling, expand the normalization block (
|
|
118
|
+
- If the device is 4G LTE or needs special push notification handling, expand the normalization block (line 768) to include the new type guard.
|
|
71
119
|
|
|
72
120
|
#### `docs/supported_devices.md`:
|
|
73
121
|
|
|
@@ -79,6 +127,10 @@ Use `:wrench:` for initial support.
|
|
|
79
127
|
|
|
80
128
|
### Files to modify in homebridge-eufy-security
|
|
81
129
|
|
|
130
|
+
#### `src/platform.ts` — `register_device()`:
|
|
131
|
+
|
|
132
|
+
Verify the new device type is handled by `register_device()`. This method uses independent `if` blocks (not `else if`) to map device types to accessory classes. If the device is a camera, it falls through to the camera path. If it's a lock, sensor, or SmartDrop, it hits those specific checks. If it's a new category that doesn't match any existing check, the device won't get an accessory — flag this to the user.
|
|
133
|
+
|
|
82
134
|
#### `homebridge-ui/public/utils/device-images.js`:
|
|
83
135
|
|
|
84
136
|
Add a case in the `getImage()` switch:
|
|
@@ -95,11 +147,33 @@ case <type_number>: return '<image_large>.png';
|
|
|
95
147
|
|
|
96
148
|
Execute the plan. Key implementation notes:
|
|
97
149
|
|
|
98
|
-
- **Property mapping**: Use the output from `map-properties.mjs` (Phase 1) as the primary guide.
|
|
150
|
+
- **Property mapping**: Use the output from `map-properties.mjs` (Phase 1) as the primary guide. Many property constants have large variant families (e.g. `DeviceMotionDetection*` has 40+ variants, `DeviceFloodlightLight*` has 12+, `DeviceVideoRecordingQuality*` has 11+). When multiple constants match the same `param_type`, pick the variant used by the closest existing device — check the "Used by DeviceTypes" column in the script output and the `--closest` flag results.
|
|
99
151
|
- **Companion custom properties**: Some properties have required companions with `custom_*` keys that never appear in raw device data (they're populated at runtime). The script detects these and marks them with `⚠ companion`. Always include them — omitting a companion breaks functionality silently. Key pairs: `DeviceRTSPStream` → `DeviceRTSPStreamUrl`, `DeviceWifiRSSI` → `DeviceWifiSignalLevel`, `DeviceCellularRSSI` → `DeviceCellularSignalLevel`.
|
|
100
152
|
- **Insert in order**: When adding to enums, switch statements, or `if` chains, maintain numeric ordering by device type number.
|
|
101
153
|
- **Audio recording property**: Different device families use different audio recording property constants (e.g. `DeviceAudioRecordingProperty`, `DeviceAudioRecordingStarlight4gLTEProperty`). Match the closest existing device.
|
|
102
154
|
|
|
155
|
+
### Registration verification checklist
|
|
156
|
+
|
|
157
|
+
After implementing all changes, run the post-implementation verification script:
|
|
158
|
+
```bash
|
|
159
|
+
node homebridge-eufy-security/.claude/skills/new-device-support/verify-device.mjs <ENUM_NAME>
|
|
160
|
+
```
|
|
161
|
+
This automatically checks all required registration points and reports PASS/FAIL:
|
|
162
|
+
|
|
163
|
+
1. `DeviceType` enum definition
|
|
164
|
+
2. `GenericTypeProperty` states
|
|
165
|
+
3. `DeviceProperties` map
|
|
166
|
+
4. `DeviceCommands` map
|
|
167
|
+
5. `StationProperties` map (if device acts as its own station — reported as WARN if missing)
|
|
168
|
+
6. `StationCommands` map (if device has station properties — reported as WARN if missing)
|
|
169
|
+
7. Dedicated type guard method (static + instance) in device.ts
|
|
170
|
+
8. Broad classification methods (isCamera, isLock, etc.) in device.ts
|
|
171
|
+
9. `docs/supported_devices.md` entry (reported as WARN if missing)
|
|
172
|
+
10. `*BySn` serial number methods (reported as WARN if missing — not all devices need these)
|
|
173
|
+
11. device-images.js case in homebridge-eufy-security
|
|
174
|
+
|
|
175
|
+
The script exits with code 1 if any required checks fail. A device type that is defined in the enum but missing from DeviceProperties will silently fall back to GenericDeviceProperties with only ~3 basic properties (name, model, serial). This is the most common implementation error — see upstream issue #853 where LOCK_85V0 was added to the enum but never registered in the lookup maps.
|
|
176
|
+
|
|
103
177
|
## Phase 4 — Build & Lint Verification
|
|
104
178
|
|
|
105
179
|
Run build and lint for both repos. Note: eufy-security-client lint may fail due to a pre-existing `jiti` library issue unrelated to our changes — the TypeScript build succeeding is sufficient validation.
|
|
@@ -126,6 +200,12 @@ Follow CLAUDE.md Git Workflow for commit messages, branch naming, and PR body fo
|
|
|
126
200
|
|
|
127
201
|
1. Branch from current beta: `git checkout -b feat/<device-slug>`
|
|
128
202
|
2. Stage: `homebridge-ui/public/utils/device-images.js` + any added image
|
|
203
|
+
3. PR to current beta branch:
|
|
204
|
+
```bash
|
|
205
|
+
gh pr create --base beta-<current-version> \
|
|
206
|
+
--title "feat: add <Device Name> (<Model>) image" \
|
|
207
|
+
--body-file /tmp/pr-body-<branch>.md
|
|
208
|
+
```
|
|
129
209
|
|
|
130
210
|
### Cross-referencing
|
|
131
211
|
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-device.mjs
|
|
4
|
+
*
|
|
5
|
+
* Pre-implementation audit for a new device type. Checks what already exists
|
|
6
|
+
* in the codebase and what's missing before starting work.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node check-device.mjs <device-type-number> [--pr-search <model>]
|
|
10
|
+
*
|
|
11
|
+
* Example:
|
|
12
|
+
* node check-device.mjs 105
|
|
13
|
+
* node check-device.mjs 203 --pr-search T85V0
|
|
14
|
+
*
|
|
15
|
+
* Output: status report showing what exists, what's missing, closest device,
|
|
16
|
+
* and upstream PR search results.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, existsSync } from "fs";
|
|
20
|
+
import { join, dirname } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { execSync } from "child_process";
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
const CLIENT_ROOT = join(__dirname, "..", "..", "..", "..", "eufy-security-client");
|
|
27
|
+
const HB_ROOT = join(__dirname, "..", "..", "..");
|
|
28
|
+
const TYPES_HTTP = join(CLIENT_ROOT, "src", "http", "types.ts");
|
|
29
|
+
const DEVICE_TS = join(CLIENT_ROOT, "src", "http", "device.ts");
|
|
30
|
+
const STATION_TS = join(CLIENT_ROOT, "src", "http", "station.ts");
|
|
31
|
+
const PUSH_SERVICE = join(CLIENT_ROOT, "src", "push", "service.ts");
|
|
32
|
+
const DEVICE_IMAGES = join(HB_ROOT, "homebridge-ui", "public", "utils", "device-images.js");
|
|
33
|
+
|
|
34
|
+
const typeNumber = parseInt(process.argv[2]);
|
|
35
|
+
if (isNaN(typeNumber)) {
|
|
36
|
+
console.error("Usage: check-device.mjs <device-type-number> [--pr-search <model>]");
|
|
37
|
+
console.error("Example: check-device.mjs 105");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let prSearchTerm = null;
|
|
42
|
+
const prIdx = process.argv.indexOf("--pr-search");
|
|
43
|
+
if (prIdx !== -1 && process.argv[prIdx + 1]) {
|
|
44
|
+
prSearchTerm = process.argv[prIdx + 1];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const httpSource = readFileSync(TYPES_HTTP, "utf-8");
|
|
48
|
+
const deviceSource = readFileSync(DEVICE_TS, "utf-8");
|
|
49
|
+
|
|
50
|
+
// ── Check 1: DeviceType enum ───────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function checkDeviceTypeEnum(source, typeNum) {
|
|
53
|
+
const re = new RegExp(`^\\s*(\\w+)\\s*=\\s*${typeNum}\\s*[,/]`, "m");
|
|
54
|
+
const match = source.match(re);
|
|
55
|
+
return match ? match[1] : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const enumName = checkDeviceTypeEnum(httpSource, typeNumber);
|
|
59
|
+
|
|
60
|
+
// ── Check 2: GenericTypeProperty states ─────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function checkGenericTypePropertyStates(source, typeNum) {
|
|
63
|
+
const gtpMatch = source.match(/export\s+const\s+GenericTypeProperty[\s\S]*?states:\s*\{([\s\S]*?)\}/);
|
|
64
|
+
if (!gtpMatch) return null;
|
|
65
|
+
const statesBody = gtpMatch[1];
|
|
66
|
+
const entryRe = new RegExp(`${typeNum}:\\s*"([^"]+)"`);
|
|
67
|
+
const match = statesBody.match(entryRe);
|
|
68
|
+
return match ? match[1] : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const gtpLabel = checkGenericTypePropertyStates(httpSource, typeNumber);
|
|
72
|
+
|
|
73
|
+
// ── Check 3: DeviceProperties ──────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function checkLookupMap(source, mapName, enumN) {
|
|
76
|
+
if (!enumN) return false;
|
|
77
|
+
const dpStart = source.indexOf(`export const ${mapName}`);
|
|
78
|
+
if (dpStart === -1) return false;
|
|
79
|
+
// Find next export const to bound the search
|
|
80
|
+
const nextExport = source.indexOf("export const ", dpStart + 20);
|
|
81
|
+
const section = source.slice(dpStart, nextExport === -1 ? undefined : nextExport);
|
|
82
|
+
return section.includes(`[DeviceType.${enumN}]`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const inDeviceProperties = checkLookupMap(httpSource, "DeviceProperties", enumName);
|
|
86
|
+
const inStationProperties = checkLookupMap(httpSource, "StationProperties", enumName);
|
|
87
|
+
const inDeviceCommands = checkLookupMap(httpSource, "DeviceCommands", enumName);
|
|
88
|
+
const inStationCommands = checkLookupMap(httpSource, "StationCommands", enumName);
|
|
89
|
+
|
|
90
|
+
// ── Check 4: Classification methods in device.ts ───────────────────────────
|
|
91
|
+
|
|
92
|
+
function findClassificationMethods(source, enumN) {
|
|
93
|
+
if (!enumN) return { inMethods: [], hasTypeGuard: false, hasInstanceMethod: false };
|
|
94
|
+
|
|
95
|
+
const inMethods = [];
|
|
96
|
+
// Find all static isXxx() methods and check if they reference this enum
|
|
97
|
+
const methodRegex = /static\s+(is\w+)\s*\(\s*type:\s*number\s*\)[\s\S]*?\n\s*\}/g;
|
|
98
|
+
let m;
|
|
99
|
+
while ((m = methodRegex.exec(source)) !== null) {
|
|
100
|
+
const methodName = m[1];
|
|
101
|
+
const methodBody = m[0];
|
|
102
|
+
if (methodBody.includes(`DeviceType.${enumN}`)) {
|
|
103
|
+
inMethods.push(methodName);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for dedicated type guard (single-enum return)
|
|
108
|
+
const hasTypeGuard = inMethods.some((name) => {
|
|
109
|
+
const singleCheck = new RegExp(`static\\s+${name}\\s*\\(\\s*type:\\s*number\\s*\\)[^}]*return\\s+DeviceType\\.${enumN}\\s*==\\s*type`, "s");
|
|
110
|
+
return singleCheck.test(source);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// If type guard exists, instance method likely does too
|
|
114
|
+
const hasInstanceMethod = hasTypeGuard;
|
|
115
|
+
|
|
116
|
+
return { inMethods, hasTypeGuard, hasInstanceMethod };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const classInfo = findClassificationMethods(deviceSource, enumName);
|
|
120
|
+
|
|
121
|
+
// ── Check 5: device-images.js ──────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function checkDeviceImages(typeNum) {
|
|
124
|
+
if (!existsSync(DEVICE_IMAGES)) return null;
|
|
125
|
+
const source = readFileSync(DEVICE_IMAGES, "utf-8");
|
|
126
|
+
const re = new RegExp(`case\\s+${typeNum}:\\s*return\\s+'([^']+)'`);
|
|
127
|
+
const match = source.match(re);
|
|
128
|
+
return match ? match[1] : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const imageName = checkDeviceImages(typeNumber);
|
|
132
|
+
|
|
133
|
+
// ── Check 6: station.ts integration ─────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
function checkStationIntegration(enumN) {
|
|
136
|
+
if (!enumN || !existsSync(STATION_TS)) return { referenced: false, methods: [] };
|
|
137
|
+
const source = readFileSync(STATION_TS, "utf-8");
|
|
138
|
+
const referenced = source.includes(`DeviceType.${enumN}`);
|
|
139
|
+
// Also check via classification methods
|
|
140
|
+
const methods = [];
|
|
141
|
+
const methodRefs = source.match(/Device\.(is\w+)\(/g);
|
|
142
|
+
if (methodRefs) {
|
|
143
|
+
const uniqueMethods = [...new Set(methodRefs.map((m) => m.match(/Device\.(is\w+)/)[1]))];
|
|
144
|
+
for (const method of uniqueMethods) {
|
|
145
|
+
const re = new RegExp(`static\\s+${method}\\s*\\(\\s*type:\\s*number\\s*\\)[\\s\\S]*?\\n\\s*\\}`, "g");
|
|
146
|
+
const match = deviceSource.match(re);
|
|
147
|
+
if (match && match[0].includes(`DeviceType.${enumN}`)) {
|
|
148
|
+
methods.push(method);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { referenced, methods };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const stationInfo = checkStationIntegration(enumName);
|
|
156
|
+
|
|
157
|
+
// ── Check 6b: push/service.ts normalization ─────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function checkPushService(enumN) {
|
|
160
|
+
if (!enumN || !existsSync(PUSH_SERVICE)) return false;
|
|
161
|
+
const source = readFileSync(PUSH_SERVICE, "utf-8");
|
|
162
|
+
return source.includes(`DeviceType.${enumN}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const inPushService = checkPushService(enumName);
|
|
166
|
+
|
|
167
|
+
// ── Check 7: Find closest device by property overlap ───────────────────────
|
|
168
|
+
|
|
169
|
+
function findClosestDevice(source, enumN) {
|
|
170
|
+
if (!enumN || !inDeviceProperties) return null;
|
|
171
|
+
|
|
172
|
+
// Parse DeviceProperties section
|
|
173
|
+
const dpStart = source.indexOf("export const DeviceProperties");
|
|
174
|
+
const dpEnd = source.indexOf("export const StationProperties");
|
|
175
|
+
if (dpStart === -1) return null;
|
|
176
|
+
const dpSection = source.slice(dpStart, dpEnd === -1 ? undefined : dpEnd);
|
|
177
|
+
|
|
178
|
+
// Extract property sets for each device
|
|
179
|
+
const deviceProps = new Map();
|
|
180
|
+
const blockRegex = /\[DeviceType\.(\w+)\]\s*:\s*\{([\s\S]*?)\}/g;
|
|
181
|
+
let m;
|
|
182
|
+
while ((m = blockRegex.exec(dpSection)) !== null) {
|
|
183
|
+
const dt = m[1];
|
|
184
|
+
const body = m[2];
|
|
185
|
+
const props = new Set();
|
|
186
|
+
const propRef = /\[PropertyName\.(\w+)\]/g;
|
|
187
|
+
let pm;
|
|
188
|
+
while ((pm = propRef.exec(body)) !== null) {
|
|
189
|
+
props.add(pm[1]);
|
|
190
|
+
}
|
|
191
|
+
deviceProps.set(dt, props);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const targetProps = deviceProps.get(enumN);
|
|
195
|
+
if (!targetProps || targetProps.size === 0) return null;
|
|
196
|
+
|
|
197
|
+
// Jaccard similarity
|
|
198
|
+
const similarities = [];
|
|
199
|
+
for (const [dt, props] of deviceProps) {
|
|
200
|
+
if (dt === enumN) continue;
|
|
201
|
+
const intersection = new Set([...targetProps].filter((p) => props.has(p)));
|
|
202
|
+
const union = new Set([...targetProps, ...props]);
|
|
203
|
+
const jaccard = union.size > 0 ? intersection.size / union.size : 0;
|
|
204
|
+
if (jaccard > 0.3) {
|
|
205
|
+
similarities.push({
|
|
206
|
+
device: dt,
|
|
207
|
+
jaccard: Math.round(jaccard * 100),
|
|
208
|
+
shared: intersection.size,
|
|
209
|
+
total: union.size,
|
|
210
|
+
targetOnly: [...targetProps].filter((p) => !props.has(p)).length,
|
|
211
|
+
otherOnly: [...props].filter((p) => !targetProps.has(p)).length,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
similarities.sort((a, b) => b.jaccard - a.jaccard);
|
|
217
|
+
return similarities.slice(0, 5);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const closestDevices = findClosestDevice(httpSource, enumName);
|
|
221
|
+
|
|
222
|
+
// ── Check 7: Upstream PR search ────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
// Sanitize shell arguments to prevent command injection
|
|
225
|
+
function shellEscape(str) {
|
|
226
|
+
return "'" + String(str).replace(/'/g, "'\\''") + "'";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function searchUpstreamPRs(searchTerm) {
|
|
230
|
+
if (!searchTerm) return null;
|
|
231
|
+
try {
|
|
232
|
+
const result = execSync(
|
|
233
|
+
`gh pr list --repo bropat/eufy-security-client --state all --search ${shellEscape(searchTerm)} --limit 5 --json number,title,state,mergedAt 2>/dev/null`,
|
|
234
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
235
|
+
);
|
|
236
|
+
return JSON.parse(result);
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Also search by type number (typeNum is validated as integer, but escape for safety)
|
|
243
|
+
function searchByTypeNumber(typeNum) {
|
|
244
|
+
try {
|
|
245
|
+
const result = execSync(
|
|
246
|
+
`gh pr list --repo bropat/eufy-security-client --state all --search ${shellEscape(String(typeNum))} --limit 5 --json number,title,state,mergedAt 2>/dev/null`,
|
|
247
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
248
|
+
);
|
|
249
|
+
return JSON.parse(result);
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const prsByModel = prSearchTerm ? searchUpstreamPRs(prSearchTerm) : null;
|
|
256
|
+
const prsByType = searchByTypeNumber(typeNumber);
|
|
257
|
+
|
|
258
|
+
// ── Output ─────────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
const W = 80;
|
|
261
|
+
const PASS = "FOUND";
|
|
262
|
+
const FAIL = "MISSING";
|
|
263
|
+
const NA = "N/A";
|
|
264
|
+
|
|
265
|
+
console.log("=".repeat(W));
|
|
266
|
+
console.log(`Device Type Audit: ${typeNumber}${enumName ? ` (${enumName})` : ""}`);
|
|
267
|
+
console.log("=".repeat(W));
|
|
268
|
+
console.log();
|
|
269
|
+
|
|
270
|
+
// Registration status
|
|
271
|
+
console.log("REGISTRATION STATUS");
|
|
272
|
+
console.log("-".repeat(W));
|
|
273
|
+
const checks = [
|
|
274
|
+
["DeviceType enum", enumName ? `${PASS} — ${enumName}` : FAIL],
|
|
275
|
+
["GenericTypeProperty states", gtpLabel ? `${PASS} — "${gtpLabel}"` : FAIL],
|
|
276
|
+
["DeviceProperties map", inDeviceProperties ? PASS : FAIL],
|
|
277
|
+
["StationProperties map", inStationProperties ? PASS : `${FAIL} (may not be needed)`],
|
|
278
|
+
["DeviceCommands map", inDeviceCommands ? PASS : FAIL],
|
|
279
|
+
["StationCommands map", inStationCommands ? PASS : `${FAIL} (may not be needed)`],
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
for (const [label, status] of checks) {
|
|
283
|
+
const icon = status.startsWith(PASS) ? "+" : status === NA ? " " : "-";
|
|
284
|
+
console.log(` [${icon}] ${label.padEnd(30)} ${status}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log();
|
|
288
|
+
|
|
289
|
+
// Classification methods
|
|
290
|
+
console.log("CLASSIFICATION METHODS");
|
|
291
|
+
console.log("-".repeat(W));
|
|
292
|
+
if (classInfo.inMethods.length > 0) {
|
|
293
|
+
console.log(` Included in ${classInfo.inMethods.length} method(s):`);
|
|
294
|
+
for (const m of classInfo.inMethods) {
|
|
295
|
+
console.log(` - ${m}()`);
|
|
296
|
+
}
|
|
297
|
+
console.log(` Dedicated type guard: ${classInfo.hasTypeGuard ? "YES" : "NO"}`);
|
|
298
|
+
} else if (enumName) {
|
|
299
|
+
console.log(" Not included in any classification methods");
|
|
300
|
+
} else {
|
|
301
|
+
console.log(" Cannot check — device type not in enum");
|
|
302
|
+
}
|
|
303
|
+
console.log();
|
|
304
|
+
|
|
305
|
+
// Device images
|
|
306
|
+
console.log("PLUGIN UI");
|
|
307
|
+
console.log("-".repeat(W));
|
|
308
|
+
console.log(` device-images.js: ${imageName ? `${PASS} — ${imageName}` : FAIL}`);
|
|
309
|
+
console.log();
|
|
310
|
+
|
|
311
|
+
// Station & push integration
|
|
312
|
+
console.log("INTEGRATION POINTS");
|
|
313
|
+
console.log("-".repeat(W));
|
|
314
|
+
if (stationInfo.referenced) {
|
|
315
|
+
console.log(` station.ts: directly referenced`);
|
|
316
|
+
} else if (stationInfo.methods.length > 0) {
|
|
317
|
+
console.log(` station.ts: indirectly via ${stationInfo.methods.join(", ")}()`);
|
|
318
|
+
} else {
|
|
319
|
+
console.log(` station.ts: not referenced (may need integration for standalone/integrated devices)`);
|
|
320
|
+
}
|
|
321
|
+
console.log(` push/service.ts: ${inPushService ? "referenced (has push normalization)" : "not referenced (normal for most devices)"}`);
|
|
322
|
+
console.log();
|
|
323
|
+
|
|
324
|
+
// Closest device
|
|
325
|
+
if (closestDevices && closestDevices.length > 0) {
|
|
326
|
+
console.log("CLOSEST EXISTING DEVICES (by property overlap)");
|
|
327
|
+
console.log("-".repeat(W));
|
|
328
|
+
console.log(" Device".padEnd(40) + "Similarity Shared Target-only Other-only");
|
|
329
|
+
for (const d of closestDevices) {
|
|
330
|
+
console.log(
|
|
331
|
+
` ${d.device.padEnd(38)} ${String(d.jaccard + "%").padEnd(11)} ${String(d.shared).padEnd(7)} ${String(d.targetOnly).padEnd(12)} ${d.otherOnly}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
console.log();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Upstream PRs
|
|
338
|
+
const allPRs = new Map();
|
|
339
|
+
if (prsByModel) for (const pr of prsByModel) allPRs.set(pr.number, pr);
|
|
340
|
+
if (prsByType) for (const pr of prsByType) allPRs.set(pr.number, pr);
|
|
341
|
+
|
|
342
|
+
if (allPRs.size > 0) {
|
|
343
|
+
console.log("UPSTREAM PRs (bropat/eufy-security-client)");
|
|
344
|
+
console.log("-".repeat(W));
|
|
345
|
+
for (const pr of allPRs.values()) {
|
|
346
|
+
const state = pr.mergedAt ? "MERGED" : pr.state;
|
|
347
|
+
console.log(` #${pr.number} [${state}] ${pr.title}`);
|
|
348
|
+
}
|
|
349
|
+
console.log();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Summary
|
|
353
|
+
console.log("=".repeat(W));
|
|
354
|
+
const missing = checks.filter(([, s]) => s.startsWith(FAIL) && !s.includes("may not")).length;
|
|
355
|
+
const total = 4; // Required: enum, GTP, DeviceProperties, DeviceCommands
|
|
356
|
+
if (missing === 0) {
|
|
357
|
+
console.log("STATUS: All required registrations present. Device may only need homebridge-side work.");
|
|
358
|
+
} else if (missing === total) {
|
|
359
|
+
console.log("STATUS: Device is completely new. Full implementation needed across both repos.");
|
|
360
|
+
} else {
|
|
361
|
+
console.log(`STATUS: Partially implemented. ${missing} required registration(s) missing.`);
|
|
362
|
+
}
|
|
363
|
+
console.log("=".repeat(W));
|
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
* to the corresponding property constants in eufy-security-client.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* node map-properties.mjs <raw-properties.json>
|
|
10
|
-
* cat raw.json | node map-properties.mjs
|
|
9
|
+
* node map-properties.mjs <raw-properties.json> [--closest]
|
|
10
|
+
* cat raw.json | node map-properties.mjs [--closest]
|
|
11
11
|
*
|
|
12
12
|
* The input JSON should be the rawProperties object from a device dump,
|
|
13
13
|
* e.g. { "1101": { "value": 100 }, "1013": { "value": 1 }, ... }
|
|
14
14
|
* or an array of { "param_type": 1101, "param_value": "..." } objects.
|
|
15
15
|
*
|
|
16
|
+
* Options:
|
|
17
|
+
* --closest Find the closest existing devices by Jaccard similarity
|
|
18
|
+
* on resolved PropertyName overlap. Useful to identify which
|
|
19
|
+
* existing device to base the DeviceProperties block on.
|
|
20
|
+
*
|
|
16
21
|
* Output: a table mapping each param_type to its CommandType/ParamType enum name
|
|
17
22
|
* and all property constants that use that key.
|
|
18
23
|
*/
|
|
@@ -122,14 +127,17 @@ function parsePropertyConstants(source) {
|
|
|
122
127
|
props.push({ constName, key, keyRaw, propertyName, spreadFrom });
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
// Resolve spreads:
|
|
130
|
+
// Resolve spreads: inherit missing fields from parent property
|
|
126
131
|
const byName = new Map(props.map((p) => [p.constName, p]));
|
|
127
132
|
for (const prop of props) {
|
|
128
|
-
if (prop.
|
|
133
|
+
if (prop.spreadFrom) {
|
|
129
134
|
const parent = byName.get(prop.spreadFrom);
|
|
130
135
|
if (parent) {
|
|
131
|
-
prop.key
|
|
132
|
-
|
|
136
|
+
if (prop.key === null) {
|
|
137
|
+
prop.key = parent.key;
|
|
138
|
+
if (!prop.keyRaw) prop.keyRaw = `(spread from ${prop.spreadFrom}) ${parent.keyRaw}`;
|
|
139
|
+
}
|
|
140
|
+
// Always inherit propertyName from parent when missing, even if key is overridden
|
|
133
141
|
if (!prop.propertyName) prop.propertyName = parent.propertyName;
|
|
134
142
|
}
|
|
135
143
|
}
|
|
@@ -182,7 +190,7 @@ const propUsage = parseDevicePropertiesUsage(httpSource);
|
|
|
182
190
|
// ── Step 4: Read and parse input JSON ──────────────────────────────────────
|
|
183
191
|
|
|
184
192
|
let inputData;
|
|
185
|
-
const inputFile = process.argv
|
|
193
|
+
const inputFile = process.argv.slice(2).find((a) => !a.startsWith("-"));
|
|
186
194
|
if (inputFile) {
|
|
187
195
|
inputData = readFileSync(inputFile, "utf-8");
|
|
188
196
|
} else {
|
|
@@ -332,9 +340,12 @@ console.log("=".repeat(80));
|
|
|
332
340
|
const seen = new Set();
|
|
333
341
|
const companionsAdded = [];
|
|
334
342
|
for (const m of matched) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
343
|
+
// Use original props lookup to get correct constName → propertyName mapping
|
|
344
|
+
// (the deduplicated m.propertyNames array loses the index association with m.constNames)
|
|
345
|
+
const propsForKey = keyToProps.get(m.paramType) || [];
|
|
346
|
+
for (const prop of propsForKey) {
|
|
347
|
+
const pn = prop.propertyName;
|
|
348
|
+
const cn = prop.constName;
|
|
338
349
|
if (pn && !seen.has(pn)) {
|
|
339
350
|
seen.add(pn);
|
|
340
351
|
console.log(` [${pn}]: ${cn},`);
|
|
@@ -359,3 +370,81 @@ if (companionsAdded.length > 0) {
|
|
|
359
370
|
console.log(` ${c.constName}: ${c.reason}`);
|
|
360
371
|
}
|
|
361
372
|
}
|
|
373
|
+
|
|
374
|
+
// ── Step 7: Closest device detection (--closest flag) ───────────────────────
|
|
375
|
+
|
|
376
|
+
if (process.argv.includes("--closest")) {
|
|
377
|
+
// Collect the set of PropertyName values from the matched raw properties
|
|
378
|
+
const targetPropNames = new Set();
|
|
379
|
+
for (const m of matched) {
|
|
380
|
+
for (const pn of m.propertyNames) {
|
|
381
|
+
if (pn) targetPropNames.add(pn);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (targetPropNames.size === 0) {
|
|
386
|
+
console.log("\nCannot compute closest device — no PropertyName matches found.");
|
|
387
|
+
} else {
|
|
388
|
+
// Parse DeviceProperties blocks to get property sets per device type
|
|
389
|
+
const dpStart = httpSource.indexOf("export const DeviceProperties");
|
|
390
|
+
const dpEnd = httpSource.indexOf("export const StationProperties");
|
|
391
|
+
if (dpStart !== -1) {
|
|
392
|
+
const dpSection = httpSource.slice(dpStart, dpEnd === -1 ? undefined : dpEnd);
|
|
393
|
+
|
|
394
|
+
const devicePropSets = new Map(); // DeviceType name → Set of PropertyName references
|
|
395
|
+
const blockRegex = /\[DeviceType\.(\w+)\]\s*:\s*\{([\s\S]*?)\}/g;
|
|
396
|
+
let bm;
|
|
397
|
+
while ((bm = blockRegex.exec(dpSection)) !== null) {
|
|
398
|
+
const dt = bm[1];
|
|
399
|
+
const body = bm[2];
|
|
400
|
+
const props = new Set();
|
|
401
|
+
// Extract property constants used, then resolve to PropertyName
|
|
402
|
+
const refRegex = /:\s*(\w+Property)/g;
|
|
403
|
+
let rm;
|
|
404
|
+
while ((rm = refRegex.exec(body)) !== null) {
|
|
405
|
+
const constName = rm[1];
|
|
406
|
+
const prop = allProps.find((p) => p.constName === constName);
|
|
407
|
+
if (prop && prop.propertyName) props.add(prop.propertyName);
|
|
408
|
+
}
|
|
409
|
+
devicePropSets.set(dt, props);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Jaccard similarity
|
|
413
|
+
const similarities = [];
|
|
414
|
+
for (const [dt, props] of devicePropSets) {
|
|
415
|
+
const intersection = new Set([...targetPropNames].filter((p) => props.has(p)));
|
|
416
|
+
const union = new Set([...targetPropNames, ...props]);
|
|
417
|
+
const jaccard = union.size > 0 ? intersection.size / union.size : 0;
|
|
418
|
+
if (jaccard > 0.2) {
|
|
419
|
+
similarities.push({
|
|
420
|
+
device: dt,
|
|
421
|
+
jaccard: Math.round(jaccard * 100),
|
|
422
|
+
shared: intersection.size,
|
|
423
|
+
targetOnly: [...targetPropNames].filter((p) => !props.has(p)).length,
|
|
424
|
+
otherOnly: [...props].filter((p) => !targetPropNames.has(p)).length,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
similarities.sort((a, b) => b.jaccard - a.jaccard);
|
|
430
|
+
const top = similarities.slice(0, 8);
|
|
431
|
+
|
|
432
|
+
console.log();
|
|
433
|
+
console.log("=".repeat(80));
|
|
434
|
+
console.log("CLOSEST EXISTING DEVICES (by property overlap with raw data)");
|
|
435
|
+
console.log("=".repeat(80));
|
|
436
|
+
if (top.length === 0) {
|
|
437
|
+
console.log(" No devices with >20% property overlap found.");
|
|
438
|
+
} else {
|
|
439
|
+
console.log(" " + "Device".padEnd(38) + "Similarity Shared Target-only Other-only");
|
|
440
|
+
for (const d of top) {
|
|
441
|
+
console.log(
|
|
442
|
+
` ${d.device.padEnd(38)} ${String(d.jaccard + "%").padEnd(11)} ${String(d.shared).padEnd(7)} ${String(d.targetOnly).padEnd(12)} ${d.otherOnly}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
console.log();
|
|
446
|
+
console.log(` Recommended base: ${top[0].device} (${top[0].jaccard}% overlap)`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-device.mjs
|
|
4
|
+
*
|
|
5
|
+
* Post-implementation verification script. Checks that a device type is fully
|
|
6
|
+
* wired into all required registration points after implementation.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node verify-device.mjs <ENUM_NAME>
|
|
10
|
+
*
|
|
11
|
+
* Example:
|
|
12
|
+
* node verify-device.mjs CAMERA_4G_S330
|
|
13
|
+
* node verify-device.mjs INDOOR_PT_CAMERA_E30
|
|
14
|
+
*
|
|
15
|
+
* Output: PASS/FAIL per check, with a final summary.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, existsSync } from "fs";
|
|
19
|
+
import { join, dirname } from "path";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
|
|
24
|
+
const CLIENT_ROOT = join(__dirname, "..", "..", "..", "..", "eufy-security-client");
|
|
25
|
+
const HB_ROOT = join(__dirname, "..", "..", "..");
|
|
26
|
+
const TYPES_HTTP = join(CLIENT_ROOT, "src", "http", "types.ts");
|
|
27
|
+
const DEVICE_TS = join(CLIENT_ROOT, "src", "http", "device.ts");
|
|
28
|
+
const DEVICE_IMAGES = join(HB_ROOT, "homebridge-ui", "public", "utils", "device-images.js");
|
|
29
|
+
const SUPPORTED_DEVICES = join(CLIENT_ROOT, "docs", "supported_devices.md");
|
|
30
|
+
|
|
31
|
+
const enumName = process.argv[2];
|
|
32
|
+
if (!enumName || enumName.startsWith("-")) {
|
|
33
|
+
console.error("Usage: verify-device.mjs <ENUM_NAME>");
|
|
34
|
+
console.error("Example: verify-device.mjs CAMERA_4G_S330");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const httpSource = readFileSync(TYPES_HTTP, "utf-8");
|
|
39
|
+
const deviceSource = readFileSync(DEVICE_TS, "utf-8");
|
|
40
|
+
|
|
41
|
+
// ── Extract type number from enum ───────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function getTypeNumber(source, enumN) {
|
|
44
|
+
const re = new RegExp(`^\\s*${enumN}\\s*=\\s*(\\d+)`, "m");
|
|
45
|
+
const match = source.match(re);
|
|
46
|
+
return match ? parseInt(match[1]) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const typeNumber = getTypeNumber(httpSource, enumName);
|
|
50
|
+
|
|
51
|
+
// ── Check functions ─────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function checkEnumExists(source, enumN) {
|
|
54
|
+
const re = new RegExp(`^\\s*${enumN}\\s*=\\s*\\d+`, "m");
|
|
55
|
+
return re.test(source);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkGenericTypePropertyStates(source, typeNum) {
|
|
59
|
+
if (!typeNum) return { pass: false, detail: "no type number" };
|
|
60
|
+
const gtpMatch = source.match(/export\s+const\s+GenericTypeProperty[\s\S]*?states:\s*\{([\s\S]*?)\}/);
|
|
61
|
+
if (!gtpMatch) return { pass: false, detail: "GenericTypeProperty not found" };
|
|
62
|
+
const re = new RegExp(`${typeNum}:\\s*"`);
|
|
63
|
+
const found = re.test(gtpMatch[1]);
|
|
64
|
+
return { pass: found, detail: found ? "label present" : "missing label entry" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function checkLookupMap(source, mapName, enumN) {
|
|
68
|
+
const dpStart = source.indexOf(`export const ${mapName}`);
|
|
69
|
+
if (dpStart === -1) return { pass: false, detail: `${mapName} not found` };
|
|
70
|
+
const nextExport = source.indexOf("export const ", dpStart + 20);
|
|
71
|
+
const section = source.slice(dpStart, nextExport === -1 ? undefined : nextExport);
|
|
72
|
+
const found = section.includes(`[DeviceType.${enumN}]`);
|
|
73
|
+
return { pass: found, detail: found ? "entry present" : "missing entry" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkTypeGuard(source, enumN) {
|
|
77
|
+
// Look for a static method that returns DeviceType.ENUM == type
|
|
78
|
+
const re = new RegExp(`static\\s+(is\\w+)\\s*\\(\\s*type:\\s*number\\s*\\)[^}]*DeviceType\\.${enumN}`, "g");
|
|
79
|
+
const methods = [];
|
|
80
|
+
let m;
|
|
81
|
+
while ((m = re.exec(source)) !== null) {
|
|
82
|
+
methods.push(m[1]);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
pass: methods.length > 0,
|
|
86
|
+
detail: methods.length > 0 ? `found: ${methods.join(", ")}()` : "no type guard method references this enum",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkInstanceMethod(source, enumN) {
|
|
91
|
+
// Find methods from the type guard check, then look for matching instance methods
|
|
92
|
+
const staticRe = new RegExp(`static\\s+(is\\w+)\\s*\\(\\s*type:\\s*number\\s*\\)[^}]*DeviceType\\.${enumN}`, "g");
|
|
93
|
+
const staticMethods = [];
|
|
94
|
+
let m;
|
|
95
|
+
while ((m = staticRe.exec(source)) !== null) {
|
|
96
|
+
staticMethods.push(m[1]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const instanceMethods = [];
|
|
100
|
+
for (const methodName of staticMethods) {
|
|
101
|
+
const instanceRe = new RegExp(`public\\s+${methodName}\\s*\\(\\s*\\)\\s*:\\s*boolean`);
|
|
102
|
+
if (instanceRe.test(source)) {
|
|
103
|
+
instanceMethods.push(methodName);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
pass: instanceMethods.length > 0,
|
|
109
|
+
detail: instanceMethods.length > 0
|
|
110
|
+
? `found: ${instanceMethods.join(", ")}()`
|
|
111
|
+
: staticMethods.length > 0
|
|
112
|
+
? `static ${staticMethods.join(", ")}() found but missing instance method(s)`
|
|
113
|
+
: "no type guard found",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function checkClassificationMethods(source, enumN) {
|
|
118
|
+
// Check broad classification methods (isCamera, isLock, isSensor, etc.)
|
|
119
|
+
const broadMethods = [
|
|
120
|
+
"isCamera", "isLock", "isSensor", "isKeyPad", "isSmartDrop",
|
|
121
|
+
"isSmartSafe", "isSmartTrack", "hasBattery", "isFloodLight",
|
|
122
|
+
"isIndoorCamera", "isDoorbell", "isPanAndTiltCamera",
|
|
123
|
+
"isSoloCameras", "isLockWifi", "isLockBle", "isEntrySensor",
|
|
124
|
+
"isMotionSensor",
|
|
125
|
+
];
|
|
126
|
+
const included = [];
|
|
127
|
+
for (const method of broadMethods) {
|
|
128
|
+
const re = new RegExp(`static\\s+${method}\\s*\\(\\s*type:\\s*number\\s*\\)[\\s\\S]*?\\n\\s*\\}`, "g");
|
|
129
|
+
const match = source.match(re);
|
|
130
|
+
if (match && match[0].includes(`DeviceType.${enumN}`)) {
|
|
131
|
+
included.push(method);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
pass: included.length > 0,
|
|
136
|
+
detail: included.length > 0
|
|
137
|
+
? `in ${included.length} method(s): ${included.join(", ")}`
|
|
138
|
+
: "not in any broad classification method",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function checkDeviceImages(typeNum) {
|
|
143
|
+
if (!typeNum) return { pass: false, detail: "no type number" };
|
|
144
|
+
if (!existsSync(DEVICE_IMAGES)) return { pass: false, detail: "device-images.js not found" };
|
|
145
|
+
const source = readFileSync(DEVICE_IMAGES, "utf-8");
|
|
146
|
+
const re = new RegExp(`case\\s+${typeNum}:`);
|
|
147
|
+
const found = re.test(source);
|
|
148
|
+
return { pass: found, detail: found ? "case present" : "missing case" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function checkSupportedDevicesDocs(enumN) {
|
|
152
|
+
if (!existsSync(SUPPORTED_DEVICES)) return { pass: false, detail: "supported_devices.md not found" };
|
|
153
|
+
const source = readFileSync(SUPPORTED_DEVICES, "utf-8");
|
|
154
|
+
// Check for the enum name or the display name pattern
|
|
155
|
+
const found = source.includes(enumN) || (typeNumber && new RegExp(`type\\s+${typeNumber}|${typeNumber}\\)`).test(source));
|
|
156
|
+
// Also check for model number pattern from enum comment (e.g. "CAMERA_4G_S330 = 111, //T86P2")
|
|
157
|
+
const enumLine = httpSource.match(new RegExp(`${enumN}\\s*=\\s*\\d+.*?//\\s*(\\S+)`));
|
|
158
|
+
const model = enumLine ? enumLine[1] : null;
|
|
159
|
+
const modelFound = model && source.includes(model);
|
|
160
|
+
if (modelFound) return { pass: true, detail: `model ${model} found in docs` };
|
|
161
|
+
if (found) return { pass: true, detail: "entry found in docs" };
|
|
162
|
+
return { pass: false, detail: model ? `model ${model} not found` : "not found in docs" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function checkBySn(source, enumN) {
|
|
166
|
+
// Check if this device is referenced in any *BySn method
|
|
167
|
+
// First get the model prefix from the enum comment
|
|
168
|
+
const enumLine = httpSource.match(new RegExp(`${enumN}\\s*=\\s*\\d+.*?//\\s*(\\S+)`));
|
|
169
|
+
const model = enumLine ? enumLine[1] : null;
|
|
170
|
+
if (!model) return { pass: false, detail: "cannot determine model prefix from enum comment" };
|
|
171
|
+
|
|
172
|
+
const bySnMethods = ["isIntegratedDeviceBySn", "isSoloCameraBySn", "isSmartDropBySn", "isGarageCameraBySn", "isFloodlightBySn"];
|
|
173
|
+
const found = [];
|
|
174
|
+
for (const method of bySnMethods) {
|
|
175
|
+
const re = new RegExp(`static\\s+${method}\\s*\\([^)]*\\)[\\s\\S]*?\\n\\s*\\}`, "g");
|
|
176
|
+
const match = source.match(re);
|
|
177
|
+
if (match && match[0].includes(`"${model}"`)) {
|
|
178
|
+
found.push(method);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
pass: found.length > 0,
|
|
183
|
+
detail: found.length > 0
|
|
184
|
+
? `in ${found.join(", ")}()`
|
|
185
|
+
: `model ${model} not in any *BySn method (may not be needed)`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Run all checks ──────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
const W = 80;
|
|
192
|
+
console.log("=".repeat(W));
|
|
193
|
+
console.log(`Post-Implementation Verification: ${enumName}${typeNumber ? ` (type ${typeNumber})` : ""}`);
|
|
194
|
+
console.log("=".repeat(W));
|
|
195
|
+
console.log();
|
|
196
|
+
|
|
197
|
+
if (!typeNumber) {
|
|
198
|
+
console.log(`FATAL: ${enumName} not found in DeviceType enum. Cannot proceed.`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const results = [];
|
|
203
|
+
|
|
204
|
+
function run(label, check, required = true) {
|
|
205
|
+
const icon = check.pass ? "+" : required ? "-" : "~";
|
|
206
|
+
const status = check.pass ? "PASS" : required ? "FAIL" : "WARN";
|
|
207
|
+
results.push({ label, ...check, required, status });
|
|
208
|
+
console.log(` [${icon}] ${label.padEnd(35)} ${status.padEnd(6)} ${check.detail}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log("EUFY-SECURITY-CLIENT (src/http/types.ts)");
|
|
212
|
+
console.log("-".repeat(W));
|
|
213
|
+
run("DeviceType enum", { pass: checkEnumExists(httpSource, enumName), detail: `${enumName} = ${typeNumber}` });
|
|
214
|
+
run("GenericTypeProperty states", checkGenericTypePropertyStates(httpSource, typeNumber));
|
|
215
|
+
run("DeviceProperties map", checkLookupMap(httpSource, "DeviceProperties", enumName));
|
|
216
|
+
run("DeviceCommands map", checkLookupMap(httpSource, "DeviceCommands", enumName));
|
|
217
|
+
run("StationProperties map", checkLookupMap(httpSource, "StationProperties", enumName), false);
|
|
218
|
+
run("StationCommands map", checkLookupMap(httpSource, "StationCommands", enumName), false);
|
|
219
|
+
console.log();
|
|
220
|
+
|
|
221
|
+
console.log("EUFY-SECURITY-CLIENT (src/http/device.ts)");
|
|
222
|
+
console.log("-".repeat(W));
|
|
223
|
+
run("Dedicated type guard (static)", checkTypeGuard(deviceSource, enumName));
|
|
224
|
+
run("Instance method", checkInstanceMethod(deviceSource, enumName));
|
|
225
|
+
run("Classification methods", checkClassificationMethods(deviceSource, enumName));
|
|
226
|
+
console.log();
|
|
227
|
+
|
|
228
|
+
console.log("EUFY-SECURITY-CLIENT (docs)");
|
|
229
|
+
console.log("-".repeat(W));
|
|
230
|
+
run("docs/supported_devices.md", checkSupportedDevicesDocs(enumName), false);
|
|
231
|
+
console.log();
|
|
232
|
+
|
|
233
|
+
console.log("EUFY-SECURITY-CLIENT (serial number methods)");
|
|
234
|
+
console.log("-".repeat(W));
|
|
235
|
+
run("*BySn serial number methods", checkBySn(deviceSource, enumName), false);
|
|
236
|
+
console.log();
|
|
237
|
+
|
|
238
|
+
console.log("HOMEBRIDGE-EUFY-SECURITY");
|
|
239
|
+
console.log("-".repeat(W));
|
|
240
|
+
run("device-images.js", checkDeviceImages(typeNumber));
|
|
241
|
+
console.log();
|
|
242
|
+
|
|
243
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
const failed = results.filter((r) => r.status === "FAIL");
|
|
246
|
+
const warned = results.filter((r) => r.status === "WARN");
|
|
247
|
+
const passed = results.filter((r) => r.status === "PASS");
|
|
248
|
+
|
|
249
|
+
console.log("=".repeat(W));
|
|
250
|
+
if (failed.length === 0) {
|
|
251
|
+
console.log(`ALL REQUIRED CHECKS PASSED (${passed.length} pass, ${warned.length} warn)`);
|
|
252
|
+
if (warned.length > 0) {
|
|
253
|
+
console.log("Warnings (optional):");
|
|
254
|
+
for (const w of warned) {
|
|
255
|
+
console.log(` - ${w.label}: ${w.detail}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
console.log(`${failed.length} REQUIRED CHECK(S) FAILED:`);
|
|
260
|
+
for (const f of failed) {
|
|
261
|
+
console.log(` - ${f.label}: ${f.detail}`);
|
|
262
|
+
}
|
|
263
|
+
if (warned.length > 0) {
|
|
264
|
+
console.log(`\n${warned.length} warning(s):`);
|
|
265
|
+
for (const w of warned) {
|
|
266
|
+
console.log(` - ${w.label}: ${w.detail}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
console.log("=".repeat(W));
|
|
271
|
+
|
|
272
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const LIB_VERSION = "4.6.0-beta.
|
|
1
|
+
export const LIB_VERSION = "4.6.0-beta.29";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "Homebridge Eufy Security",
|
|
3
3
|
"name": "@homebridge-plugins/homebridge-eufy-security",
|
|
4
|
-
"version": "4.6.0-beta.
|
|
4
|
+
"version": "4.6.0-beta.29",
|
|
5
5
|
"description": "Control Eufy Security from homebridge.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|