@linanw/homebridge-mijia-remote-camera 0.3.0 → 0.4.1
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 +32 -0
- package/README.md +40 -25
- package/THIRD_PARTY_NOTICES.md +11 -3
- package/backend/CloudMetadata.java +386 -0
- package/backend/bridge.jar +0 -0
- package/docs/verification.md +74 -0
- package/homebridge-ui/public/index.html +98 -6
- package/homebridge-ui/server.js +43 -0
- package/lib/matter-device.mjs +24 -2
- package/lib/matter-metadata-cluster.mjs +243 -0
- package/lib/matter-service.mjs +15 -1
- package/lib/metadata-sync.mjs +192 -0
- package/lib/platform.mjs +35 -3
- package/lib/rpc.mjs +7 -5
- package/lib/runtime-assets.mjs +36 -1
- package/lib/runtime-factory.mjs +18 -2
- package/lib/runtime.mjs +157 -12
- package/lib/setup-job.mjs +49 -0
- package/npm-shrinkwrap.json +149 -167
- package/package.json +19 -7
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Homebridge verification preparation
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Compatibility and distribution
|
|
6
|
+
|
|
7
|
+
| Area | Declared scope |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| npm name | `@linanw/homebridge-mijia-remote-camera` |
|
|
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 |
|
|
12
|
+
| Node.js | `^22.13.1 || ^24.0.0` |
|
|
13
|
+
| Homebridge | `^1.11.4 || ^2.0.0`; current development dependency 2.4.0 |
|
|
14
|
+
| OS / CPU | Linux x64 only; pinned local runtime contains glibc and musl variants |
|
|
15
|
+
| Current device validation | One Xiaomi Smart Camera 3 Pro PTZ, CS2/vendor 4; other models are not promised |
|
|
16
|
+
| Initial configuration | Homebridge Plugin Settings GUI; asynchronous local component import, official Xiaomi login, home and camera selection |
|
|
17
|
+
| Runtime components | Existing, matching user-owned local components; no acquisition service, bundled vendor runtime, or automatic download |
|
|
18
|
+
|
|
19
|
+
The current criteria specify the supported Node LTS lines, currently 22 and 24. They do not state a universal OS/CPU support requirement. **Linux x64 remains a material limitation for the reviewers to assess**, especially for Raspberry Pi and other ARM users. A successful npm installation without the local components does not provide working video.
|
|
20
|
+
|
|
21
|
+
## Criteria and evidence to present
|
|
22
|
+
|
|
23
|
+
| Official requirement | Implementation / review evidence |
|
|
24
|
+
| --- | --- |
|
|
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 |
|
|
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
|
+
| Settings GUI | Schema plus protected custom settings page |
|
|
30
|
+
| No analytics or tracking | No author-operated analytics service; Xiaomi account, cloud metadata and remote-camera services are necessary network dependencies |
|
|
31
|
+
| Files confined to Homebridge storage | Private credentials, selection, local runtime and Matter identity under the instance storage directory; inspect clean-runtime writes during final review |
|
|
32
|
+
| Errors handled by the plugin | Bounded public errors, caught background work and shutdown failures; source tests cover safe failure paths |
|
|
33
|
+
|
|
34
|
+
The official [checker implementation](https://github.com/homebridge/plugins/blob/latest/src/plugin-checks/workspace/index.ts) also checks package metadata, dependency placement, configuration, repository/release availability and runtime behavior. `homebridge` is a development dependency; HAP is obtained from the host. HTTPS homepage/issues and both `supports-hap` and `supports-matter` describe the actual transports. A production `npm audit --omit=dev --json` on 2026-10-24 reported **0 vulnerabilities**. This local audit does not replace the Homebridge team's checks.
|
|
35
|
+
|
|
36
|
+
## Distinction from related verified plugins
|
|
37
|
+
|
|
38
|
+
The comparison uses the [official verified list](https://github.com/homebridge/plugins/blob/latest/verified-plugins.json) and the projects' own documentation. It explains a separate use case and does not claim broader feature coverage than those projects.
|
|
39
|
+
|
|
40
|
+
| Project | Published capability and difference |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| [Camera FFmpeg](https://github.com/homebridge-plugins/homebridge-camera-ffmpeg) | Its configuration requires an FFmpeg input source, usually an existing camera URL. This plugin obtains the selected supported Xiaomi camera's remote P2P source through account/home selection instead of requiring the user to supply that URL. |
|
|
43
|
+
| [camera.ui](https://github.com/SeydX/homebridge-camera-ui) | Its documentation targets cameras exposing RTSP and includes recording, HSV and motion features. This plugin targets the Xiaomi remote transport and does not implement HSV or recording. |
|
|
44
|
+
| [homebridge-miot](https://github.com/merdok/homebridge-miot) | A broad Xiaomi MIOT device-control platform that explicitly lists a camera category. Its current [CameraAccessory](https://github.com/merdok/homebridge-miot/blob/main/lib/modules/camera/CameraAccessory.js) builds a power switch, while [CameraDevice](https://github.com/merdok/homebridge-miot/blob/main/lib/modules/camera/CameraDevice.js) monitors MIOT power/indicator/storage properties; those classes do not implement a HomeKit streaming delegate. Our request specifically adds the tested remote camera video/audio/talkback transport and scoped metadata, rather than claiming that homebridge-miot has no camera support. |
|
|
45
|
+
|
|
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
|
+
|
|
48
|
+
## Scope that must remain explicit
|
|
49
|
+
|
|
50
|
+
- 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).
|
|
51
|
+
- The GUI importer accepts only already-held matching files inside Homebridge storage, verifies fixed hashes and preserves legal files. It solves terminal-free import; it does not solve lawful acquisition for a new user without components. Reviewers may regard that remaining setup limitation as blocking.
|
|
52
|
+
- Mi Home metadata polls every 60 seconds. Confirmed owner-only edits cover the device name and moves to existing rooms of the same home; cloud-side name normalization can reject exact confirmation even for locally valid Unicode; the runtime must expose the validated write capability before editing is enabled. Shared homes are read-only. There is no room rename/create or cross-home move.
|
|
53
|
+
- 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
|
+
- Matter remains an experimental development device using SDK test attestation. Homebridge verification, if granted, would not confer Matter product certification.
|
|
55
|
+
|
|
56
|
+
## Validation and release checklist
|
|
57
|
+
|
|
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.
|
|
59
|
+
|
|
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.
|
|
61
|
+
|
|
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.
|
|
63
|
+
|
|
64
|
+
- [x] Publish the reviewed source to the intended public GitHub repository, enable Issues, and verify all package URLs resolve.
|
|
65
|
+
- [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.
|
|
67
|
+
- [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.
|
|
69
|
+
- [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.
|
|
71
|
+
|
|
72
|
+
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
|
+
|
|
74
|
+
The 0.4.1 patch accepts a Homebridge-configured storage-root symlink such as Synology's default alias, while still refusing user-selected symlink descendants, escapes and root-directory imports. Component import and repeat import through that alias are covered by independent fixtures. The protected settings UI and its name/room region were confirmed visible by the user on the real NAS; active custom-UI IPC returned current metadata and import readiness without errors.
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
<p class="intro">登录你的小米账号,选择家庭和摄像头。视频由此插件连接米家远程中继,再接入 Apple 家庭。</p>
|
|
16
16
|
<div id="runtimeStatus" class="status"><span class="dot"></span><div><span id="runtimeText">正在检查插件状态…</span><div id="connectionDetail" class="small"></div><div id="mediaDetail" class="small"></div><div id="audioDetail" class="small"></div></div></div>
|
|
17
17
|
<details style="margin:-8px 0 20px"><summary style="cursor:pointer;color:var(--muted)">连接诊断</summary><p class="small" style="margin-top:10px">以下报告只包含连接阶段和状态,可复制用于排查。</p><pre id="diagnosticsReport" style="white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;padding:12px;background:var(--panel);border-radius:8px">尚未读取到连接状态。</pre><button id="copyDiagnostics" class="secondary" disabled>复制脱敏诊断</button><p id="diagnosticsHint" class="small" aria-live="polite" style="margin-top:8px"></p></details>
|
|
18
|
-
<section id="components" class="hide"><div class="step">首次设置 · 本地组件</div><h2>导入已有的运行包</h2
|
|
18
|
+
<section id="components" class="hide"><div class="step">首次设置 · 本地组件</div><h2>导入已有的运行包</h2>
|
|
19
|
+
<p class="small">请先将你已持有的运行包放入 Homebridge 数据目录,再填写目录内的文件名。导入只读取本地组件并核验完整性,不读取旧包中的账号信息。</p>
|
|
20
|
+
<label for="componentSource">本地运行包文件名或目录内路径</label><input id="componentSource" maxlength="4096" value="homebridge-mijia-remote-camera-0.1.0.tgz" autocomplete="off" spellcheck="false">
|
|
21
|
+
<div class="row"><button id="componentImport">导入并校验组件</button></div><p id="componentState" class="small" role="status" aria-live="polite">运行组件不随公开 npm 包分发;没有已有组件时,此插件暂时不能提供视频。</p>
|
|
22
|
+
</section>
|
|
19
23
|
<section id="bootstrap" class="hide"><div class="step">首次设置</div><h2>先启用插件</h2><p class="small">保存基本配置后,在 Homebridge 首页重新启动。然后回到这里登录小米账号。</p><button id="enable">启用并保存配置</button><div class="row"><button id="check" class="secondary">已重新启动,检查连接</button></div><p id="bootstrapText" class="small" style="margin-top:12px"></p></section>
|
|
20
24
|
<section><div class="step">01 · 小米账号</div><h2>通过官方页面登录</h2>
|
|
21
25
|
<label for="region">米家 App 的地区</label><select id="region"><option value="cn">中国大陆</option><option value="sg">新加坡</option><option value="de">欧洲(德国)</option><option value="us">美国</option><option value="ru">俄罗斯</option><option value="i2">印度</option></select>
|
|
@@ -27,6 +31,17 @@
|
|
|
27
31
|
<label for="cameraName">Apple 家庭中的名称</label><input id="cameraName" maxlength="64" placeholder="选择摄像头后填写" disabled>
|
|
28
32
|
<div class="row"><button id="save" disabled>使用这台摄像头</button></div><p id="selectionState" class="small" style="margin-top:12px"></p>
|
|
29
33
|
</section>
|
|
34
|
+
<section id="metadataSection"><div class="step">米家资料同步</div><h2>当前摄像头的名称与房间</h2>
|
|
35
|
+
<p id="metadataCurrent" class="small">尚未读取米家资料。</p>
|
|
36
|
+
<label><input id="metadataEnabled" type="checkbox" style="width:auto" disabled> 自动读取米家名称和房间(每 60 秒)</label>
|
|
37
|
+
<div class="row"><button id="metadataRefresh" class="secondary" disabled>刷新米家资料</button><button id="metadataDiscard" class="secondary hide">放弃草稿,采用米家资料</button></div>
|
|
38
|
+
<label for="metadataName">米家中的摄像头名称</label><input id="metadataName" maxlength="160" disabled autocomplete="off">
|
|
39
|
+
<div class="row"><button id="metadataRename" disabled>修改米家名称</button></div>
|
|
40
|
+
<label for="metadataRoom">米家中的所属房间</label><select id="metadataRoom" disabled><option value="">尚未读取房间</option></select>
|
|
41
|
+
<div class="row"><button id="metadataMove" disabled>移到选定米家房间</button></div>
|
|
42
|
+
<p id="metadataState" class="small" role="status" aria-live="polite">正在读取同步状态…</p>
|
|
43
|
+
<p class="small">修改会写入米家;本页只管理当前摄像头,不新建或删除房间。Apple“家庭”的自定义名称和房间不会随插件自动改变。</p>
|
|
44
|
+
</section>
|
|
30
45
|
<section><div class="step">03 · 声音与夜视</div><h2>摄像头功能</h2>
|
|
31
46
|
<label for="nightMode">夜视模式</label><select id="nightMode" disabled><option value="auto">自动</option><option value="on">开启</option><option value="off">关闭</option></select>
|
|
32
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>
|
|
@@ -263,9 +278,9 @@
|
|
|
263
278
|
function options(select,items,render,empty){select.replaceChildren(); if(!items.length){const option=document.createElement('option');option.textContent=empty;select.append(option);select.disabled=true;return;} items.forEach((item,index)=>{const option=document.createElement('option');option.value=String(index);option.textContent=render(item);select.append(option)});select.disabled=false;}
|
|
264
279
|
function chosenHome(){return homes[Number($('homes').value)];}
|
|
265
280
|
function chosenDevice(){return devices[Number($('devices').value)];}
|
|
266
|
-
function setLoginBusy(busy){authBusy=busy;$('login').disabled=busy||!connected; $('load').disabled=busy||!connected||regionNeedsLogin; $('region').disabled=busy; $('cancel').classList.toggle('hide',!busy);renderMatter();renderPtz();}
|
|
281
|
+
function setLoginBusy(busy){authBusy=busy;if(busy||regionNeedsLogin)resetMetadata();$('login').disabled=busy||!connected; $('load').disabled=busy||!connected||regionNeedsLogin; $('region').disabled=busy; $('cancel').classList.toggle('hide',!busy);renderMatter();renderPtz();}
|
|
267
282
|
function hideLoginLink(){$('loginLink').classList.add('hide');$('officialLogin').removeAttribute('href');}
|
|
268
|
-
async function status(){try{const value=await request('/status');connected=true;$('components').classList.add('hide');$('bootstrap').classList.add('hide');if(regions.includes(value.region)){runtimeRegion=value.region;if(!regionTouched&&!authBusy)$('region').value=runtimeRegion;}regionNeedsLogin=$('region').value!==runtimeRegion;matterValue=safeMatterStatus(value.matter);setLoginBusy(authBusy);diagnosticValue=safeDiagnostics(value);statusReceivedAt=Date.now();$('diagnosticsReport').textContent=JSON.stringify(diagnosticValue,null,2);$('copyDiagnostics').disabled=false;renderRuntime();renderMatter();await readPtzCapabilities();}catch(error){connected=false;resetPtz();matterValue={};clearMatterPairing();renderMatter();$('login').disabled=true;$('load').disabled=true;$('runtimeStatus').dataset.live='false';$('runtimeText').textContent='等待插件启动';$('connectionDetail').textContent='';$('mediaDetail').textContent='';showError(error);}}
|
|
283
|
+
async function status(){try{const value=await request('/status');connected=true;$('components').classList.add('hide');$('bootstrap').classList.add('hide');if(regions.includes(value.region)){runtimeRegion=value.region;if(!regionTouched&&!authBusy)$('region').value=runtimeRegion;}regionNeedsLogin=$('region').value!==runtimeRegion;matterValue=safeMatterStatus(value.matter);setLoginBusy(authBusy);diagnosticValue=safeDiagnostics(value);statusReceivedAt=Date.now();$('diagnosticsReport').textContent=JSON.stringify(diagnosticValue,null,2);$('copyDiagnostics').disabled=false;renderRuntime();renderMatter();await readPtzCapabilities();await readMetadataStatus();}catch(error){connected=false;resetMetadata();resetPtz();matterValue={};clearMatterPairing();renderMatter();$('login').disabled=true;$('load').disabled=true;$('runtimeStatus').dataset.live='false';$('runtimeText').textContent='等待插件启动';$('connectionDetail').textContent='';$('mediaDetail').textContent='';showError(error);}}
|
|
269
284
|
$('copyDiagnostics').onclick=async()=>{try{if(typeof navigator==='undefined'||typeof navigator.clipboard?.writeText!=='function')throw Error('clipboard_unavailable');await navigator.clipboard.writeText($('diagnosticsReport').textContent);$('diagnosticsHint').textContent='已复制脱敏诊断。';}catch{$('diagnosticsHint').textContent='浏览器无法自动复制,请手动选中上方诊断报告并复制。';}};
|
|
270
285
|
async function loadHomes(){resetPtz(true);clearMatterPairing();clearError();$('selectionState').textContent='正在读取家庭…';try{const response=await request('/homes');homes=Array.isArray(response)?response:(response.homes||[]);options($('homes'),homes,home=>(home.name||'未命名家庭')+(home.shared?' · 共享家庭':''),'当前地区没有可访问的家庭');$('selectionState').textContent=homes.length?'请选择目标家庭。':'请检查地区及账号的家庭访问权限。';devices=[];options($('devices'),[],()=>'', '先选择家庭');$('save').disabled=true;$('cameraName').disabled=true;if(homes.length)await loadDevices();}catch(error){$('selectionState').textContent='';showError(error);}}
|
|
271
286
|
async function loadDevices(){const home=chosenHome();if(!home)return;resetPtz(true);clearMatterPairing();clearError();$('selectionState').textContent='正在读取此家庭的摄像头…';$('save').disabled=true;try{const response=await request('/devices',{homeId:String(home.id),homeOwner:String(home.ownerId)});devices=Array.isArray(response)?response:(response.devices||[]);options($('devices'),devices,device=>(device.name||device.model||'摄像头')+(device.online===false?' · 离线':''),'此家庭没有摄像头');$('selectionState').textContent='';deviceChanged();}catch(error){devices=[];options($('devices'),[],()=>'', '读取设备失败');$('selectionState').textContent='';showError(error);}}
|
|
@@ -273,8 +288,85 @@
|
|
|
273
288
|
async function pollAuth(){try{const value=await request('/auth/status',{loginId});if(value.state==='waiting'||value.state==='starting'){$('authState').textContent=`等待你在小米页面确认,剩余 ${value.remainingSeconds||0} 秒。`;pollTimer=setTimeout(pollAuth,1200);return;}setLoginBusy(false);hideLoginLink();if(value.success||value.state==='complete'){$('authState').textContent='登录成功。正在读取家庭列表…';await status();await loadHomes();}else $('authState').textContent=messages[value.reason]||'本次登录未完成,可重新生成链接。';}catch(error){setLoginBusy(false);hideLoginLink();showError(error);}}
|
|
274
289
|
$('login').onclick=async()=>{resetNight();clearError();hideLoginLink();clearTimeout(pollTimer);setLoginBusy(true);$('authState').textContent='正在生成官方登录链接…';try{const value=await request('/auth/start',{region:$('region').value});const url=new URL(value.loginUrl);if(url.protocol!=='https:'||!(url.hostname==='mi.com'||url.hostname.endsWith('.mi.com')||url.hostname==='xiaomi.com'||url.hostname.endsWith('.xiaomi.com')))throw Error('unsafe_url');loginId=value.loginId;$('officialLogin').href=url.href;$('loginLink').classList.remove('hide');$('authState').textContent='请打开链接完成授权。';pollTimer=setTimeout(pollAuth,1000);}catch(error){setLoginBusy(false);showError(error);$('authState').textContent='';}};
|
|
275
290
|
$('cancel').onclick=async()=>{clearTimeout(pollTimer);try{await request('/auth/cancel',{loginId});}catch(error){showError(error);}hideLoginLink();setLoginBusy(false);$('authState').textContent='已取消。已有登录数据保留。';};
|
|
291
|
+
let metadataValue={},metadataBusy=false,metadataDraft=false,metadataDraftGuard=null,metadataGeneration=0,metadataNotice='';
|
|
292
|
+
const metadataErrors={metadata_unavailable:'米家资料暂不可用,请稍后刷新。',metadata_incomplete:'米家返回的房间资料不完整,尚未采用。',metadata_conflict:'米家资料已变化,草稿已保留。请核对后重新编辑。',metadata_busy:'上一项资料操作尚未完成,请稍后再试。',metadata_write_unsupported:'当前环境尚未验证米家写回,编辑功能未启用。',metadata_not_owner:'当前账号不是此家庭的所有者,暂时只读。',metadata_unconfirmed:'写入结果尚未确认。请先刷新米家资料,不要重复提交。',metadata_not_applied:'米家尚未回读到修改后的资料,请刷新核对。',metadata_rejected:'米家未接受修改,请检查账号权限。'};
|
|
293
|
+
const metadataGuard=()=>({expectedIdentity:metadataValue.identity,expectedSourceEpoch:metadataValue.sourceEpoch,expectedRevision:metadataValue.revision});
|
|
294
|
+
function metadataAllowed(){return connected&&!authBusy&&!regionNeedsLogin&&!metadataBusy&&!!metadataValue.metadata;}
|
|
295
|
+
function renderMetadata(){
|
|
296
|
+
const v=metadataValue,m=v.metadata,c=v.capabilities||{},ready=metadataAllowed();
|
|
297
|
+
$('metadataEnabled').checked=v.enabled===true;$('metadataEnabled').disabled=!connected||authBusy||regionNeedsLogin||metadataBusy||c.syncConfig!==true;
|
|
298
|
+
$('metadataRefresh').disabled=!connected||authBusy||regionNeedsLogin||metadataBusy||c.read!==true;
|
|
299
|
+
const conflict=metadataDraft&&metadataDraftGuard&&(metadataDraftGuard.expectedIdentity!==v.identity||metadataDraftGuard.expectedSourceEpoch!==v.sourceEpoch||metadataDraftGuard.expectedRevision!==v.revision);
|
|
300
|
+
$('metadataCurrent').textContent=m?`当前米家名称:${m.name} · 家庭:${m.homeName} · 房间:${m.roomName||'未分房'}`:'尚未读取当前摄像头的米家资料。';
|
|
301
|
+
if(!metadataDraft){
|
|
302
|
+
$('metadataName').value=m?.name||'';$('metadataRoom').replaceChildren();
|
|
303
|
+
const empty=document.createElement('option');empty.value='';empty.textContent='未分房(只读)';empty.disabled=true;$('metadataRoom').append(empty);
|
|
304
|
+
for(const room of Array.isArray(m?.rooms)?m.rooms:[]){const item=document.createElement('option');item.value=room.id;item.textContent=room.name;$('metadataRoom').append(item);}
|
|
305
|
+
$('metadataRoom').value=m?.roomId||'';
|
|
306
|
+
}
|
|
307
|
+
$('metadataName').disabled=!ready||c.rename!==true;$('metadataRoom').disabled=!ready||c.move!==true;
|
|
308
|
+
$('metadataRename').disabled=!ready||c.rename!==true||!!conflict||!$('metadataName').value.trim();
|
|
309
|
+
$('metadataMove').disabled=!ready||c.move!==true||!!conflict||!$('metadataRoom').value;
|
|
310
|
+
$('metadataDiscard').classList.toggle('hide',!metadataDraft);
|
|
311
|
+
if(metadataBusy)$('metadataState').textContent='正在处理米家资料…';
|
|
312
|
+
else if(conflict)$('metadataState').textContent=metadataErrors.metadata_conflict;
|
|
313
|
+
else if(metadataNotice)$('metadataState').textContent=metadataNotice;
|
|
314
|
+
else if(v.lastError)$('metadataState').textContent=metadataErrors[v.lastError]||messages[v.lastError]||metadataErrors.metadata_unavailable;
|
|
315
|
+
else if(!m)$('metadataState').textContent='选择摄像头后可读取米家名称和房间。';
|
|
316
|
+
else $('metadataState').textContent=(v.loading?'正在读取米家资料…':`已读取米家资料${v.lastSuccessAt?' · '+v.lastSuccessAt:''}`)+(c.rename===true&&c.move===true?'':' · 当前仅支持读取。');
|
|
317
|
+
}
|
|
318
|
+
function resetMetadata(){metadataGeneration++;metadataValue={};metadataDraft=false;metadataDraftGuard=null;metadataNotice='';renderMetadata();}
|
|
319
|
+
async function readMetadataStatus(){
|
|
320
|
+
if(!connected||authBusy||regionNeedsLogin){resetMetadata();return;}
|
|
321
|
+
const generation=metadataGeneration;
|
|
322
|
+
try{const value=await request('/metadata/status');if(generation!==metadataGeneration)return;metadataValue=value&&typeof value==='object'?value:{};renderMetadata();}
|
|
323
|
+
catch{if(generation===metadataGeneration){metadataValue={};renderMetadata();}}
|
|
324
|
+
}
|
|
325
|
+
function metadataEdited(){metadataNotice='';if(!metadataDraft){metadataDraftGuard=metadataGuard();metadataDraft=true;}renderMetadata();}
|
|
326
|
+
$('metadataName').oninput=metadataEdited;$('metadataRoom').onchange=metadataEdited;
|
|
327
|
+
$('metadataDiscard').onclick=()=>{metadataDraft=false;metadataDraftGuard=null;metadataNotice='';renderMetadata();};
|
|
328
|
+
$('metadataRefresh').onclick=async()=>{
|
|
329
|
+
if(metadataBusy)return;const generation=metadataGeneration;metadataNotice='';metadataBusy=true;renderMetadata();
|
|
330
|
+
try{await request('/metadata/refresh');if(generation!==metadataGeneration)return;await readMetadataStatus();}
|
|
331
|
+
catch(error){if(generation===metadataGeneration)metadataNotice=metadataErrors[reason(error)]||messages[reason(error)]||metadataErrors.metadata_unavailable;}
|
|
332
|
+
finally{metadataBusy=false;renderMetadata();}
|
|
333
|
+
};
|
|
334
|
+
$('metadataEnabled').onchange=async()=>{
|
|
335
|
+
if(metadataBusy)return;const generation=metadataGeneration,enabled=$('metadataEnabled').checked;metadataNotice='';metadataBusy=true;renderMetadata();
|
|
336
|
+
try{const value=await request('/metadata/sync',{...metadataGuard(),enabled});if(generation!==metadataGeneration)return;metadataValue=value;}
|
|
337
|
+
catch(error){if(generation===metadataGeneration)metadataNotice=metadataErrors[reason(error)]||messages[reason(error)]||metadataErrors.metadata_unavailable;}
|
|
338
|
+
finally{metadataBusy=false;renderMetadata();}
|
|
339
|
+
};
|
|
340
|
+
async function updateMetadata(operation){
|
|
341
|
+
if(!metadataAllowed()||metadataValue.capabilities?.[operation=== 'rename'?'rename':'move']!==true)return;
|
|
342
|
+
const requestedName=$('metadataName').value;
|
|
343
|
+
if(operation==='rename'&&(!requestedName.length||requestedName.length>160||!requestedName.isWellFormed()||requestedName.trim()!==requestedName||/[\p{Cc}]/u.test(requestedName))){metadataNotice='名称须为 1–160 个字符,且不含首尾空白或控制字符。';renderMetadata();return;}
|
|
344
|
+
const generation=metadataGeneration,guard=metadataDraftGuard||metadataGuard();metadataNotice='';metadataBusy=true;renderMetadata();
|
|
345
|
+
try{
|
|
346
|
+
const payload=operation==='rename'?{name:$('metadataName').value}:{roomId:$('metadataRoom').value};
|
|
347
|
+
const result=await request('/metadata/update',{...guard,operation,...payload});
|
|
348
|
+
if(generation!==metadataGeneration)return;
|
|
349
|
+
if(result?.confirmed!==true)throw {reason:'metadata_unconfirmed'};
|
|
350
|
+
metadataDraft=false;metadataDraftGuard=null;await readMetadataStatus();if(generation!==metadataGeneration)return;metadataNotice='已在米家回读确认修改。';
|
|
351
|
+
}catch(error){if(generation===metadataGeneration)metadataNotice=metadataErrors[reason(error)]||messages[reason(error)]||metadataErrors.metadata_unavailable;}
|
|
352
|
+
finally{metadataBusy=false;if(generation===metadataGeneration)renderMetadata();}
|
|
353
|
+
}
|
|
354
|
+
$('metadataRename').onclick=()=>updateMetadata('rename');$('metadataMove').onclick=()=>updateMetadata('move');
|
|
355
|
+
let componentTimer,componentBusy=false;
|
|
356
|
+
const componentErrors={invalid_request:'请填写 Homebridge 数据目录内的运行包文件名。',runtime_components_missing:'未找到完整运行组件,请检查本地运行包。',runtime_components_invalid:'组件校验失败,请保留原目录并核对运行包。',runtime_in_use:'插件正在使用组件目录。请先停用米家平台并重启 Homebridge,再导入。',storage_failed:'无法访问 Homebridge 数据目录,请检查权限。',setup_failed:'本地导入未完成,请检查运行包后重试。'};
|
|
357
|
+
function renderComponentImport(value){
|
|
358
|
+
componentBusy=value?.state==='running';$('componentSource').disabled=componentBusy;$('componentImport').disabled=componentBusy;
|
|
359
|
+
$('componentState').textContent=componentBusy?'正在校验并导入本地组件,请稍候…':value?.success===true?'组件已完成校验。请重启摄像头子桥,然后回到本页登录。':value?.state==='error'?(componentErrors[value.reason]||componentErrors.setup_failed):'运行组件不随公开 npm 包分发;请提供你已持有的本地运行包。';
|
|
360
|
+
clearTimeout(componentTimer);if(componentBusy)componentTimer=setTimeout(pollComponentImport,1000);
|
|
361
|
+
}
|
|
362
|
+
async function pollComponentImport(){try{renderComponentImport(await request('/components/import-status'));}catch{$('componentState').textContent='暂时无法读取导入进度,请重新打开设置页检查。';componentBusy=false;$('componentSource').disabled=false;$('componentImport').disabled=false;}}
|
|
363
|
+
$('componentImport').onclick=async()=>{
|
|
364
|
+
if(componentBusy)return;componentBusy=true;$('componentImport').disabled=true;$('componentSource').disabled=true;
|
|
365
|
+
try{renderComponentImport(await request('/components/import',{source:$('componentSource').value.trim()}));}
|
|
366
|
+
catch{renderComponentImport({state:'error',reason:'setup_failed'});}
|
|
367
|
+
};
|
|
276
368
|
$('region').onchange=()=>{resetNight();regionTouched=true;regionNeedsLogin=$('region').value!==runtimeRegion;setLoginBusy(authBusy);$('authState').textContent=regionNeedsLogin?'已切换地区,请生成新登录链接后读取家庭。':'';}; $('load').onclick=loadHomes; $('homes').onchange=loadDevices; $('devices').onchange=deviceChanged;
|
|
277
|
-
$('save').onclick=async()=>{const home=chosenHome(),device=chosenDevice();if(!home||!device)return;resetPtz(true);clearMatterPairing();clearError();$('save').disabled=true;try{const result=await request('/select',{homeId:String(home.id),homeOwner:String(home.ownerId),deviceId:String(device.deviceId||device.did),name:$('cameraName').value.trim()||device.name,model:device.model,region:$('region').value});resetNight();ptzSelectionDirty=Boolean(result.restartRequired);$('selectionState').textContent=result.restartRequired?'已保存新的摄像头。请重启 Homebridge 使配件变更生效。':'选择已保存,正在连接摄像头。配对码见摄像头启动日志中的 Setup Code。';await status();}catch(error){showError(error);}finally{$('save').disabled=false;}};
|
|
369
|
+
$('save').onclick=async()=>{const home=chosenHome(),device=chosenDevice();if(!home||!device)return;resetMetadata();resetPtz(true);clearMatterPairing();clearError();$('save').disabled=true;try{const result=await request('/select',{homeId:String(home.id),homeOwner:String(home.ownerId),deviceId:String(device.deviceId||device.did),name:$('cameraName').value.trim()||device.name,model:device.model,region:$('region').value});resetNight();ptzSelectionDirty=Boolean(result.restartRequired);$('selectionState').textContent=result.restartRequired?'已保存新的摄像头。请重启 Homebridge 使配件变更生效。':'选择已保存,正在连接摄像头。配对码见摄像头启动日志中的 Setup Code。';await status();}catch(error){showError(error);}finally{$('save').disabled=false;}};
|
|
278
370
|
$('enable').onclick=async()=>{clearError();try{if(!configs.length){configs=[{platform:'MijiaRemoteCamera',name:'米家远程摄像头',region:$('region').value}];await homebridge.updatePluginConfig(configs);}await homebridge.savePluginConfig();$('bootstrapText').textContent='已保存。请在 Homebridge 首页重新启动,再回到这里检查连接。';}catch(error){showError(error);}};
|
|
279
371
|
$('check').onclick=status;
|
|
280
372
|
function resetNight(){resetPtz();clearMatterPairing();++nightGeneration;$('nightMode').disabled=true;$('nightSave').disabled=true;$('nightState').textContent='读取摄像头实际模式后可修改。';}
|
|
@@ -348,8 +440,8 @@
|
|
|
348
440
|
}
|
|
349
441
|
for(const direction of ptzDirections)$('ptz'+direction[0].toUpperCase()+direction.slice(1)).onclick=()=>movePtz(direction);
|
|
350
442
|
$('ptzRead').onclick=readPtzCapabilities;
|
|
351
|
-
async function init(){try{homebridge.hideSchemaForm();try{const components=await request('/components');if(components.ready===false)$('components').classList.remove('hide');}catch{}configs=await homebridge.getPluginConfig();if(!Array.isArray(configs))configs=[];for(const key of ['enableAudio','enableTalkback','enableNightVision'])$(key).checked=configs[0]?.[key]!==false;$('enablePtzSwitches').checked=configs[0]?.enablePtzSwitches===true;$('enableTalkback').disabled=!$('enableAudio').checked;$('enableMatter').checked=configs[0]?.enableMatter===true;$('matterPort').value=String(Number.isInteger(configs[0]?.matterPort)?configs[0].matterPort:5540);$('matterInterface').value=typeof configs[0]?.matterInterface==='string'?configs[0].matterInterface:'eth0';const region=configs[0]?.region;if(regions.includes(region))$('region').value=region;if(!configs.length)$('bootstrap').classList.remove('hide');await status();try{const pending=await request('/auth/status');if(pending.state==='waiting'){loginId=pending.loginId;setLoginBusy(true);$('authState').textContent='先前的授权仍在等待确认。也可取消后生成新链接。';pollTimer=setTimeout(pollAuth,1000);}}catch{}statusTimer=setInterval(status,5000);clockTimer=setInterval(()=>{if(connected)renderRuntime();},1000);}catch(error){showError(error);}}
|
|
352
|
-
window.addEventListener('beforeunload',()=>{resetPtz();clearMatterPairing();clearInterval(statusTimer);clearInterval(clockTimer);clearTimeout(pollTimer);});
|
|
443
|
+
async function init(){try{homebridge.hideSchemaForm();try{const components=await request('/components');if(components.ready===false){$('components').classList.remove('hide');await pollComponentImport();}}catch{}configs=await homebridge.getPluginConfig();if(!Array.isArray(configs))configs=[];for(const key of ['enableAudio','enableTalkback','enableNightVision'])$(key).checked=configs[0]?.[key]!==false;$('enablePtzSwitches').checked=configs[0]?.enablePtzSwitches===true;$('enableTalkback').disabled=!$('enableAudio').checked;$('enableMatter').checked=configs[0]?.enableMatter===true;$('matterPort').value=String(Number.isInteger(configs[0]?.matterPort)?configs[0].matterPort:5540);$('matterInterface').value=typeof configs[0]?.matterInterface==='string'?configs[0].matterInterface:'eth0';const region=configs[0]?.region;if(regions.includes(region))$('region').value=region;if(!configs.length)$('bootstrap').classList.remove('hide');await status();try{const pending=await request('/auth/status');if(pending.state==='waiting'){loginId=pending.loginId;setLoginBusy(true);$('authState').textContent='先前的授权仍在等待确认。也可取消后生成新链接。';pollTimer=setTimeout(pollAuth,1000);}}catch{}statusTimer=setInterval(status,5000);clockTimer=setInterval(()=>{if(connected)renderRuntime();},1000);}catch(error){showError(error);}}
|
|
444
|
+
window.addEventListener('beforeunload',()=>{resetPtz();metadataGeneration++;clearMatterPairing();clearInterval(statusTimer);clearInterval(clockTimer);clearTimeout(pollTimer);clearTimeout(componentTimer);});
|
|
353
445
|
init();
|
|
354
446
|
})();
|
|
355
447
|
</script></body></html>
|
package/homebridge-ui/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
4
|
import { HomebridgePluginUiServer, RequestError } from '@homebridge/plugin-ui-utils';
|
|
5
|
+
import { RuntimeSetupJob } from '../lib/setup-job.mjs';
|
|
5
6
|
|
|
6
7
|
const SAFE_REASONS = new Set(['plugin_not_running', 'invalid_request', 'unauthenticated', 'auth_required',
|
|
7
8
|
'home_request_failed', 'home_request_rejected', 'session_expired', 'invalid_home_selection',
|
|
@@ -13,6 +14,9 @@ const SAFE_REASONS = new Set(['plugin_not_running', 'invalid_request', 'unauthen
|
|
|
13
14
|
'unsupported_transport', 'no_video', 'connect_timeout', 'backend_failed', 'storage_failed',
|
|
14
15
|
'runtime_in_use', 'query_timeout', 'unsupported_platform', 'bundled_runtime_missing', 'not_selected', 'auth_in_progress',
|
|
15
16
|
'runtime_components_missing', 'runtime_components_invalid', 'night_vision_unsupported', 'night_vision_unavailable',
|
|
17
|
+
'metadata_unavailable', 'metadata_incomplete', 'metadata_conflict', 'metadata_busy', 'metadata_read_only',
|
|
18
|
+
'metadata_rejected', 'metadata_unconfirmed', 'metadata_not_applied',
|
|
19
|
+
'metadata_write_unsupported', 'metadata_not_owner',
|
|
16
20
|
'night_vision_rejected', 'night_vision_not_applied', 'night_vision_timeout',
|
|
17
21
|
'ptz_unsupported', 'ptz_unavailable', 'ptz_rejected', 'ptz_timeout', 'ptz_busy',
|
|
18
22
|
'matter_disabled', 'matter_not_ready', 'matter_start_failed', 'matter_port_in_use', 'matter_configuration_invalid',
|
|
@@ -39,6 +43,24 @@ function loginId(value) {
|
|
|
39
43
|
return value;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
function metadataGuard(payload, required = false) {
|
|
47
|
+
const result = {};
|
|
48
|
+
if (payload?.expectedRevision !== undefined) {
|
|
49
|
+
if (!Number.isSafeInteger(payload.expectedRevision) || payload.expectedRevision < 0) throw new Error('invalid_request');
|
|
50
|
+
result.expectedRevision = payload.expectedRevision;
|
|
51
|
+
}
|
|
52
|
+
if (payload?.expectedSourceEpoch !== undefined) {
|
|
53
|
+
if (typeof payload.expectedSourceEpoch !== 'string' || !/^[a-f0-9-]{36}$/.test(payload.expectedSourceEpoch)) throw new Error('invalid_request');
|
|
54
|
+
result.expectedSourceEpoch = payload.expectedSourceEpoch;
|
|
55
|
+
}
|
|
56
|
+
if (payload?.expectedIdentity !== undefined) {
|
|
57
|
+
if (typeof payload.expectedIdentity !== 'string' || !/^[a-f0-9]{64}$/.test(payload.expectedIdentity)) throw new Error('invalid_request');
|
|
58
|
+
result.expectedIdentity = payload.expectedIdentity;
|
|
59
|
+
}
|
|
60
|
+
if (required && ['expectedIdentity', 'expectedSourceEpoch', 'expectedRevision'].some(key => result[key] === undefined)) throw new Error('invalid_request');
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
/** Pure proxy: the active Homebridge plugin owns all auth jobs and Java processes. */
|
|
43
65
|
export function createUiHandlers(runtime) {
|
|
44
66
|
const routes = {
|
|
@@ -55,6 +77,24 @@ export function createUiHandlers(runtime) {
|
|
|
55
77
|
deviceId: id(payload?.deviceId), name: label(payload?.name, '米家摄像头'), model: label(payload?.model, 'Xiaomi Camera'),
|
|
56
78
|
region: ['cn', 'de', 'us', 'ru', 'sg', 'i2'].includes(payload?.region) ? payload.region : 'cn' }),
|
|
57
79
|
'/status': () => runtime.request('status', {}),
|
|
80
|
+
'/metadata/status': () => runtime.request('metadata.status', {}),
|
|
81
|
+
'/metadata/refresh': payload => runtime.request('metadata.refresh', metadataGuard(payload)),
|
|
82
|
+
'/metadata/sync': payload => {
|
|
83
|
+
if (typeof payload?.enabled !== 'boolean') throw new Error('invalid_request');
|
|
84
|
+
return runtime.request('metadata.sync', { ...metadataGuard(payload), enabled: payload.enabled });
|
|
85
|
+
},
|
|
86
|
+
'/metadata/update': payload => {
|
|
87
|
+
const guard = metadataGuard(payload, true);
|
|
88
|
+
if (payload?.operation === 'rename') {
|
|
89
|
+
if (typeof payload.name !== 'string' || !payload.name.length || payload.name.length > 160
|
|
90
|
+
|| !payload.name.isWellFormed() || payload.name.trim() !== payload.name || /[\p{Cc}]/u.test(payload.name)) throw new Error('invalid_request');
|
|
91
|
+
return runtime.request('metadata.update', { ...guard, operation: 'rename', name: payload.name });
|
|
92
|
+
}
|
|
93
|
+
if (payload?.operation === 'move') {
|
|
94
|
+
return runtime.request('metadata.update', { ...guard, operation: 'move', roomId: id(payload.roomId) });
|
|
95
|
+
}
|
|
96
|
+
throw new Error('invalid_request');
|
|
97
|
+
},
|
|
58
98
|
'/matter/pairing': () => runtime.request('matter.pairing', {}),
|
|
59
99
|
'/matter/open-window': () => runtime.request('matter.openWindow', {}),
|
|
60
100
|
'/matter/close-window': () => runtime.request('matter.closeWindow', {}),
|
|
@@ -84,6 +124,9 @@ export async function runUiServer() {
|
|
|
84
124
|
super();
|
|
85
125
|
const storagePath = this.homebridgeStoragePath;
|
|
86
126
|
if (!storagePath) throw new Error('plugin_not_running');
|
|
127
|
+
const setup = new RuntimeSetupJob({ homebridgeStoragePath: storagePath });
|
|
128
|
+
this.onRequest('/components/import', payload => setup.start(payload));
|
|
129
|
+
this.onRequest('/components/import-status', () => setup.safeStatus());
|
|
87
130
|
const client = createRpcClient(join(storagePath, 'mijia-remote-camera'));
|
|
88
131
|
for (const [path, handler] of Object.entries(createUiHandlers(client))) this.onRequest(path, handler);
|
|
89
132
|
this.onRequest('/components', async () => {
|
package/lib/matter-device.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { WebRtcTransportRequestor } from '@matter/main/clusters/web-rtc-transpor
|
|
|
12
12
|
import { Status, StatusResponseError, StreamUsage, VendorId, EndpointNumber } from '@matter/main/types';
|
|
13
13
|
import { assertRemoteActor, hasRemoteActor, NodeSession, Invoke, DeviceCommissioner } from '@matter/main/protocol';
|
|
14
14
|
import { MatterPtzStepControl, MijiaPtzStepBehavior } from './matter-ptz-cluster.mjs';
|
|
15
|
+
import { MatterMetadataControl, MijiaMetadataBehavior } from './matter-metadata-cluster.mjs';
|
|
15
16
|
|
|
16
17
|
const LIVE = StreamUsage.LiveView;
|
|
17
18
|
const NIGHT = ['off', 'on', 'auto'];
|
|
@@ -217,6 +218,15 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
217
218
|
const ptz = new MatterPtzStepControl({ runtime, onChange: state => {
|
|
218
219
|
if (endpoint && !closed) return endpoint.set({ mijiaPtzStep: state });
|
|
219
220
|
} });
|
|
221
|
+
const metadata = new MatterMetadataControl({ runtime, onChange: async state => {
|
|
222
|
+
if (!endpoint || closed) return;
|
|
223
|
+
await endpoint.set({ mijiaMetadata: state });
|
|
224
|
+
if (state.syncEnabled && state.available && state.snapshot.name && server) {
|
|
225
|
+
// Only confirmed cloud metadata updates this advertised label. A controller's
|
|
226
|
+
// direct BasicInformation.NodeLabel write never invokes a Xiaomi cloud write.
|
|
227
|
+
await server.set({ basicInformation: { nodeLabel: text(state.snapshot.name, 'Mijia Camera') } });
|
|
228
|
+
}
|
|
229
|
+
} });
|
|
220
230
|
const features = ['Video', 'Audio', 'Snapshot', ...(speakerEnabled ? ['Speaker'] : []), ...(nightEnabled ? ['NightVision'] : [])];
|
|
221
231
|
const dimensions = { width, height };
|
|
222
232
|
const snapshotCapabilities = [{ resolution: dimensions, maxFrameRate: 1, imageCodec: 0,
|
|
@@ -311,6 +321,13 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
311
321
|
await ptz.move(direction);
|
|
312
322
|
}
|
|
313
323
|
}
|
|
324
|
+
class MijiaMetadataServer extends MijiaMetadataBehavior {
|
|
325
|
+
static lockOnInvoke = false;
|
|
326
|
+
async refreshMetadata(request) { actor(this); await metadata.refreshMetadata(request); }
|
|
327
|
+
async setSyncEnabled(request) { actor(this); await metadata.setSyncEnabled(request); }
|
|
328
|
+
async renameDevice(request) { actor(this); await metadata.renameDevice(request); }
|
|
329
|
+
async moveDevice(request) { actor(this); await metadata.moveDevice(request); }
|
|
330
|
+
}
|
|
314
331
|
class MijiaAvServer extends CameraAvStreamManagementServer.with(...features) {
|
|
315
332
|
async initialize() {
|
|
316
333
|
// Allocations hold runtime encoder resources and do not survive a process restart.
|
|
@@ -489,12 +506,13 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
489
506
|
...(config.listeningAddressIpv6 ? { listeningAddressIpv6: config.listeningAddressIpv6 } : {}) },
|
|
490
507
|
basicInformation: { vendorName: 'linanw', vendorId: VendorId(0xfff1), productName: 'Mijia Remote Camera',
|
|
491
508
|
productId: 0x8001, nodeLabel: text(name, 'Mijia Camera'), serialNumber: text(serialNumber, identifier),
|
|
492
|
-
hardwareVersion: 1, softwareVersion:
|
|
509
|
+
hardwareVersion: 1, softwareVersion: 400, softwareVersionString: '0.4.0' },
|
|
493
510
|
});
|
|
494
511
|
const audioCapabilities = { maxNumberOfChannels: 1, supportedCodecs: [0], supportedSampleRates: [48000], supportedBitDepths: [16] };
|
|
495
|
-
endpoint = new Endpoint(CameraDevice.with(MijiaAvServer, MijiaWebRtcServer, WebRtcTransportRequestorClient, MijiaPtzStepServer), {
|
|
512
|
+
endpoint = new Endpoint(CameraDevice.with(MijiaAvServer, MijiaWebRtcServer, WebRtcTransportRequestorClient, MijiaPtzStepServer, MijiaMetadataServer), {
|
|
496
513
|
id: 'camera', number: EndpointNumber(1),
|
|
497
514
|
mijiaPtzStep: ptz.state(),
|
|
515
|
+
mijiaMetadata: metadata.state(),
|
|
498
516
|
cameraAvStreamManagement: {
|
|
499
517
|
maxContentBufferSize: 1000000, maxNetworkBandwidth: 5000000 * maxStreams, maxConcurrentEncoders: maxStreams,
|
|
500
518
|
maxEncodedPixelRate: width * height * fps * maxStreams,
|
|
@@ -524,6 +542,7 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
524
542
|
});
|
|
525
543
|
} catch (error) {
|
|
526
544
|
ptz.close();
|
|
545
|
+
metadata.close();
|
|
527
546
|
await server?.close().catch(() => {});
|
|
528
547
|
throw error;
|
|
529
548
|
}
|
|
@@ -538,6 +557,7 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
538
557
|
if (closed) throw new Error('matter_closed');
|
|
539
558
|
await server.start();
|
|
540
559
|
await ptz.refresh();
|
|
560
|
+
await metadata.refresh();
|
|
541
561
|
if (nightEnabled && typeof runtime?.getNightVision === 'function') {
|
|
542
562
|
const value = await runtime.getNightVision().catch(() => undefined);
|
|
543
563
|
if (value) await nightChanged(value);
|
|
@@ -574,9 +594,11 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
574
594
|
return this.status();
|
|
575
595
|
},
|
|
576
596
|
refreshPtzCapabilities(options) { return ptz.refresh(options); },
|
|
597
|
+
refreshMetadataState(options) { return metadata.refresh(options); },
|
|
577
598
|
async close() {
|
|
578
599
|
if (closed) return;
|
|
579
600
|
ptz.close();
|
|
601
|
+
metadata.close();
|
|
580
602
|
cachedCommissioned = !!server.state.commissioning?.commissioned;
|
|
581
603
|
await Promise.allSettled([...sessions.values()].map(item => end(item, 2, true)));
|
|
582
604
|
closed = true;
|