@homebridge-plugins/homebridge-eufy-security 4.4.5-beta.0 → 4.4.5-beta.2
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.
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add New Eufy Security Device Support
|
|
7
|
+
|
|
8
|
+
You are adding support for a new device type to the eufy-security ecosystem. The user will provide a GitHub issue URL or device details (model name, model number like T86P2, device type number like 111). They may also provide raw device properties JSON.
|
|
9
|
+
|
|
10
|
+
Use `$ARGUMENTS` for the issue URL or device details.
|
|
11
|
+
|
|
12
|
+
## Phase 1 — Gather Information
|
|
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. **Run the property mapping script**: Save the raw properties JSON to a temp file and run:
|
|
16
|
+
```bash
|
|
17
|
+
node homebridge-eufy-security/.claude/skills/new-device-support/map-properties.mjs /tmp/<device>-raw-props.json
|
|
18
|
+
```
|
|
19
|
+
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. Use this output to identify the closest existing device and plan the property mapping.
|
|
20
|
+
3. **Ask the user** two questions:
|
|
21
|
+
- Image naming convention (check if images already exist or need renaming)
|
|
22
|
+
- Enum name for the DeviceType (e.g. `CAMERA_4G_S330`)
|
|
23
|
+
|
|
24
|
+
## Phase 2 — Plan (use EnterPlanMode)
|
|
25
|
+
|
|
26
|
+
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
|
+
|
|
28
|
+
### Files to modify in eufy-security-client
|
|
29
|
+
|
|
30
|
+
#### `src/http/types.ts` — 6 locations:
|
|
31
|
+
|
|
32
|
+
1. **DeviceType enum**: Add `ENUM_NAME = <number>, //<model>` in numeric order
|
|
33
|
+
2. **GenericTypeProperty states**: Add `<number>: "<Display Name> (<Model>)"` in numeric order
|
|
34
|
+
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
|
+
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
|
+
5. **DeviceCommands**: Add `[DeviceType.ENUM_NAME]` array. Commands depend on device capabilities (livestream, talkback, pan/tilt, download, snooze, preset positions, calibrate, alarm).
|
|
37
|
+
6. **StationCommands**: Add `[DeviceType.ENUM_NAME]` array if device has station properties. Typically `[CommandName.StationReboot, CommandName.StationTriggerAlarmSound]`.
|
|
38
|
+
|
|
39
|
+
#### `src/http/device.ts` — Classification methods:
|
|
40
|
+
|
|
41
|
+
Add the new device type to all applicable static classification methods. Common ones:
|
|
42
|
+
- `isCamera()` — if it's a camera/doorbell/floodlight
|
|
43
|
+
- `hasBattery()` — if battery-powered
|
|
44
|
+
- `isPanAndTiltCamera()` — if has PTZ
|
|
45
|
+
- `isOutdoorPanAndTiltCamera()` — if outdoor PTZ (this feeds into `isSoloCameras()`)
|
|
46
|
+
- `isFloodLight()`, `isIndoorCamera()`, `isDoorbell()`, etc. — as applicable
|
|
47
|
+
|
|
48
|
+
Add a **new static type guard method** and matching **instance method**:
|
|
49
|
+
```typescript
|
|
50
|
+
static isNewDevice(type: number): boolean {
|
|
51
|
+
//<Model>
|
|
52
|
+
return DeviceType.ENUM_NAME == type;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public isNewDevice(): boolean {
|
|
56
|
+
return Device.isNewDevice(this.rawDevice.device_type);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Update serial number checks if applicable:
|
|
61
|
+
- `isIntegratedDeviceBySn()` — add `sn.startsWith("<model>")` if the device is integrated/standalone
|
|
62
|
+
- `isSoloCameraBySn()` — add `sn.startsWith("<model>")` if it's a solo camera
|
|
63
|
+
|
|
64
|
+
#### `src/http/station.ts`:
|
|
65
|
+
|
|
66
|
+
- `isIntegratedDevice()` — if the device is standalone or can pair as its own station, it may already be covered by `isSoloCameras()`, `isFloodLight()`, etc. Only add explicit check if needed.
|
|
67
|
+
|
|
68
|
+
#### `src/push/service.ts`:
|
|
69
|
+
|
|
70
|
+
- 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
|
+
|
|
72
|
+
#### `docs/supported_devices.md`:
|
|
73
|
+
|
|
74
|
+
Add an entry in the correct table section:
|
|
75
|
+
```markdown
|
|
76
|
+
|  | <Display Name> (<Model>) | :wrench: | Firmware: <version> |
|
|
77
|
+
```
|
|
78
|
+
Use `:wrench:` for initial support.
|
|
79
|
+
|
|
80
|
+
### Files to modify in homebridge-eufy-security
|
|
81
|
+
|
|
82
|
+
#### `homebridge-ui/public/utils/device-images.js`:
|
|
83
|
+
|
|
84
|
+
Add a case in the `getImage()` switch:
|
|
85
|
+
```javascript
|
|
86
|
+
case <type_number>: return '<image_large>.png';
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Image assets:
|
|
90
|
+
|
|
91
|
+
- Rename or add images in `eufy-security-client/docs/_media/` (small + large)
|
|
92
|
+
- Rename or add image in `homebridge-eufy-security/homebridge-ui/public/assets/devices/` (large only)
|
|
93
|
+
|
|
94
|
+
## Phase 3 — Implement
|
|
95
|
+
|
|
96
|
+
Execute the plan. Key implementation notes:
|
|
97
|
+
|
|
98
|
+
- **Property mapping**: Use the output from `map-properties.mjs` (Phase 1) as the primary guide. When multiple property constants match the same `param_type` (e.g. `DeviceWatermarkProperty` vs `DeviceWatermarkSoloWiredDoorbellProperty`), pick the variant used by the closest existing device. Check the "Used by DeviceTypes" column in the script output.
|
|
99
|
+
- **Insert in order**: When adding to enums, switch statements, or `if` chains, maintain numeric ordering by device type number.
|
|
100
|
+
- **Audio recording property**: Different device families use different audio recording property constants (e.g. `DeviceAudioRecordingProperty`, `DeviceAudioRecordingStarlight4gLTEProperty`). Match the closest existing device.
|
|
101
|
+
- **No Co-Authored-By**: Do not add co-author lines to commits.
|
|
102
|
+
|
|
103
|
+
## Phase 4 — Build & Lint Verification
|
|
104
|
+
|
|
105
|
+
Run these in parallel:
|
|
106
|
+
```bash
|
|
107
|
+
cd eufy-security-client && npm run build
|
|
108
|
+
cd homebridge-eufy-security && npm run build
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Then verify lint:
|
|
112
|
+
```bash
|
|
113
|
+
cd eufy-security-client && npm run lint
|
|
114
|
+
cd homebridge-eufy-security && npm run lint
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
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.
|
|
118
|
+
|
|
119
|
+
## Phase 5 — Git & PR
|
|
120
|
+
|
|
121
|
+
### eufy-security-client
|
|
122
|
+
|
|
123
|
+
1. Discard any unrelated changes (e.g. `package-lock.json`)
|
|
124
|
+
2. Sync develop with upstream: `git fetch upstream && git checkout develop && git merge upstream/develop`
|
|
125
|
+
3. Create branch: `git checkout -b feat/<device-slug>`
|
|
126
|
+
4. Stage only relevant files (images (should match the `<device-slug>`), `src/http/types.ts`, `src/http/device.ts`, `src/push/service.ts`, `docs/supported_devices.md`)
|
|
127
|
+
5. Commit: `git commit -m "feat: add <Device Name> (<Model>, type <number>) support"`
|
|
128
|
+
6. Push: `git push origin feat/<device-slug>`
|
|
129
|
+
7. Create cross-fork PR:
|
|
130
|
+
```bash
|
|
131
|
+
gh pr create --repo bropat/eufy-security-client --base develop \
|
|
132
|
+
--head lenoxys:feat/<device-slug> \
|
|
133
|
+
--title "feat: add <Device Name> (<Model>, type <number>) support" \
|
|
134
|
+
--body-file /tmp/pr-body-<branch>.md
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### homebridge-eufy-security
|
|
138
|
+
|
|
139
|
+
1. Branch from the current beta branch (check with `git branch`): `git checkout -b feat/<device-slug>`
|
|
140
|
+
2. Stage: `homebridge-ui/public/utils/device-images.js` + any added image
|
|
141
|
+
3. Commit: `git commit -m "feat: add <Device Name> (<Model>, type <number>) device image mapping"`
|
|
142
|
+
4. Push: `git push origin feat/<device-slug>`
|
|
143
|
+
5. Create PR:
|
|
144
|
+
```bash
|
|
145
|
+
gh pr create --repo homebridge-plugins/homebridge-eufy-security \
|
|
146
|
+
--base <beta-branch> \
|
|
147
|
+
--title "feat: add <Device Name> (<Model>, type <number>) device image" \
|
|
148
|
+
--body-file /tmp/pr-body-<branch>.md
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### PR body format
|
|
152
|
+
|
|
153
|
+
Write PR bodies to `/tmp/pr-body-<branch>.md` files. Include:
|
|
154
|
+
- `## Summary` — bullet points describing the changes
|
|
155
|
+
- Cross-references: `Closes homebridge-plugins/homebridge-eufy-security#<issue>` in the client PR, `Closes #<issue>` in the plugin PR
|
|
156
|
+
- `Depends on bropat/eufy-security-client#<pr>` in the plugin PR
|
|
157
|
+
- `## Test plan` — checklist of verification steps
|
|
158
|
+
|
|
159
|
+
### Link the issue
|
|
160
|
+
|
|
161
|
+
After both PRs are created, update both PR bodies so they reference the issue with closing keywords.
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* map-properties.mjs
|
|
4
|
+
*
|
|
5
|
+
* Maps raw device param_type numbers from a device properties JSON dump
|
|
6
|
+
* to the corresponding property constants in eufy-security-client.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node map-properties.mjs <raw-properties.json>
|
|
10
|
+
* cat raw.json | node map-properties.mjs
|
|
11
|
+
*
|
|
12
|
+
* The input JSON should be the rawProperties object from a device dump,
|
|
13
|
+
* e.g. { "1101": { "value": 100 }, "1013": { "value": 1 }, ... }
|
|
14
|
+
* or an array of { "param_type": 1101, "param_value": "..." } objects.
|
|
15
|
+
*
|
|
16
|
+
* Output: a table mapping each param_type to its CommandType/ParamType enum name
|
|
17
|
+
* and all property constants that use that key.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from "fs";
|
|
21
|
+
import { join, dirname } from "path";
|
|
22
|
+
import { fileURLToPath } from "url";
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
// Paths relative to this script's location
|
|
27
|
+
// eufy-security-client is a sibling repo at the same level as homebridge-eufy-security
|
|
28
|
+
const CLIENT_ROOT = join(__dirname, "..", "..", "..", "..", "eufy-security-client");
|
|
29
|
+
const TYPES_HTTP = join(CLIENT_ROOT, "src", "http", "types.ts");
|
|
30
|
+
const TYPES_P2P = join(CLIENT_ROOT, "src", "p2p", "types.ts");
|
|
31
|
+
|
|
32
|
+
// ── Step 1: Parse enum values from source ──────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function parseEnum(source, enumName) {
|
|
35
|
+
const map = new Map(); // number → enum member name
|
|
36
|
+
const re = new RegExp(`enum\\s+${enumName}\\s*\\{([\\s\\S]*?)\\}`, "m");
|
|
37
|
+
const match = source.match(re);
|
|
38
|
+
if (!match) return map;
|
|
39
|
+
|
|
40
|
+
const body = match[1];
|
|
41
|
+
for (const line of body.split("\n")) {
|
|
42
|
+
const m = line.match(/^\s*(\w+)\s*=\s*(\d+)/);
|
|
43
|
+
if (m) {
|
|
44
|
+
map.set(Number(m[2]), `${enumName}.${m[1]}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return map;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const httpSource = readFileSync(TYPES_HTTP, "utf-8");
|
|
51
|
+
const p2pSource = readFileSync(TYPES_P2P, "utf-8");
|
|
52
|
+
|
|
53
|
+
const paramTypeMap = parseEnum(httpSource, "ParamType");
|
|
54
|
+
const commandTypeMap = parseEnum(p2pSource, "CommandType");
|
|
55
|
+
const trackerCommandTypeMap = parseEnum(p2pSource, "TrackerCommandType");
|
|
56
|
+
|
|
57
|
+
// Merge all enum maps (CommandType takes precedence as it's most commonly used)
|
|
58
|
+
const allEnums = new Map();
|
|
59
|
+
for (const [k, v] of paramTypeMap) allEnums.set(k, v);
|
|
60
|
+
for (const [k, v] of trackerCommandTypeMap) allEnums.set(k, v);
|
|
61
|
+
for (const [k, v] of commandTypeMap) allEnums.set(k, v);
|
|
62
|
+
|
|
63
|
+
// ── Step 2: Parse property constants and their keys ────────────────────────
|
|
64
|
+
|
|
65
|
+
// Match property constant definitions and extract their key field
|
|
66
|
+
// Handles both direct keys and spread patterns
|
|
67
|
+
function parsePropertyConstants(source) {
|
|
68
|
+
const props = []; // { constName, key, keyRaw, propertyName }
|
|
69
|
+
|
|
70
|
+
// Find each "export const XxxProperty" block
|
|
71
|
+
const constRegex = /export\s+const\s+(\w+Property)\s*:\s*\w+\s*=\s*\{([\s\S]*?)\};/g;
|
|
72
|
+
let m;
|
|
73
|
+
while ((m = constRegex.exec(source)) !== null) {
|
|
74
|
+
const constName = m[1];
|
|
75
|
+
const body = m[2];
|
|
76
|
+
|
|
77
|
+
// Extract key field
|
|
78
|
+
let key = null;
|
|
79
|
+
let keyRaw = null;
|
|
80
|
+
const keyMatch = body.match(/key:\s*(.+?)(?:,|\n)/);
|
|
81
|
+
if (keyMatch) {
|
|
82
|
+
keyRaw = keyMatch[1].trim();
|
|
83
|
+
// Resolve to numeric if it's an enum reference
|
|
84
|
+
if (keyRaw.startsWith('"') || keyRaw.startsWith("'")) {
|
|
85
|
+
key = keyRaw.replace(/['"]/g, ""); // string key
|
|
86
|
+
} else {
|
|
87
|
+
// It's an enum reference like CommandType.CMD_GET_BATTERY
|
|
88
|
+
const parts = keyRaw.split(".");
|
|
89
|
+
if (parts.length === 2) {
|
|
90
|
+
const enumName = parts[0];
|
|
91
|
+
let enumMap;
|
|
92
|
+
if (enumName === "CommandType") enumMap = commandTypeMap;
|
|
93
|
+
else if (enumName === "ParamType") enumMap = paramTypeMap;
|
|
94
|
+
else if (enumName === "TrackerCommandType") enumMap = trackerCommandTypeMap;
|
|
95
|
+
|
|
96
|
+
if (enumMap) {
|
|
97
|
+
for (const [num, fullName] of enumMap) {
|
|
98
|
+
if (fullName === keyRaw) {
|
|
99
|
+
key = num;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Extract PropertyName
|
|
109
|
+
let propertyName = null;
|
|
110
|
+
const nameMatch = body.match(/name:\s*(PropertyName\.\w+)/);
|
|
111
|
+
if (nameMatch) {
|
|
112
|
+
propertyName = nameMatch[1];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle spread: ...SomeOtherProperty (inherits key if not overridden)
|
|
116
|
+
let spreadFrom = null;
|
|
117
|
+
const spreadMatch = body.match(/\.\.\.(\w+Property)/);
|
|
118
|
+
if (spreadMatch) {
|
|
119
|
+
spreadFrom = spreadMatch[1];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
props.push({ constName, key, keyRaw, propertyName, spreadFrom });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Resolve spreads: if a property has no key but spreads from another, inherit
|
|
126
|
+
const byName = new Map(props.map((p) => [p.constName, p]));
|
|
127
|
+
for (const prop of props) {
|
|
128
|
+
if (prop.key === null && prop.spreadFrom) {
|
|
129
|
+
const parent = byName.get(prop.spreadFrom);
|
|
130
|
+
if (parent) {
|
|
131
|
+
prop.key = parent.key;
|
|
132
|
+
if (!prop.keyRaw) prop.keyRaw = `(spread from ${prop.spreadFrom}) ${parent.keyRaw}`;
|
|
133
|
+
if (!prop.propertyName) prop.propertyName = parent.propertyName;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return props;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const allProps = parsePropertyConstants(httpSource);
|
|
142
|
+
|
|
143
|
+
// Build lookup: numeric key → list of property constants
|
|
144
|
+
const keyToProps = new Map(); // key (number|string) → [{ constName, propertyName }]
|
|
145
|
+
for (const p of allProps) {
|
|
146
|
+
if (p.key === null) continue;
|
|
147
|
+
const k = p.key;
|
|
148
|
+
if (!keyToProps.has(k)) keyToProps.set(k, []);
|
|
149
|
+
keyToProps.get(k).push({ constName: p.constName, propertyName: p.propertyName });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Step 3: Parse DeviceProperties blocks to see which constants are used ──
|
|
153
|
+
|
|
154
|
+
function parseDevicePropertiesUsage(source) {
|
|
155
|
+
// Find all [DeviceType.XXX]: { ... } blocks inside DeviceProperties
|
|
156
|
+
const usage = new Map(); // constName → Set of DeviceType names that use it
|
|
157
|
+
const dpRegex = /\[DeviceType\.(\w+)\]\s*:\s*\{([\s\S]*?)\}/g;
|
|
158
|
+
// Only search within the DeviceProperties section
|
|
159
|
+
const dpStart = source.indexOf("export const DeviceProperties");
|
|
160
|
+
const dpEnd = source.indexOf("export const StationProperties");
|
|
161
|
+
if (dpStart === -1) return usage;
|
|
162
|
+
|
|
163
|
+
const dpSection = source.slice(dpStart, dpEnd === -1 ? undefined : dpEnd);
|
|
164
|
+
let m;
|
|
165
|
+
while ((m = dpRegex.exec(dpSection)) !== null) {
|
|
166
|
+
const deviceType = m[1];
|
|
167
|
+
const body = m[2];
|
|
168
|
+
// Find all property constant references
|
|
169
|
+
const refRegex = /:\s*(\w+Property)/g;
|
|
170
|
+
let rm;
|
|
171
|
+
while ((rm = refRegex.exec(body)) !== null) {
|
|
172
|
+
const constName = rm[1];
|
|
173
|
+
if (!usage.has(constName)) usage.set(constName, new Set());
|
|
174
|
+
usage.get(constName).add(deviceType);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return usage;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const propUsage = parseDevicePropertiesUsage(httpSource);
|
|
181
|
+
|
|
182
|
+
// ── Step 4: Read and parse input JSON ──────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
let inputData;
|
|
185
|
+
const inputFile = process.argv[2];
|
|
186
|
+
if (inputFile) {
|
|
187
|
+
inputData = readFileSync(inputFile, "utf-8");
|
|
188
|
+
} else {
|
|
189
|
+
// Read from stdin
|
|
190
|
+
inputData = readFileSync("/dev/stdin", "utf-8");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let rawParamTypes; // Set of numeric param_type values
|
|
194
|
+
try {
|
|
195
|
+
const parsed = JSON.parse(inputData);
|
|
196
|
+
|
|
197
|
+
if (Array.isArray(parsed)) {
|
|
198
|
+
// Array of { param_type: number, ... }
|
|
199
|
+
rawParamTypes = new Set(parsed.map((item) => Number(item.param_type)));
|
|
200
|
+
} else if (typeof parsed === "object") {
|
|
201
|
+
// Check if it's a full device dump with rawProperties nested
|
|
202
|
+
if (parsed.rawProperties && typeof parsed.rawProperties === "object") {
|
|
203
|
+
rawParamTypes = new Set(Object.keys(parsed.rawProperties).map(Number));
|
|
204
|
+
} else {
|
|
205
|
+
// Direct { "1101": ..., "1013": ... } format
|
|
206
|
+
rawParamTypes = new Set(Object.keys(parsed).map(Number));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
console.error("Error: Could not parse input JSON. Provide either:");
|
|
211
|
+
console.error(' - A raw properties object: { "1101": {...}, "1013": {...}, ... }');
|
|
212
|
+
console.error(' - An array: [{ "param_type": 1101, ... }, ...]');
|
|
213
|
+
console.error(" - A device dump with rawProperties key");
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Filter out NaN (from string keys in the input)
|
|
218
|
+
rawParamTypes = new Set([...rawParamTypes].filter((n) => !isNaN(n)));
|
|
219
|
+
|
|
220
|
+
// ── Step 5: Output results ─────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
const COL1 = 12; // param_type
|
|
223
|
+
const COL2 = 48; // enum name
|
|
224
|
+
const COL3 = 50; // property constants
|
|
225
|
+
// COL4 reserved for future use
|
|
226
|
+
|
|
227
|
+
const header = [
|
|
228
|
+
"param_type".padEnd(COL1),
|
|
229
|
+
"Enum Name".padEnd(COL2),
|
|
230
|
+
"Property Constant(s)".padEnd(COL3),
|
|
231
|
+
"Used by DeviceTypes",
|
|
232
|
+
].join(" | ");
|
|
233
|
+
|
|
234
|
+
console.log("=".repeat(header.length));
|
|
235
|
+
console.log("Device Raw Properties → Property Constants Mapping");
|
|
236
|
+
console.log(`Total raw param_types: ${rawParamTypes.size}`);
|
|
237
|
+
console.log("=".repeat(header.length));
|
|
238
|
+
console.log();
|
|
239
|
+
console.log(header);
|
|
240
|
+
console.log("-".repeat(header.length));
|
|
241
|
+
|
|
242
|
+
const matched = [];
|
|
243
|
+
const unmatched = [];
|
|
244
|
+
|
|
245
|
+
for (const paramType of [...rawParamTypes].sort((a, b) => a - b)) {
|
|
246
|
+
const enumName = allEnums.get(paramType) || "???";
|
|
247
|
+
const props = keyToProps.get(paramType);
|
|
248
|
+
|
|
249
|
+
if (props && props.length > 0) {
|
|
250
|
+
const constNames = props.map((p) => p.constName);
|
|
251
|
+
const propertyNames = [...new Set(props.map((p) => p.propertyName).filter(Boolean))];
|
|
252
|
+
const usedBy = new Set();
|
|
253
|
+
for (const cn of constNames) {
|
|
254
|
+
const devices = propUsage.get(cn);
|
|
255
|
+
if (devices) devices.forEach((d) => usedBy.add(d));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
matched.push({
|
|
259
|
+
paramType,
|
|
260
|
+
enumName,
|
|
261
|
+
constNames,
|
|
262
|
+
propertyNames,
|
|
263
|
+
usedBy: [...usedBy],
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
console.log(
|
|
267
|
+
[
|
|
268
|
+
String(paramType).padEnd(COL1),
|
|
269
|
+
enumName.padEnd(COL2),
|
|
270
|
+
constNames.join(", ").padEnd(COL3),
|
|
271
|
+
[...usedBy].slice(0, 5).join(", ") + (usedBy.size > 5 ? "..." : ""),
|
|
272
|
+
].join(" | ")
|
|
273
|
+
);
|
|
274
|
+
} else {
|
|
275
|
+
unmatched.push({ paramType, enumName });
|
|
276
|
+
console.log(
|
|
277
|
+
[
|
|
278
|
+
String(paramType).padEnd(COL1),
|
|
279
|
+
enumName.padEnd(COL2),
|
|
280
|
+
"(no property constant found)".padEnd(COL3),
|
|
281
|
+
"",
|
|
282
|
+
].join(" | ")
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log();
|
|
288
|
+
console.log("=".repeat(80));
|
|
289
|
+
console.log(`MATCHED: ${matched.length} / ${rawParamTypes.size} param_types have property constants`);
|
|
290
|
+
console.log(`UNMATCHED: ${unmatched.length} param_types have no known property constant`);
|
|
291
|
+
console.log("=".repeat(80));
|
|
292
|
+
|
|
293
|
+
if (unmatched.length > 0) {
|
|
294
|
+
console.log();
|
|
295
|
+
console.log("Unmatched param_types (may need new property constants or may be internal):");
|
|
296
|
+
for (const u of unmatched) {
|
|
297
|
+
console.log(` ${u.paramType} → ${u.enumName}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Output a suggested PropertyName list for use in DeviceProperties block
|
|
302
|
+
console.log();
|
|
303
|
+
console.log("=".repeat(80));
|
|
304
|
+
console.log("SUGGESTED DeviceProperties block entries:");
|
|
305
|
+
console.log("=".repeat(80));
|
|
306
|
+
const seen = new Set();
|
|
307
|
+
for (const m of matched) {
|
|
308
|
+
for (let i = 0; i < m.constNames.length; i++) {
|
|
309
|
+
const pn = m.propertyNames[0]; // Use first PropertyName
|
|
310
|
+
const cn = m.constNames[i];
|
|
311
|
+
if (pn && !seen.has(pn)) {
|
|
312
|
+
seen.add(pn);
|
|
313
|
+
console.log(` [${pn}]: ${cn},`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const LIB_VERSION = "4.4.5-beta.
|
|
1
|
+
export const LIB_VERSION = "4.4.5-beta.2";
|
|
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.4.5-beta.
|
|
4
|
+
"version": "4.4.5-beta.2",
|
|
5
5
|
"description": "Control Eufy Security from homebridge.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|