@linanw/homebridge-mijia-remote-camera 0.1.2 → 0.2.0
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/README.md +24 -0
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/config.schema.json +20 -0
- package/homebridge-ui/public/index.html +74 -9
- package/homebridge-ui/server.js +6 -1
- package/lib/diagnostics.mjs +5 -0
- package/lib/matter-device.mjs +519 -0
- package/lib/matter-diagnostics.mjs +30 -0
- package/lib/matter-media.mjs +296 -0
- package/lib/matter-service.mjs +117 -0
- package/lib/platform.mjs +23 -3
- package/lib/rpc.mjs +4 -2
- package/lib/runtime-factory.mjs +5 -1
- package/npm-shrinkwrap.json +533 -17
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -64,6 +64,29 @@ mijia-camera-setup --from /homebridge/homebridge-mijia-remote-camera-0.1.0.tgz -
|
|
|
64
64
|
|
|
65
65
|
夜视目前仅支持已核对 MIOT 定义的 `chuangmi.camera.061a01`/`chuangmi.camera.061a03`。插件设置页先读取设备实际状态,再选择“自动/开启/关闭”并应用;写入后会回读确认。Apple“家庭”的“夜视”开关表示允许夜视:自动模式显示为开,重复开不会把自动改为强制开启;关掉后重新打开会设置为始终开启。需要恢复自动时,请在插件设置页选择。该开关表示夜视策略,不表示红外灯此刻是否点亮。启动和轮询只读状态,不更改策略。
|
|
66
66
|
|
|
67
|
+
## Matter 摄像头(0.2.0,实验性)
|
|
68
|
+
|
|
69
|
+
可在保留现有 HomeKit 配对的同时启用独立的 Matter Camera(设备类型 `0x0142`,端点 `1`)。它实现 Camera AV Stream Management 与 WebRTC Transport Provider,包含 H.264 视频、Opus 现场声音、双向对讲、JPEG 快照、音量/静音和支持型号的夜视模式。当前使用 matter.js 0.18.0 的 Matter 1.6.1 模型,已与 matter-server 1.5.3 控制器进行协议对接。MatterDesk Qt 客户端是当前目标客户端;客户端需要实现摄像头播放。Apple“家庭”仍使用本插件原有的 HomeKit 配件。
|
|
70
|
+
|
|
71
|
+
在插件设置页勾选“启用 Matter 摄像头”,选定 NAS 的局域网接口(群晖 host 容器通常为 `eth0`),保存并重启摄像头子桥。默认端口为 `5540`,需可用且允许同一局域网的 TCP/UDP;mDNS 与 IPv6 也需可用。高级配置示例:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"platform": "MijiaRemoteCamera",
|
|
76
|
+
"enableMatter": true,
|
|
77
|
+
"matterInterface": "eth0",
|
|
78
|
+
"matterPort": 5540
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
升级默认不开启 Matter。启用后,从受保护的插件设置页点击显示 Matter 配对信息,将手动配对码或 `MT:` 内容输入控制器。它与 HomeKit Setup Code 不同。已配对后增加其他控制器时可在设置页重新打开配对窗口,操作完成后可以关闭窗口。配对码和 Matter 私钥不会写入脱敏报告。Matter fabric/设备身份保存在插件私有存储的 `matter/` 内,升级和正常重启保留身份;删除该目录会使已有配对失效。
|
|
83
|
+
|
|
84
|
+
这是未认证的开发设备,使用 SDK 的测试 VID/开发证书。matter-server 默认会拒绝这种证书;只在明确信任此开发设备时,使用它的 `--enable-test-net-dcl`(或 `ENABLE_TEST_NET_DCL=true`)选项。插件不冒充通过认证的产品,客户端普通启动应保留严格验证。
|
|
85
|
+
|
|
86
|
+
Matter 信令及 WebRTC 播放目前面向同一局域网,无外部 STUN/TURN,不能据此承诺跨网 Matter 播放。远端摄像头与 NAS 可以继续处于不同网络,由原有小米远程连接取流。最多一路 Matter 实时会话,默认视频上限 640×360、12 fps、700 kbps,可使用已有 `maxWidth`/`maxHeight`/`maxFPS`/`maxBitrate` 配置调整。没有声明录像、云台、运动事件或云存储能力。Matter 和 HomeKit 共享同一个远程源,分别编码;群晖上同时播放会增加 CPU 使用。
|
|
87
|
+
|
|
88
|
+
打开直播时客户端会唤起插件取流;小米中继仍可能间歇连接失败,与是否先打开米家 App 无关。连接失败时会结束当前 Matter 会话,待源恢复后在客户端重新打开。当前未完成长时间稳定性验证。
|
|
89
|
+
|
|
67
90
|
## 数据和运行管理
|
|
68
91
|
|
|
69
92
|
会话、设备选择和导入的组件保存在:
|
|
@@ -74,6 +97,7 @@ mijia-camera-setup --from /homebridge/homebridge-mijia-remote-camera-0.1.0.tgz -
|
|
|
74
97
|
selection.json
|
|
75
98
|
region.json
|
|
76
99
|
runtime/
|
|
100
|
+
matter/
|
|
77
101
|
```
|
|
78
102
|
|
|
79
103
|
私有目录权限为 `0700`,凭据和选择文件为 `0600`。小米 Token 不写入 Homebridge `config.json`。设置页通过 Homebridge 的受保护接口和本机 Unix socket 通信;原始视频和音频仅监听临时的本机回环端口,不保存录音。对讲接口也仅允许本机访问。Homebridge 启动后恢复所选摄像头,关闭时清理 Java、FFmpeg 和 UDP 会话,临时网络故障会自动重连。
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -29,3 +29,12 @@ are separate from the package and retain their original rights and notices.
|
|
|
29
29
|
|
|
30
30
|
The HomeKit streaming delegate uses Homebridge's public HAP API and requires
|
|
31
31
|
neither `homebridge-camera-ffmpeg` nor an independent relay service.
|
|
32
|
+
|
|
33
|
+
The optional Matter camera implementation resolves `@matter/main` and
|
|
34
|
+
`@matter/nodejs` 0.18.0 (Apache-2.0) and `werift` 0.25.1 (MIT), together with
|
|
35
|
+
their transitive npm dependencies under their own licenses. These libraries
|
|
36
|
+
are installed by npm, not bundled in this package tarball. Matter development
|
|
37
|
+
attestation credentials are supplied by the SDK. This package is an experimental
|
|
38
|
+
development device and makes no certification claim. WebRTC media encryption and
|
|
39
|
+
signaling use these libraries; FFmpeg conversion continues to use the separately
|
|
40
|
+
imported local binary described above.
|
package/config.schema.json
CHANGED
|
@@ -83,6 +83,26 @@
|
|
|
83
83
|
"type": "boolean",
|
|
84
84
|
"title": "在家庭中显示夜视开关",
|
|
85
85
|
"default": true
|
|
86
|
+
},
|
|
87
|
+
"enableMatter": {
|
|
88
|
+
"type": "boolean",
|
|
89
|
+
"title": "启用 Matter 摄像头(开发设备)",
|
|
90
|
+
"default": false
|
|
91
|
+
},
|
|
92
|
+
"matterPort": {
|
|
93
|
+
"type": "integer",
|
|
94
|
+
"title": "Matter UDP 端口",
|
|
95
|
+
"default": 5540,
|
|
96
|
+
"minimum": 1024,
|
|
97
|
+
"maximum": 65535
|
|
98
|
+
},
|
|
99
|
+
"matterInterface": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"title": "Matter 局域网网卡",
|
|
102
|
+
"default": "eth0",
|
|
103
|
+
"minLength": 1,
|
|
104
|
+
"maxLength": 40,
|
|
105
|
+
"pattern": "^[A-Za-z0-9_.:-]+$"
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
}
|
|
@@ -35,6 +35,16 @@
|
|
|
35
35
|
<label><input id="enableNightVision" type="checkbox" style="width:auto"> 在 Apple“家庭”中显示夜视开关</label>
|
|
36
36
|
<div class="row"><button id="saveFeatures" class="secondary">保存功能配置</button></div><p id="featureState" class="small" aria-live="polite" style="margin-top:12px">功能配置保存后需重启摄像头子桥。对讲时,在“家庭”直播页面使用对讲按钮。</p>
|
|
37
37
|
</section>
|
|
38
|
+
<section id="matterSection"><div class="step">04 · MATTER</div><h2>Matter 摄像头 · 开发设备</h2>
|
|
39
|
+
<p class="small">可在支持 Matter 摄像头的控制器中测试实时视频、声音与对讲。MatterDesk 配对时需允许开发证书;当前不承诺 Apple“家庭”支持 Matter 摄像头。</p>
|
|
40
|
+
<label><input id="enableMatter" type="checkbox" style="width:auto"> 启用独立 Matter 摄像头</label>
|
|
41
|
+
<div class="columns"><div><label for="matterPort">Matter TCP/UDP 端口</label><input id="matterPort" type="number" min="1024" max="65535" step="1" value="5540"></div><div><label for="matterInterface">局域网网卡</label><input id="matterInterface" maxlength="40" value="eth0" autocomplete="off" spellcheck="false"></div></div>
|
|
42
|
+
<div class="row"><button id="saveMatter" class="secondary">保存 Matter 配置</button></div><p id="matterConfigState" class="small" aria-live="polite" style="margin-top:12px">配置保存后需重启摄像头子桥。</p>
|
|
43
|
+
<div class="note"><div id="matterStatus" aria-live="polite">正在读取 Matter 状态…</div><div id="matterMedia" class="small"></div><p class="small" style="margin:8px 0 0">仅支持可直接互通的局域网;当前不使用 STUN 或 TURN。请填写 Homebridge 容器内实际连接局域网的网卡名称。</p></div>
|
|
44
|
+
<div class="row"><button id="matterPairing" disabled>显示私密配对码</button><button id="matterOpen" class="secondary" disabled>打开配对窗口</button><button id="matterClose" class="secondary" disabled>关闭配对窗口</button></div>
|
|
45
|
+
<p id="matterActionState" class="small" aria-live="polite" style="margin-top:12px"></p>
|
|
46
|
+
<div id="matterPairingPanel" class="linkbox hide"><p class="small">配对码仅供管理员在自己的控制器中使用,请勿转发。此处内容不会加入脱敏诊断。</p><label for="matterManualCode">手动配对码</label><input id="matterManualCode" readonly autocomplete="off" spellcheck="false"><label for="matterQrCode">二维码内容(MT:)</label><input id="matterQrCode" readonly autocomplete="off" spellcheck="false"></div>
|
|
47
|
+
</section>
|
|
38
48
|
<div id="error" class="error" role="alert"></div>
|
|
39
49
|
<div class="footer small"><p>首次连接可能需要一两分钟。提供实时视频、快照、现场声音和双向对讲;夜视模式可在上方控制。</p><p>配件发布后,在“Apple 家庭 → 添加配件 → 更多选项”中选择摄像头,使用摄像头启动日志中的 Setup Code 添加;子桥配对码可能与主桥不同。</p><p>登录数据只保存在 Homebridge 的私有插件目录,不写入插件配置。</p></div>
|
|
40
50
|
</main>
|
|
@@ -75,6 +85,41 @@
|
|
|
75
85
|
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
76
86
|
const count = value => Number.isSafeInteger(value) && value >= 0;
|
|
77
87
|
const timestamp = value => value === null || (typeof value === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value));
|
|
88
|
+
const matterStates = new Set(['disabled','starting','ready','failed','stopped']);
|
|
89
|
+
const matterReasons = new Set(['matter_disabled','matter_not_ready','matter_start_failed','matter_port_in_use','matter_configuration_invalid','matter_pairing_unavailable','matter_window_failed','matter_closed']);
|
|
90
|
+
const matterMessages = {matter_disabled:'Matter 尚未启用。',matter_not_ready:'Matter 尚未就绪,请检查配置并重启摄像头子桥。',matter_start_failed:'Matter 启动失败,请检查端口、网卡和插件状态。',matter_port_in_use:'Matter 端口已被占用,请修改配置并重启。',matter_configuration_invalid:'Matter 配置无效,请检查端口和网卡。',matter_pairing_unavailable:'暂时无法取得 Matter 配对码。',matter_window_failed:'配对窗口操作未完成,请重试。',matter_closed:'Matter 已停止。'};
|
|
91
|
+
let matterValue={}, matterBusy=false, matterGeneration=0, matterDirty=false, matterRestartPending=false;
|
|
92
|
+
function safeMatterStatus(value={}) {
|
|
93
|
+
if(!record(value))return {};
|
|
94
|
+
const result={};
|
|
95
|
+
for(const key of ['enabled','running','commissioned','commissioningWindowOpen'])if(typeof value[key]==='boolean')result[key]=value[key];
|
|
96
|
+
if(matterStates.has(value.state))result.state=value.state;
|
|
97
|
+
if(matterReasons.has(value.reason))result.reason=value.reason;
|
|
98
|
+
if(value.deviceType===322)result.deviceType=322;
|
|
99
|
+
for(const key of ['activeSessions','videoStreams','audioStreams','snapshotStreams'])if(count(value[key])&&value[key]<=100)result[key]=value[key];
|
|
100
|
+
if(record(value.media)){
|
|
101
|
+
const media={};for(const key of ['sessions','failures','videoPackets','audioPackets','talkPackets','talkBytes'])if(count(value.media[key]))media[key]=value.media[key];
|
|
102
|
+
for(const key of ['active','microphoneMuted','speakerMuted'])if(typeof value.media[key]==='boolean')media[key]=value.media[key];
|
|
103
|
+
if(Object.keys(media).length)result.media=media;
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
function clearMatterPairing(){++matterGeneration;$('matterManualCode').value='';$('matterQrCode').value='';$('matterPairingPanel').classList.add('hide');}
|
|
108
|
+
function matterReady(){return connected&&!authBusy&&!regionNeedsLogin&&!matterDirty&&!matterRestartPending&&configs[0]?.enableMatter===true&&$('enableMatter').checked&&matterValue.enabled===true&&matterValue.running===true&&matterValue.state==='ready';}
|
|
109
|
+
function renderMatter(){
|
|
110
|
+
const ready=matterReady(), value=matterValue;
|
|
111
|
+
$('matterPairing').disabled=!ready||matterBusy||value.commissioningWindowOpen!==true;
|
|
112
|
+
$('matterOpen').disabled=!ready||matterBusy||value.commissioningWindowOpen===true;
|
|
113
|
+
$('matterClose').disabled=!ready||matterBusy||value.commissioningWindowOpen!==true;
|
|
114
|
+
if(!ready||(value.commissioningWindowOpen!==true&&($('matterManualCode').value||$('matterQrCode').value)))clearMatterPairing();
|
|
115
|
+
const state=matterRestartPending?'配置已保存,请重启摄像头子桥并重新打开本页。':matterDirty?'配置尚未保存。':!$('enableMatter').checked?'Matter 尚未启用。':value.reason?matterMessages[value.reason]:value.running===true?`${value.commissioned?'已配对':'尚未配对'} · 配对窗口${value.commissioningWindowOpen?'已打开':'已关闭'}`:value.state==='starting'?'Matter 正在启动…':'Matter 尚未就绪,请重启摄像头子桥。';
|
|
116
|
+
$('matterStatus').textContent=state;
|
|
117
|
+
const details=[];if(count(value.activeSessions))details.push(`活动会话 ${value.activeSessions}`);
|
|
118
|
+
if(count(value.media?.videoPackets))details.push(`视频包 ${value.media.videoPackets}`);
|
|
119
|
+
if(count(value.media?.audioPackets))details.push(`声音包 ${value.media.audioPackets}`);
|
|
120
|
+
if(count(value.media?.talkBytes))details.push(`对讲 ${value.media.talkBytes} 字节`);
|
|
121
|
+
$('matterMedia').textContent=details.join(' · ');
|
|
122
|
+
}
|
|
78
123
|
|
|
79
124
|
function safeMediaDiagnostics(value) {
|
|
80
125
|
const result = {};
|
|
@@ -148,6 +193,7 @@
|
|
|
148
193
|
if ([1, 2].includes(value.audio.channels)) audio.channels = value.audio.channels;
|
|
149
194
|
result.audio = audio;
|
|
150
195
|
}
|
|
196
|
+
if(record(value.matter)){const matter=safeMatterStatus(value.matter);if(Object.keys(matter).length)result.matter=matter;}
|
|
151
197
|
if (value.media === null) result.media = null;
|
|
152
198
|
else if (record(value.media)) {
|
|
153
199
|
const media = safeMediaDiagnostics(value.media);
|
|
@@ -197,21 +243,21 @@
|
|
|
197
243
|
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;}
|
|
198
244
|
function chosenHome(){return homes[Number($('homes').value)];}
|
|
199
245
|
function chosenDevice(){return devices[Number($('devices').value)];}
|
|
200
|
-
function setLoginBusy(busy){authBusy=busy;$('login').disabled=busy||!connected; $('load').disabled=busy||!connected||regionNeedsLogin; $('region').disabled=busy; $('cancel').classList.toggle('hide',!busy);}
|
|
246
|
+
function setLoginBusy(busy){authBusy=busy;$('login').disabled=busy||!connected; $('load').disabled=busy||!connected||regionNeedsLogin; $('region').disabled=busy; $('cancel').classList.toggle('hide',!busy);renderMatter();}
|
|
201
247
|
function hideLoginLink(){$('loginLink').classList.add('hide');$('officialLogin').removeAttribute('href');}
|
|
202
|
-
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;setLoginBusy(authBusy);diagnosticValue=safeDiagnostics(value);statusReceivedAt=Date.now();$('diagnosticsReport').textContent=JSON.stringify(diagnosticValue,null,2);$('copyDiagnostics').disabled=false;renderRuntime();}catch(error){connected=false;$('login').disabled=true;$('load').disabled=true;$('runtimeStatus').dataset.live='false';$('runtimeText').textContent='等待插件启动';$('connectionDetail').textContent='';$('mediaDetail').textContent='';showError(error);}}
|
|
248
|
+
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();}catch(error){connected=false;matterValue={};clearMatterPairing();renderMatter();$('login').disabled=true;$('load').disabled=true;$('runtimeStatus').dataset.live='false';$('runtimeText').textContent='等待插件启动';$('connectionDetail').textContent='';$('mediaDetail').textContent='';showError(error);}}
|
|
203
249
|
$('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='浏览器无法自动复制,请手动选中上方诊断报告并复制。';}};
|
|
204
|
-
async function loadHomes(){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);}}
|
|
205
|
-
async function loadDevices(){const home=chosenHome();if(!home)return;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);}}
|
|
206
|
-
function deviceChanged(){const device=chosenDevice();$('cameraName').disabled=!device;$('save').disabled=!device;$('cameraName').value=device?(device.name||'米家摄像头'):'';}
|
|
250
|
+
async function loadHomes(){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);}}
|
|
251
|
+
async function loadDevices(){const home=chosenHome();if(!home)return;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);}}
|
|
252
|
+
function deviceChanged(){clearMatterPairing();const device=chosenDevice();$('cameraName').disabled=!device;$('save').disabled=!device;$('cameraName').value=device?(device.name||'米家摄像头'):'';}
|
|
207
253
|
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);}}
|
|
208
254
|
$('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='';}};
|
|
209
255
|
$('cancel').onclick=async()=>{clearTimeout(pollTimer);try{await request('/auth/cancel',{loginId});}catch(error){showError(error);}hideLoginLink();setLoginBusy(false);$('authState').textContent='已取消。已有登录数据保留。';};
|
|
210
256
|
$('region').onchange=()=>{resetNight();regionTouched=true;regionNeedsLogin=$('region').value!==runtimeRegion;setLoginBusy(authBusy);$('authState').textContent=regionNeedsLogin?'已切换地区,请生成新登录链接后读取家庭。':'';}; $('load').onclick=loadHomes; $('homes').onchange=loadDevices; $('devices').onchange=deviceChanged;
|
|
211
|
-
$('save').onclick=async()=>{const home=chosenHome(),device=chosenDevice();if(!home||!device)return;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();$('selectionState').textContent=result.restartRequired?'已保存新的摄像头。请重启 Homebridge 使配件变更生效。':'选择已保存,正在连接摄像头。配对码见摄像头启动日志中的 Setup Code。';await status();}catch(error){showError(error);}finally{$('save').disabled=false;}};
|
|
257
|
+
$('save').onclick=async()=>{const home=chosenHome(),device=chosenDevice();if(!home||!device)return;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();$('selectionState').textContent=result.restartRequired?'已保存新的摄像头。请重启 Homebridge 使配件变更生效。':'选择已保存,正在连接摄像头。配对码见摄像头启动日志中的 Setup Code。';await status();}catch(error){showError(error);}finally{$('save').disabled=false;}};
|
|
212
258
|
$('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);}};
|
|
213
259
|
$('check').onclick=status;
|
|
214
|
-
function resetNight(){
|
|
260
|
+
function resetNight(){clearMatterPairing();++nightGeneration;$('nightMode').disabled=true;$('nightSave').disabled=true;$('nightState').textContent='读取摄像头实际模式后可修改。';}
|
|
215
261
|
async function nightRequest(write=false){
|
|
216
262
|
if(nightBusy)return;nightBusy=true;const generation=nightGeneration;$('nightRead').disabled=true;$('nightSave').disabled=true;$('nightMode').disabled=true;
|
|
217
263
|
$('nightState').textContent=write?'正在修改并核对摄像头模式…':'正在读取摄像头实际模式…';
|
|
@@ -231,8 +277,27 @@
|
|
|
231
277
|
configs=[next,...configs.slice(1)];await homebridge.updatePluginConfig(configs);await homebridge.savePluginConfig();
|
|
232
278
|
$('featureState').textContent='配置已保存,请重启摄像头子桥以应用。';
|
|
233
279
|
}catch(error){showError(error);}};
|
|
234
|
-
|
|
235
|
-
|
|
280
|
+
for(const key of ['enableMatter','matterPort','matterInterface'])$(key).onchange=()=>{matterDirty=true;clearMatterPairing();renderMatter();};
|
|
281
|
+
$('saveMatter').onclick=async()=>{clearMatterPairing();try{
|
|
282
|
+
const port=Number($('matterPort').value), networkInterface=$('matterInterface').value.trim();
|
|
283
|
+
if(!Number.isInteger(port)||port<1024||port>65535||!/^[A-Za-z0-9_.:-]{1,40}$/.test(networkInterface)){ $('matterConfigState').textContent='请填写 1024–65535 的整数端口和有效的网卡名称。';return; }
|
|
284
|
+
const next={...(configs[0]||{platform:'MijiaRemoteCamera',name:'米家远程摄像头',region:$('region').value}),enableMatter:$('enableMatter').checked,matterPort:port,matterInterface:networkInterface};
|
|
285
|
+
const updated=[next,...configs.slice(1)];await homebridge.updatePluginConfig(updated);await homebridge.savePluginConfig();configs=updated;matterDirty=false;matterRestartPending=true;
|
|
286
|
+
$('matterConfigState').textContent='配置已保存,请重启摄像头子桥并重新打开本页。';renderMatter();
|
|
287
|
+
}catch{ $('matterConfigState').textContent='Matter 配置未能保存,请重试。';renderMatter();}};
|
|
288
|
+
async function matterAction(action){
|
|
289
|
+
if(!matterReady()||matterBusy)return;clearMatterPairing();const generation=matterGeneration;matterBusy=true;renderMatter();$('matterActionState').textContent='正在处理…';
|
|
290
|
+
try{const value=await request('/matter/'+action);if(generation!==matterGeneration)return;
|
|
291
|
+
if(action==='pairing'){
|
|
292
|
+
if(!/^(?:\d{11}|\d{21})$/.test(value?.manualPairingCode)||typeof value?.qrPairingCode!=='string'||!/^MT:[0-9A-Z.:-]{10,250}$/.test(value.qrPairingCode))throw Error('matter_pairing_unavailable');
|
|
293
|
+
$('matterManualCode').value=value.manualPairingCode;$('matterQrCode').value=value.qrPairingCode;$('matterPairingPanel').classList.remove('hide');$('matterActionState').textContent='已显示私密配对码,请在自己的控制器中完成配对。';
|
|
294
|
+
}else{matterValue=safeMatterStatus(value);$('matterActionState').textContent=action==='open-window'?'配对窗口已打开,可显示配对码。':'配对窗口已关闭。';}
|
|
295
|
+
}catch(error){if(generation!==matterGeneration)return;clearMatterPairing();$('matterActionState').textContent=matterMessages[reason(error)]||'Matter 操作未完成,请检查状态后重试。';}
|
|
296
|
+
finally{matterBusy=false;renderMatter();}
|
|
297
|
+
}
|
|
298
|
+
$('matterPairing').onclick=()=>matterAction('pairing');$('matterOpen').onclick=()=>matterAction('open-window');$('matterClose').onclick=()=>matterAction('close-window');
|
|
299
|
+
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;$('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);}}
|
|
300
|
+
window.addEventListener('beforeunload',()=>{clearMatterPairing();clearInterval(statusTimer);clearInterval(clockTimer);clearTimeout(pollTimer);});
|
|
236
301
|
init();
|
|
237
302
|
})();
|
|
238
303
|
</script></body></html>
|
package/homebridge-ui/server.js
CHANGED
|
@@ -13,7 +13,9 @@ const SAFE_REASONS = new Set(['plugin_not_running', 'invalid_request', 'unauthen
|
|
|
13
13
|
'unsupported_transport', 'no_video', 'connect_timeout', 'backend_failed', 'storage_failed',
|
|
14
14
|
'runtime_in_use', 'query_timeout', 'unsupported_platform', 'bundled_runtime_missing', 'not_selected', 'auth_in_progress',
|
|
15
15
|
'runtime_components_missing', 'runtime_components_invalid', 'night_vision_unsupported', 'night_vision_unavailable',
|
|
16
|
-
'night_vision_rejected', 'night_vision_not_applied', 'night_vision_timeout'
|
|
16
|
+
'night_vision_rejected', 'night_vision_not_applied', 'night_vision_timeout',
|
|
17
|
+
'matter_disabled', 'matter_not_ready', 'matter_start_failed', 'matter_port_in_use', 'matter_configuration_invalid',
|
|
18
|
+
'matter_pairing_unavailable', 'matter_window_failed', 'matter_closed']);
|
|
17
19
|
|
|
18
20
|
function failureReason(error) {
|
|
19
21
|
if (['ENOENT', 'ECONNREFUSED'].includes(error?.code)) return 'plugin_not_running';
|
|
@@ -52,6 +54,9 @@ export function createUiHandlers(runtime) {
|
|
|
52
54
|
deviceId: id(payload?.deviceId), name: label(payload?.name, '米家摄像头'), model: label(payload?.model, 'Xiaomi Camera'),
|
|
53
55
|
region: ['cn', 'de', 'us', 'ru', 'sg', 'i2'].includes(payload?.region) ? payload.region : 'cn' }),
|
|
54
56
|
'/status': () => runtime.request('status', {}),
|
|
57
|
+
'/matter/pairing': () => runtime.request('matter.pairing', {}),
|
|
58
|
+
'/matter/open-window': () => runtime.request('matter.openWindow', {}),
|
|
59
|
+
'/matter/close-window': () => runtime.request('matter.closeWindow', {}),
|
|
55
60
|
'/night/get': () => runtime.request('night.get', {}),
|
|
56
61
|
'/night/set': payload => {
|
|
57
62
|
if (!['auto', 'on', 'off'].includes(payload?.mode)) throw new Error('invalid_request');
|
package/lib/diagnostics.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeMatterStatus } from './matter-diagnostics.mjs';
|
|
1
2
|
const states = new Set(['idle', 'starting', 'ready', 'retrying', 'blocked', 'stopped', 'paused']);
|
|
2
3
|
const stages = new Set(['device_lookup', 'provisioning', 'relay_connect', 'authentication', 'waiting_video']);
|
|
3
4
|
const reasons = new Set(['not_selected', 'starting', 'ready', 'stopped', 'retrying', 'connect_timeout',
|
|
@@ -95,6 +96,10 @@ export function safeDiagnostics(value = {}) {
|
|
|
95
96
|
if ([1, 2].includes(value.audio.channels)) audio.channels = value.audio.channels;
|
|
96
97
|
result.audio = audio;
|
|
97
98
|
}
|
|
99
|
+
if (record(value.matter)) {
|
|
100
|
+
const matter = safeMatterStatus(value.matter);
|
|
101
|
+
if (Object.keys(matter).length) result.matter = matter;
|
|
102
|
+
}
|
|
98
103
|
if (value.media === null) result.media = null;
|
|
99
104
|
else if (record(value.media)) {
|
|
100
105
|
const media = safeMediaDiagnostics(value.media);
|