@linanw/homebridge-mijia-remote-camera 0.4.1 → 0.4.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.3 — 2026-10-24
4
+
5
+ - Name the existing HomeKit night-vision control 手动夜视 and add 自动夜视, with distinct `Name` and `ConfiguredName` values and the original service identity preserved.
6
+ - Reflect manual, automatic and off policies as two mutually exclusive switches. Serialize fresh mode checks and conditional changes with other camera-control requests, so an already-off switch does not disable the other mode.
7
+ - Synchronize both controls from confirmed Xiaomi/UI/Matter mode observations and prevent an older in-flight read from overwriting a newer observed mode.
8
+
9
+ ## 0.4.2 — 2026-10-24
10
+
11
+ - Publish explicit HomeKit `ConfiguredName` values for the four camera direction switches: 云台向上, 云台向下, 云台向左 and 云台向右.
12
+ - Repair default service names in place while preserving existing custom configured names, service identifiers, pairing and automation targets.
13
+
3
14
  ## 0.4.1 — 2026-10-24
4
15
 
5
16
  - Accept the trusted Homebridge storage alias used by Synology Docker during GUI component import. User-selected descendants still reject symbolic links and directory escapes.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  要求 **Linux x86_64、Node.js 22.13.1+(22.x)或 24.x、Homebridge 1.11.4+(1.x)或 2.x**,以及支持自定义插件设置页的 Homebridge UI。适用 Synology DS1618+ 上以 host 网络运行的 Homebridge Docker。运行组件提供 glibc 和 musl 版本;ARM、macOS 和 Windows 不支持。
10
10
 
11
- 这是独立开发的插件,与小米、Apple 和 Homebridge 项目没有隶属关系。当前尚未获得 Homebridge Verified 或 Matter 认证。
11
+ 这是独立开发的插件,与小米、Apple 和 Homebridge 项目没有隶属关系。[0.4.1](https://github.com/linanw/homebridge-mijia-remote-camera/releases/tag/v0.4.1) 已公开发布;Homebridge 认证申请 [#1296](https://github.com/homebridge/plugins/issues/1296) 已提交,正在等待官方审核。当前尚未获得 Homebridge Verified 或 Matter 认证。
12
12
 
13
13
  ## 安装和首次导入
14
14
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  1. 在 Homebridge UI 的“插件”页搜索完整包名 `@linanw/homebridge-mijia-remote-camera` 并安装。迁移时先停用此前连接同一摄像头的 viewer/relay 或旧插件,避免重复占用运行目录。
18
18
  2. 将你已持有的 `homebridge-mijia-remote-camera-0.1.0.tgz` 放入 **Homebridge 持久化存储目录**,容器内通常为 `/homebridge`。也支持放在该目录内的匹配版本 `assets` 目录。插件不会提供或下载这些组件。
19
- 3. 打开插件设置页的“导入已有的运行包”,填写存储目录内的相对路径,例如 `homebridge-mijia-remote-camera-0.1.0.tgz`,点击“导入并校验组件”,等待结果。此过程无需终端,也无需让 Homebridge 在 TTY 中启动。绝对路径也必须位于同一存储目录内;越界路径、符号链接与不匹配的组件会被拒绝。
19
+ 3. 打开插件设置页的“导入已有的运行包”,填写存储目录内的相对路径,例如 `homebridge-mijia-remote-camera-0.1.0.tgz`,点击“导入并校验组件”,等待结果。此过程无需终端,也无需让 Homebridge 在 TTY 中启动。绝对路径也必须位于同一配置存储目录内;Homebridge 指定的根目录可以是别名链接,但用户选择的子路径中的符号链接、越界路径与不匹配的组件会被拒绝。
20
20
  4. 点击“启用并保存配置”,按界面提示从 Homebridge 首页重新启动,再回到设置页登录。
21
21
 
22
22
  导入只读取运行文件、核验固定 SHA-256,并保留组件的 legal 目录;目标为 `<Homebridge storage>/mijia-remote-camera/runtime`。它不导入旧包的插件代码、凭据或配置。已有有效组件会保留;目录正被运行中的插件占用时返回 `runtime_in_use`。此时先停用平台并正常重启,待旧进程退出后再导入。损坏的已有目录也不会被覆盖,请先备份后处理。
@@ -69,9 +69,13 @@ mijia-camera-setup --from /homebridge/homebridge-mijia-remote-camera-0.1.0.tgz -
69
69
 
70
70
  默认开启现场声音、双向对讲和夜视开关。升级后重启摄像头子桥即可,保留已有 HomeKit 配对。在“家庭”直播页面使用对讲按钮,并允许 iPhone 访问麦克风;松开或结束对讲后会停止向摄像头发送声音。声音静音和音量控制会作用于实际音频转发。插件设置页显示是否已收到真实摄像头声音,以及当前协议是否支持对讲。
71
71
 
72
- 设置页可修改“收听摄像头现场声音”“允许双向对讲”“在家庭中显示夜视开关”;保存后需重启摄像头子桥。对应配置项为 `enableAudio`、`enableTalkback`、`enableNightVision`,默认均为 `true`。关闭现场声音也会禁用对讲。
72
+ 设置页可修改“收听摄像头现场声音”“允许双向对讲”“在家庭中显示手动/自动夜视开关”;保存后需重启摄像头子桥。对应配置项为 `enableAudio`、`enableTalkback`、`enableNightVision`,默认均为 `true`。关闭现场声音也会禁用对讲。
73
73
 
74
- 夜视目前仅支持已核对 MIOT 定义的 `chuangmi.camera.061a01`/`chuangmi.camera.061a03`。插件设置页先读取设备实际状态,再选择“自动/开启/关闭”并应用;写入后会回读确认。Apple“家庭”的“夜视”开关表示允许夜视:自动模式显示为开,重复开不会把自动改为强制开启;关掉后重新打开会设置为始终开启。需要恢复自动时,请在插件设置页选择。该开关表示夜视策略,不表示红外灯此刻是否点亮。启动和轮询只读状态,不更改策略。
74
+ 夜视目前仅支持已核对 MIOT 定义的 `chuangmi.camera.061a01`/`chuangmi.camera.061a03`。插件设置页先读取设备实际状态,再选择“自动/开启/关闭”并应用;写入后会回读确认。
75
+
76
+ 0.4.3 起 Apple“家庭”提供两个互斥开关:“手动夜视”开启时设置为始终开启;“自动夜视”开启时由摄像头按环境光自动决定是否启用红外。关闭当前开启的开关会关闭夜视,两者均显示关。重复关闭已经关闭的开关会保留另一个模式。两个开关反映夜视策略,不表示红外灯此刻是否点亮;米家 App、插件设置或 Matter 修改模式后,它们会同步更新。启动和轮询只读状态,不更改策略。
77
+
78
+ 原夜视开关保留配对和服务编号,名称改为“手动夜视”;自动模式下它现在显示关,“自动夜视”显示开。已有自动化中打开原开关表示强制开启夜视;需要自动模式的场景请使用新增开关。
75
79
 
76
80
  ## 云台方向控制(0.3.0)
77
81
 
@@ -79,6 +83,8 @@ mijia-camera-setup --from /homebridge/homebridge-mijia-remote-camera-0.1.0.tgz -
79
83
 
80
84
  需要在 Apple“家庭”中操作时,在插件设置页勾选“在 Apple家庭中显示四个云台方向开关”,保存并重启摄像头子桥。对应 `enablePtzSwitches: true`,默认关闭。每个开关打开后只发一次方向指令,随后自动回到关闭状态;关闭开关不会再次移动摄像头。无需重新配对。
81
85
 
86
+ 0.4.2 为四个方向补充 Apple“家庭”使用的独立配置名称:“云台向上”“云台向下”“云台向左”“云台向右”。已有配对和方向服务标识不变。升级并重启摄像头子桥后,重新打开“家庭”查看;Apple 已保存的自定义别名可能需要在该开关设置中手动调整,无需移除摄像头。
87
+
82
88
  MatterDesk 使用摄像头端点上的厂商方向步进接口 `MijiaPtzStep`(cluster `0xFFF1FC01`)。客户端需要先读取型号支持与当前可用状态;方向命令仅接受上、下、左、右,不提供任意设备指令入口。它没有将固定步进冒充为标准 Matter 机械角度控制;其他 Matter 客户端可能只显示视频功能。
83
89
 
84
90
  ## Matter 摄像头(实验性)
@@ -129,7 +135,9 @@ Matter 信令及 WebRTC 播放目前面向同一局域网,无外部 STUN/TUR
129
135
 
130
136
  0.4.0 在所选自有家庭完成真实云端往返:中文临时名称写入、独立读取并恢复原名;移到同家庭另一个现有房间、独立读取并恢复原房间。最终名称/房间和原凭据均保持原值。云端去除 emoji 的案例未被误报为确认成功。该验证不覆盖所有名称字符、家庭权限或摄像头型号。
131
137
 
132
- 2026-10-24 的完整本地测试在 Node 22.22.1 和 Node 24.19.0、Homebridge 2.4.0 下分别为 325 项:324 通过、1 项仅 root 用户适用的所有权测试跳过、0 失败;包含私有组件和独立 Matter 控制器集成。无私有组件的公开源码测试另见 [验证记录](docs/verification.md),两者不能互相替代。
138
+ 2026-10-24,0.4.1 的完整本地测试在 Node 22.22.1 和 Node 24.19.0、Homebridge 2.4.0 下分别为 328 项:327 通过、1 项仅 root 用户适用的所有权测试跳过、0 失败;包含已持有的运行组件和独立 Matter 控制器集成。[公开 CI](https://github.com/linanw/homebridge-mijia-remote-camera/actions/runs/40076759349) 的 Node 22.13.1/22.x/24.x × Homebridge 1.11.4/2.4.0 六个组合全部通过;公开 CI 不含私有组件,依赖这些组件的测试会明确跳过。
139
+
140
+ 0.4.1 已通过从公开 npm 匿名全新安装到非 root 用户环境、无平台配置启动 Homebridge 和生产依赖审计(0 个漏洞)。这些检查不表示新用户无需本地运行组件即可播放视频。详细验证范围见 [验证记录](docs/verification.md)。
133
141
 
134
142
  远程中继仍可能间歇断开并自动重连,重连期间预览和直播可能暂不可用。目前尚未完成长时间稳定性验证。
135
143
 
@@ -145,4 +153,6 @@ mijia-camera-diagnose --storage /var/lib/homebridge
145
153
 
146
154
  能在插件中收到视频、但 Apple“家庭”添加失败时,请检查摄像头日志中的独立 HomeKit 端口及 Setup Code;摄像头端口与主桥/子桥端口不同。
147
155
 
148
- 公开仓库与认证准备、兼容性矩阵及已知限制见 [verification.md](docs/verification.md),组件权利说明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
156
+ 相关的 [homebridge-xiaomi-camera-native](https://github.com/Kosztyk/homebridge-xiaomi-camera-native) 已提出认证申请 [#1289](https://github.com/homebridge/plugins/issues/1289),截至 2026-10-24 尚待最终审核。它的 README 描述直接局域网 MISS/CS2 视频、快照与云台,不含音频/双向对讲。本插件针对已验证的跨网络摄像头传输、音频/对讲和米家/Matter 资料接口;存在功能重叠,不代表整体功能优于其他项目。
157
+
158
+ 公开发布与认证审核状态、兼容性矩阵、同类项目对照及已知限制见 [verification.md](docs/verification.md),组件权利说明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
@@ -81,7 +81,7 @@
81
81
  },
82
82
  "enableNightVision": {
83
83
  "type": "boolean",
84
- "title": "在家庭中显示夜视开关",
84
+ "title": "在家庭中显示手动/自动夜视开关",
85
85
  "default": true
86
86
  },
87
87
  "enablePtzSwitches": {
@@ -1,6 +1,6 @@
1
- # Homebridge verification preparation
1
+ # Homebridge verification status and evidence
2
2
 
3
- Reviewed against the [current official criteria](https://github.com/homebridge/plugins/wiki/Verified-Plugins) on 2026-10-24. **This plugin has not been verified by the Homebridge team.** This document records preparation and remaining review points; it is not an approval or a guarantee that the request will be accepted.
3
+ Reviewed against the [current official criteria](https://github.com/homebridge/plugins/wiki/Verified-Plugins) on 2026-10-24. Release **0.4.1** is public on [npm](https://www.npmjs.com/package/@linanw/homebridge-mijia-remote-camera), with matching [GitHub source and release](https://github.com/linanw/homebridge-mijia-remote-camera/releases/tag/v0.4.1). [Verification request #1296](https://github.com/homebridge/plugins/issues/1296) has been submitted and is open with `pending` / `request-verification` labels. **The Homebridge team has not granted verification.** The project's own checks and CI do not constitute approval, and no Verified badge is displayed.
4
4
 
5
5
  ## Compatibility and distribution
6
6
 
@@ -8,7 +8,7 @@ Reviewed against the [current official criteria](https://github.com/homebridge/p
8
8
  | --- | --- |
9
9
  | npm name | `@linanw/homebridge-mijia-remote-camera` |
10
10
  | Platform | Dynamic platform `MijiaRemoteCamera`; external native HAP camera; optional independently registered Matter camera |
11
- | Target release | 0.4.1; matching public npm and GitHub release required before submission |
11
+ | Review release | Public npm and matching GitHub release 0.4.1; code commit `b0fcb441872d5fd38e6035b18a3967c9e7c49421` |
12
12
  | Node.js | `^22.13.1 || ^24.0.0` |
13
13
  | Homebridge | `^1.11.4 || ^2.0.0`; current development dependency 2.4.0 |
14
14
  | OS / CPU | Linux x64 only; pinned local runtime contains glibc and musl variants |
@@ -23,8 +23,8 @@ The current criteria specify the supported Node LTS lines, currently 22 and 24.
23
23
  | Official requirement | Implementation / review evidence |
24
24
  | --- | --- |
25
25
  | Dynamic platform and a distinct purpose | Registered platform with a single selected external camera; the comparison below explains the remote Xiaomi transport use case |
26
- | Public npm and GitHub source with Issues enabled | Public source, Issues, npm 0.4.0 and its GitHub release exist; the 0.4.1 alias-import patch is the final review target |
27
- | Supported Node LTS, clean installation and no unconfigured startup | Node 22/24 public-source CI matrix; no platform instance or camera is created without configuration; clean tarball installation still needs final release verification |
26
+ | Public npm and GitHub source with Issues enabled | Public source and Issues are available; npm 0.4.1 and the matching GitHub release are published |
27
+ | Supported Node LTS, clean installation and no unconfigured startup | All six public CI combinations passed; anonymous fresh nonroot installation of npm 0.4.1 and unconfigured Homebridge startup passed without creating the plugin runtime, account session or camera |
28
28
  | No system-changing postinstall or TTY setup | No plugin lifecycle install scripts; local import and account setup are in Homebridge UI; CLI tools are optional |
29
29
  | Settings GUI | Schema plus protected custom settings page |
30
30
  | No analytics or tracking | No author-operated analytics service; Xiaomi account, cloud metadata and remote-camera services are necessary network dependencies |
@@ -45,6 +45,8 @@ The comparison uses the [official verified list](https://github.com/homebridge/p
45
45
 
46
46
  A related [homebridge-xiaomi-go2rtc project](https://github.com/moguennouni/homebridge-xiaomi-go2rtc) now documents local Xiaomi camera streaming and controls, automatic go2rtc provisioning and broader server architectures. It was not present in the official verified list checked for this preparation. Its overlapping functionality must still be disclosed; acceptance of a distinct use case is the Homebridge team's decision.
47
47
 
48
+ Another related project, [homebridge-xiaomi-camera-native](https://github.com/Kosztyk/homebridge-xiaomi-camera-native), has an open [verification request #1289](https://github.com/homebridge/plugins/issues/1289), marked `awaiting-final-review` on 2026-10-24. It was not in the official verified list checked on that date. Its [README](https://github.com/Kosztyk/homebridge-xiaomi-camera-native/blob/main/README.md) describes direct LAN MISS/CS2 video, snapshots and pan/tilt, requires LAN access to the camera, and explicitly excludes audio and two-way talk. This plugin's validated remote transport, camera audio/talkback and Mi Home/Matter metadata are relevant differences. Both the existing overlap and the pending application should be considered by the reviewers; no general feature-superset or replacement claim is made.
49
+
48
50
  ## Scope that must remain explicit
49
51
 
50
52
  - The npm package excludes micam, its native components, JRE and FFmpeg. The known local backend repository has no license, and the known FFmpeg build enables `nonfree`. No redistribution rights are asserted. See [third-party notices](../THIRD_PARTY_NOTICES.md).
@@ -53,21 +55,22 @@ A related [homebridge-xiaomi-go2rtc project](https://github.com/moguennouni/home
53
55
  - Apple Home's own name and room edits are not written back to Mi Home. Matter metadata uses a vendor interface and a 160-byte UTF-8 bound; controllers must explicitly support it. Neither of these features is a universal HomeKit/Matter room sync promise.
54
56
  - Matter remains an experimental development device using SDK test attestation. Homebridge verification, if granted, would not confer Matter product certification.
55
57
 
56
- ## Validation and release checklist
58
+ ## Validation and release status
57
59
 
58
- The CI workflow tests public-source behavior with Node 22.13.1, the current 22.x and 24.x releases against Homebridge 1.11.4 and 2.4.0. It runs `npm test` without proprietary runtime assets; codec-dependent HomeKit media and the full-runtime UI IPC fixture explicitly report skipped tests when their required components are absent. Java/vendor protocol fixtures and real AAC-ELD/media checks require the separately held components; execute those locally and report their results separately. The workflow must actually pass on the public repository before describing CI as passed.
60
+ The [0.4.1 public CI run](https://github.com/linanw/homebridge-mijia-remote-camera/actions/runs/40076759349), for code commit `b0fcb441872d5fd38e6035b18a3967c9e7c49421`, passed all six combinations of Node 22.13.1 / current 22.x / 24.x and Homebridge 1.11.4 / 2.4.0. It installs public dependencies and runs unfiltered `npm test` without proprietary runtime assets. Codec-dependent HomeKit media, Java/vendor protocol fixtures and the full-runtime UI IPC fixture explicitly skip when their required components are absent. Public CI therefore does not replace the separately reported full-runtime and real-device validation.
59
61
 
60
- The final local full suite, with the held runtime and an independent Matter controller integration, passed on **Node 22.22.1 and Node 24.19.0 / Homebridge 2.4.0: 325 tests, 324 passed, 1 root-only ownership test skipped, 0 failed on each Node version**. Real-account validation changed a Chinese camera name and restored it, moved the camera between two existing rooms of its owned home and restored the original room, and independently read back each result. The original name/room and credentials were preserved at completion. An emoji-containing target was normalized by the cloud, correctly returned `metadata_not_applied`, and was restored; the plugin does not promise arbitrary cloud name characters or automatic rollback.
62
+ The final 0.4.1 local full suite, with the held runtime and an independent Matter controller integration, passed on **Node 22.22.1 and Node 24.19.0 / Homebridge 2.4.0: 328 tests, 327 passed, 1 root-only ownership test skipped, 0 failed on each Node version**. Real-account validation changed a Chinese camera name and restored it, moved the camera between two existing rooms of its owned home and restored the original room, and independently read back each result. The original name/room and credentials were preserved at completion. An emoji-containing target was normalized by the cloud, correctly returned `metadata_not_applied`, and was restored; the plugin does not promise arbitrary cloud name characters or automatic rollback.
61
63
 
62
- A local isolated source copy with no `assets/`, using Node 22.22.1 and Homebridge 2.4.0, ran the unfiltered `npm test`: **325 tests, 295 passed, 30 explicitly skipped, 0 failed**. It reused installed public dependencies, so it is not evidence of a fresh registry installation or of all six CI combinations. The four affected native-media/UI test files also ran with the held runtime: **40 passed, 0 skipped**; without assets the same subset reported **35 passed, 5 skipped**. No real account writes were performed by these tests.
64
+ Anonymous installation of **0.4.1** from the public npm registry into a fresh nonroot environment passed with lifecycle scripts disabled. Unconfigured Homebridge loaded the plugin and started normally without creating the plugin runtime, account session or camera. The public tarball contains no vendor runtime assets, and its production dependency audit reported **0 vulnerabilities**. These installation checks do not establish working video for users without the matching local components.
63
65
 
64
66
  - [x] Publish the reviewed source to the intended public GitHub repository, enable Issues, and verify all package URLs resolve.
65
67
  - [x] Complete changed-value Chinese rename/restore and existing-room round-trip validation with independent reads, original values restored and credentials unchanged. Confirm exact readback rejects a cloud-normalized emoji result; run the final local suite on Node 22 and 24.
66
- - [x] Public [CI matrix](https://github.com/linanw/homebridge-mijia-remote-camera/actions/runs/40076414483) passed all six combinations for 0.4.0 plus test portability fixes. The final 0.4.1 patch must retain a passing matrix. Anonymous registry installation as a nonroot user succeeded, and unconfigured Homebridge loaded the plugin without creating its runtime.
68
+ - [x] Public [0.4.1 CI matrix](https://github.com/linanw/homebridge-mijia-remote-camera/actions/runs/40076759349) passed all six combinations. Anonymous fresh nonroot registry installation and unconfigured Homebridge startup passed.
67
69
  - [x] Inspect the exact npm tarball and Git tree for credentials, imported assets, APK/DEX/decompiled code and third-party binaries. Only the project's own Java adapter JAR is intended for distribution.
68
- - [ ] Publish npm 0.4.1 and a matching GitHub release with release notes; ensure the default branch package version matches npm latest.
70
+ - [x] Publish npm 0.4.1 and a matching GitHub release with release notes; verify the default-branch package version matches npm latest.
69
71
  - [x] Refresh the production audit and official criteria/checker findings, then provide the limitations and comparison to the reviewers.
70
- - [ ] Submit the official verification request and address review feedback. Add a Verified badge only after the Homebridge team grants verification.
72
+ - [x] Submit [verification request #1296](https://github.com/homebridge/plugins/issues/1296) for 0.4.1; it is awaiting official review.
73
+ - [ ] Address official review feedback and await the Homebridge team's decision. Add a Verified badge only if verification is granted.
71
74
 
72
75
  The [official request form](https://github.com/homebridge/plugins/issues/new?template=1_verification-request.yml) requires the npm name, public repository and six Yes/No declarations. A roughly 100×100 PNG icon is optional and must respect licensing/trademarks. Include setup restrictions and test evidence in More Information. Use the [current template](https://github.com/homebridge/plugins/blob/latest/.github/ISSUE_TEMPLATE/1_verification-request.yml); repository transfer or a package scope change is not required for this request. The project team performs the review described in the [program documentation](https://github.com/homebridge/plugins#plugin-verification).
73
76
 
@@ -45,10 +45,10 @@
45
45
  <section><div class="step">03 · 声音与夜视</div><h2>摄像头功能</h2>
46
46
  <label for="nightMode">夜视模式</label><select id="nightMode" disabled><option value="auto">自动</option><option value="on">开启</option><option value="off">关闭</option></select>
47
47
  <div class="row"><button id="nightRead" class="secondary">读取夜视状态</button><button id="nightSave" disabled>应用夜视模式</button></div><p id="nightState" class="small" aria-live="polite" style="margin-top:12px">读取摄像头实际模式后可修改。</p>
48
- <p class="small">Apple“家庭”中的夜视开关表示允许夜视;自动模式显示为开。关闭后再次打开会设为始终开启;恢复自动请在这里选择。</p>
48
+ <p class="small">Apple“家庭”提供“手动夜视”和“自动夜视”两个互斥开关,分别设置常开和自动模式。关闭当前开启的开关会关闭夜视;两者表示模式,不代表红外灯此刻是否点亮。</p>
49
49
  <label><input id="enableAudio" type="checkbox" style="width:auto"> 收听摄像头现场声音</label>
50
50
  <label><input id="enableTalkback" type="checkbox" style="width:auto"> 允许 Apple“家庭”双向对讲</label>
51
- <label><input id="enableNightVision" type="checkbox" style="width:auto"> 在 Apple“家庭”中显示夜视开关</label>
51
+ <label><input id="enableNightVision" type="checkbox" style="width:auto"> 在 Apple“家庭”中显示手动/自动夜视开关</label>
52
52
  <div class="row"><button id="saveFeatures" class="secondary">保存功能配置</button></div><p id="featureState" class="small" aria-live="polite" style="margin-top:12px">功能配置保存后需重启摄像头子桥。对讲时,在“家庭”直播页面使用对讲按钮。</p>
53
53
  </section>
54
54
  <section id="ptzSection"><div class="step">04 · 云台</div><h2>调整摄像头方向</h2>
@@ -11,11 +11,12 @@ export const supportsNightVision = model => MODELS.has(model);
11
11
 
12
12
  /** Reads real camera policy. Registration/polling never changes that policy. */
13
13
  export class NightVisionManager {
14
- constructor({ getMode, setMode, cacheMs = 10000, pollIntervalMs = 60000, timeoutMs = 7200,
14
+ constructor({ getMode, setMode, setModeSwitch, cacheMs = 10000, pollIntervalMs = 60000, timeoutMs = 7200,
15
15
  now = Date.now, onChange, onFailure } = {}) {
16
16
  if (typeof getMode !== 'function' || typeof setMode !== 'function') throw failure();
17
17
  this.getMode = getMode;
18
18
  this.setMode = setMode;
19
+ this.setModeSwitch = setModeSwitch;
19
20
  this.cacheMs = cacheMs;
20
21
  this.pollIntervalMs = pollIntervalMs;
21
22
  this.timeoutMs = timeoutMs;
@@ -64,6 +65,12 @@ export class NightVisionManager {
64
65
  return this.status();
65
66
  }
66
67
 
68
+ observe(mode) {
69
+ if (this.closed || !MODES.has(mode)) return;
70
+ ++this.generation;
71
+ return this.accept(mode);
72
+ }
73
+
67
74
  async get({ force = false } = {}) {
68
75
  await this.mutations;
69
76
  if (this.closed) throw failure('runtime_closed');
@@ -118,6 +125,21 @@ export class NightVisionManager {
118
125
  });
119
126
  }
120
127
 
128
+ setModeEnabled(mode, enabled) {
129
+ if (!['on', 'auto'].includes(mode) || typeof enabled !== 'boolean')
130
+ return Promise.reject(failure('night_vision_rejected'));
131
+ if (typeof this.setModeSwitch !== 'function') return Promise.reject(failure());
132
+ return this.enqueue(async () => {
133
+ const confirmed = await this.operation(async () => {
134
+ const result = await this.setModeSwitch(mode, enabled);
135
+ if (enabled ? result?.mode !== mode : result?.mode === mode)
136
+ throw failure('night_vision_not_applied');
137
+ return result;
138
+ });
139
+ return this.accept(confirmed);
140
+ });
141
+ }
142
+
121
143
  start() {
122
144
  if (this.closed || this.timer) return;
123
145
  void this.get({ force: true }).catch(() => {});
@@ -133,19 +155,36 @@ export class NightVisionManager {
133
155
  }
134
156
  }
135
157
 
136
- /** The switch enables the night-vision policy; Auto is enabled, not a real-time IR-lamp reading. */
137
- export function configureNightVision({ api, accessory, runtime, model, name = '夜视', log,
158
+ /** Separate manual and automatic policy switches; neither reports the live IR-lamp state. */
159
+ export function configureNightVision({ api, accessory, runtime, model, name = '手动夜视', log,
138
160
  cacheMs, pollIntervalMs, timeoutMs } = {}) {
139
161
  if (!supportsNightVision(model)) return null;
140
- const service = accessory.getServiceById?.(api.hap.Service.Switch, 'mijia-night-vision')
141
- || accessory.addService(api.hap.Service.Switch, name, 'mijia-night-vision');
142
- const on = service.getCharacteristic(api.hap.Characteristic.On);
162
+ const controls = [
163
+ { mode: 'on', name, subtype: 'mijia-night-vision' },
164
+ { mode: 'auto', name: '自动夜视', subtype: 'mijia-night-vision-auto' },
165
+ ].map(control => {
166
+ const service = accessory.getServiceById?.(api.hap.Service.Switch, control.subtype)
167
+ || accessory.addService(api.hap.Service.Switch, control.name, control.subtype);
168
+ service.displayName = control.name;
169
+ service.updateCharacteristic(api.hap.Characteristic.Name, control.name);
170
+ if (!service.testCharacteristic(api.hap.Characteristic.ConfiguredName))
171
+ service.addOptionalCharacteristic(api.hap.Characteristic.ConfiguredName);
172
+ const configuredName = service.getCharacteristic(api.hap.Characteristic.ConfiguredName);
173
+ if (typeof configuredName.value !== 'string' || !configuredName.value.trim())
174
+ configuredName.updateValue(control.name);
175
+ return { ...control, service, on: service.getCharacteristic(api.hap.Characteristic.On) };
176
+ });
143
177
  let lastLoggedFailure;
144
178
  const manager = new NightVisionManager({
145
179
  getMode: () => runtime.getNightVision(),
146
180
  setMode: mode => runtime.setNightVision(mode),
181
+ setModeSwitch: (mode, enabled) => runtime.setNightVisionSwitch(mode, enabled),
147
182
  cacheMs, pollIntervalMs, timeoutMs,
148
- onChange: status => { lastLoggedFailure = null; service.updateCharacteristic(api.hap.Characteristic.On, status.enabled); },
183
+ onChange: status => {
184
+ lastLoggedFailure = null;
185
+ for (const control of controls)
186
+ control.service.updateCharacteristic(api.hap.Characteristic.On, status.mode === control.mode);
187
+ },
149
188
  onFailure: reason => {
150
189
  if (reason !== lastLoggedFailure) {
151
190
  lastLoggedFailure = reason;
@@ -154,9 +193,15 @@ export function configureNightVision({ api, accessory, runtime, model, name = '
154
193
  },
155
194
  });
156
195
  const hapFailure = () => new api.hap.HapStatusError(api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
157
- on.onGet(async () => { try { return (await manager.get()).enabled; } catch { throw hapFailure(); } });
158
- on.onSet(async value => { try { await manager.setEnabled(value); } catch { throw hapFailure(); } });
159
- const observed = value => { if (!manager.closed && MODES.has(value?.mode)) manager.accept(value.mode); };
196
+ for (const control of controls) {
197
+ control.on.onGet(async () => {
198
+ try { return (await manager.get()).mode === control.mode; } catch { throw hapFailure(); }
199
+ });
200
+ control.on.onSet(async value => {
201
+ try { await manager.setModeEnabled(control.mode, value); } catch { throw hapFailure(); }
202
+ });
203
+ }
204
+ const observed = value => manager.observe(value?.mode);
160
205
  runtime.on?.('night-mode', observed);
161
206
  const shutdown = manager.shutdown.bind(manager);
162
207
  manager.shutdown = () => { runtime.off?.('night-mode', observed); shutdown(); };
package/lib/platform.mjs CHANGED
@@ -107,14 +107,14 @@ export class MijiaRemoteCameraPlatform {
107
107
  ?.setCharacteristic(this.api.hap.Characteristic.Manufacturer, 'Xiaomi')
108
108
  .setCharacteristic(this.api.hap.Characteristic.Model, label(selected.model || '米家远程摄像头'))
109
109
  .setCharacteristic(this.api.hap.Characteristic.SerialNumber, `MiCam-${opaqueId}`)
110
- .setCharacteristic(this.api.hap.Characteristic.FirmwareRevision, '0.4.0');
110
+ .setCharacteristic(this.api.hap.Characteristic.FirmwareRevision, '0.4.3');
111
111
  this.delegate = this.delegateFactory({ api: this.api, log: this.log, runtime: this.runtime,
112
112
  config: this.config, ffmpegPath: this.runtime.ffmpegPath });
113
113
  accessory.configureController(this.delegate.controller);
114
114
  if (this.delegate.audioCompatibility) this.delegate.controller.setMicrophoneMuted(this.config.enableAudio === false);
115
115
  if (this.delegate.talkbackEnabled) this.delegate.controller.setSpeakerMuted(false);
116
116
  if (this.config.enableNightVision !== false) this.nightVision = configureNightVision({ api: this.api,
117
- accessory, runtime: this.runtime, model: selected.model, name: '夜视', log: this.log });
117
+ accessory, runtime: this.runtime, model: selected.model, name: '手动夜视', log: this.log });
118
118
  if (this.config.enablePtzSwitches === true) this.ptz = await configurePtz({ api: this.api,
119
119
  accessory, runtime: this.runtime, selection: selected, log: this.log });
120
120
  if (this.stopping) { this.ptz?.shutdown?.(); this.nightVision?.shutdown?.(); this.delegate?.shutdown(); return; }
package/lib/ptz.mjs CHANGED
@@ -17,6 +17,15 @@ export async function configurePtz({ api, accessory, runtime, selection, log } =
17
17
  const subtype = `mijia-ptz-${direction}`;
18
18
  const service = accessory.getServiceById?.(api.hap.Service.Switch, subtype)
19
19
  || accessory.addService(api.hap.Service.Switch, LABELS[direction], subtype);
20
+ service.displayName = LABELS[direction];
21
+ service.updateCharacteristic(api.hap.Characteristic.Name, LABELS[direction]);
22
+ // Home uses ConfiguredName for switches grouped under a camera. Keep a
23
+ // controller's existing custom name when reusing an already configured service.
24
+ if (!service.testCharacteristic(api.hap.Characteristic.ConfiguredName))
25
+ service.addOptionalCharacteristic(api.hap.Characteristic.ConfiguredName);
26
+ const configuredName = service.getCharacteristic(api.hap.Characteristic.ConfiguredName);
27
+ if (typeof configuredName.value !== 'string' || !configuredName.value.trim())
28
+ configuredName.updateValue(LABELS[direction]);
20
29
  services.push(service);
21
30
  const on = service.getCharacteristic(api.hap.Characteristic.On);
22
31
  service.updateCharacteristic(api.hap.Characteristic.On, false);
package/lib/runtime.mjs CHANGED
@@ -405,7 +405,11 @@ export class RuntimeManager extends EventEmitter {
405
405
  if (!['on', 'off', 'auto'].includes(mode)) return Promise.reject(new RuntimeError('invalid_request'));
406
406
  return this._nightVision(mode);
407
407
  }
408
- _nightVision(mode) {
408
+ setNightVisionSwitch(mode, enabled) {
409
+ if (!['on', 'auto'].includes(mode) || typeof enabled !== 'boolean') return Promise.reject(new RuntimeError('invalid_request'));
410
+ return this._nightVision(mode, enabled);
411
+ }
412
+ _nightVision(mode, enabled) {
409
413
  if (this.metadataWriteBusy) return Promise.reject(new RuntimeError('metadata_busy'));
410
414
  if (this.closed) return Promise.reject(new RuntimeError('runtime_closed'));
411
415
  if (this.paused) return Promise.reject(new RuntimeError('auth_in_progress'));
@@ -414,43 +418,58 @@ export class RuntimeManager extends EventEmitter {
414
418
  if (!selected) return Promise.reject(new RuntimeError('not_selected'));
415
419
  const deadline = Date.now() + 6500, region = this.region, generation = this.generation;
416
420
  this.controlRequests.add(controller);
421
+ let cancelled = false, timer;
417
422
  const valid = () => {
418
423
  const current = this.selected();
419
424
  return !cancelled && !controller.signal.aborted && this.region === region && this.generation === generation && current
420
425
  && ['deviceId', 'homeId', 'homeOwner', 'region'].every(key => current[key] === selected[key]);
421
426
  };
422
- let cancelled = false, timer;
423
- const task = async () => {
427
+ const check = () => {
424
428
  if (cancelled || Date.now() >= deadline) throw new RuntimeError('query_timeout');
425
429
  if (this.closed) throw new RuntimeError('runtime_closed');
426
430
  if (this.paused) throw new RuntimeError('auth_in_progress');
427
- const current = this.selected();
428
431
  if (!valid()) throw new RuntimeError('query_cancelled');
429
- const args = [current.homeId, current.homeOwner, current.deviceId, ...(mode ? [mode] : [])];
432
+ };
433
+ const request = async requestedMode => {
434
+ check();
435
+ const current = this.selected();
436
+ const args = [current.homeId, current.homeOwner, current.deviceId, ...(requestedMode ? [requestedMode] : [])];
430
437
  let reply;
431
438
  if (this.cloudControl) {
432
439
  await this.open();
433
- if (Date.now() >= deadline) throw new RuntimeError('query_timeout');
434
- if (!valid() || this.paused || this.closed) throw new RuntimeError('query_cancelled');
440
+ check();
435
441
  try {
436
- reply = mode ? await this.cloudControl.setNightVision(current, region, mode, { signal: controller.signal })
442
+ reply = requestedMode ? await this.cloudControl.setNightVision(current, region, requestedMode, { signal: controller.signal })
437
443
  : await this.cloudControl.getNightVision(current, region, { signal: controller.signal });
438
444
  } catch (error) {
439
445
  if (error?.reason === 'session_expired') this._renewControlSession();
440
446
  else if (error?.capabilityRequired === true && this.ready) this._refreshControlCapability(current, region);
441
- throw error;
447
+ throw new RuntimeError(error?.reason, error?.code);
442
448
  }
443
- } else reply = await this._query(mode ? 'set' : 'get', args,
444
- { mainClass: 'mijia.bridge.NightVisionControl', deadline, guard: valid });
445
- if (!valid()) throw new RuntimeError('query_cancelled');
449
+ } else reply = await this._query(requestedMode ? 'set' : 'get', args,
450
+ { mainClass: 'mijia.bridge.NightVisionControl', deadline, guard: valid, signal: controller.signal });
451
+ check();
446
452
  if (!['on', 'off', 'auto'].includes(reply?.mode)) throw new RuntimeError('invalid_backend_response');
453
+ if (requestedMode && reply.mode !== requestedMode) throw new RuntimeError('night_vision_not_applied');
454
+ return { mode: reply.mode };
455
+ };
456
+ const task = async () => {
457
+ let reply;
458
+ if (typeof enabled === 'boolean') {
459
+ // Keep the fresh observation and its conditional mutation in one queue
460
+ // task so UI/Matter writes cannot change the mode between these steps.
461
+ reply = await request();
462
+ const target = enabled ? mode : reply.mode === mode ? 'off' : reply.mode;
463
+ if (target !== reply.mode) reply = await request(target);
464
+ } else reply = await request(mode);
465
+ check();
447
466
  this.emit('night-mode', { mode: reply.mode });
448
467
  return { mode: reply.mode };
449
468
  };
450
469
  const result = this.queryChain.then(task, task);
451
470
  this.queryChain = result.catch(() => {});
452
471
  const timeout = new Promise((_, reject) => {
453
- timer = setTimeout(() => { cancelled = true; controller.abort(); reject(new RuntimeError('query_timeout')); }, 6500);
472
+ timer = setTimeout(() => { cancelled = true; controller.abort(); reject(new RuntimeError('query_timeout')); }, Math.max(1, deadline - Date.now()));
454
473
  });
455
474
  return Promise.race([result, timeout]).finally(() => { clearTimeout(timer); this.controlRequests.delete(controller); });
456
475
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@linanw/homebridge-mijia-remote-camera",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@linanw/homebridge-mijia-remote-camera",
9
- "version": "0.4.1",
9
+ "version": "0.4.3",
10
10
  "cpu": [
11
11
  "x64"
12
12
  ],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linanw/homebridge-mijia-remote-camera",
3
3
  "displayName": "米家远程摄像头",
4
- "version": "0.4.1",
4
+ "version": "0.4.3",
5
5
  "description": "Xiaomi remote camera plugin with HomeKit, experimental Matter streaming and Mi Home name/room sync",
6
6
  "license": "MIT",
7
7
  "author": "linanw",