@linanw/homebridge-mijia-remote-camera 0.2.0 → 0.3.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 +13 -5
- package/backend/Bridge.java +13 -2
- package/backend/PtzControlServer.java +223 -0
- package/backend/bridge.jar +0 -0
- package/config.schema.json +13 -0
- package/homebridge-ui/public/index.html +70 -18
- package/homebridge-ui/server.js +6 -0
- package/lib/matter-device.mjs +94 -25
- package/lib/matter-diagnostics.mjs +5 -2
- package/lib/matter-media.mjs +175 -78
- package/lib/matter-ptz-cluster.mjs +100 -0
- package/lib/matter-service.mjs +15 -3
- package/lib/platform.mjs +8 -1
- package/lib/ptz-control.mjs +59 -0
- package/lib/ptz.mjs +56 -0
- package/lib/rpc.mjs +2 -1
- package/lib/runtime-factory.mjs +3 -1
- package/lib/runtime.mjs +55 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { request as httpRequest } from 'node:http';
|
|
2
|
+
|
|
3
|
+
const DIRECTIONS = ['up', 'down', 'left', 'right'];
|
|
4
|
+
// This exact model's four directions and self-stopping single-step behavior were physically verified.
|
|
5
|
+
const VERIFIED_MODELS = new Set(['chuangmi.camera.061a01']);
|
|
6
|
+
const REASONS = new Set(['invalid_request', 'ptz_unsupported', 'ptz_unavailable', 'ptz_rejected', 'ptz_timeout', 'ptz_busy', 'query_cancelled']);
|
|
7
|
+
const failure = reason => Object.assign(new Error(REASONS.has(reason) ? reason : 'ptz_unavailable'), {
|
|
8
|
+
reason: REASONS.has(reason) ? reason : 'ptz_unavailable',
|
|
9
|
+
});
|
|
10
|
+
export const validPtzDirection = value => DIRECTIONS.includes(value);
|
|
11
|
+
export const ptzModelSupported = model => VERIFIED_MODELS.has(model);
|
|
12
|
+
export function ptzCapabilities(model, available = false, supports = ptzModelSupported) {
|
|
13
|
+
const supported = supports(model) === true;
|
|
14
|
+
return { supported, available: available === true, directions: supported ? [...DIRECTIONS] : [],
|
|
15
|
+
relativeStep: supported, zoom: false, absolutePosition: false };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Authenticated loopback only. Public callers cannot provide endpoints, operation IDs, speed or duration. */
|
|
19
|
+
export function requestPtzStep(control, direction, { signal, timeoutMs = 2600, request = httpRequest } = {}) {
|
|
20
|
+
if (!validPtzDirection(direction) || !Number.isInteger(control?.port) || control.port < 1024 || control.port > 65535
|
|
21
|
+
|| typeof control?.token !== 'string' || !/^[a-f0-9]{64}$/.test(control.token)) return Promise.reject(failure('invalid_request'));
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
let child, response, timer, done = false, size = 0;
|
|
24
|
+
const chunks = [];
|
|
25
|
+
const finish = (error, value) => {
|
|
26
|
+
if (done) return; done = true; clearTimeout(timer); signal?.removeEventListener('abort', cancelled);
|
|
27
|
+
for (const chunk of chunks) chunk.fill(0);
|
|
28
|
+
error ? reject(error) : resolve(value);
|
|
29
|
+
};
|
|
30
|
+
const cancel = reason => { child?.destroy(); response?.destroy(); finish(failure(reason)); };
|
|
31
|
+
const cancelled = () => cancel('query_cancelled');
|
|
32
|
+
if (signal?.aborted) { finish(failure('query_cancelled')); return; }
|
|
33
|
+
try {
|
|
34
|
+
const body = JSON.stringify({ direction });
|
|
35
|
+
child = request({ hostname: '127.0.0.1', port: control.port, path: '/ptz/move', method: 'POST',
|
|
36
|
+
headers: { Authorization: `Bearer ${control.token}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, incoming => {
|
|
37
|
+
response = incoming;
|
|
38
|
+
incoming.on('data', chunk => {
|
|
39
|
+
if (done) return;
|
|
40
|
+
size += chunk.length;
|
|
41
|
+
if (size > 2048) { cancel('ptz_unavailable'); return; }
|
|
42
|
+
chunks.push(chunk);
|
|
43
|
+
});
|
|
44
|
+
incoming.once('error', () => finish(failure('ptz_unavailable')));
|
|
45
|
+
incoming.once('end', () => {
|
|
46
|
+
let value;
|
|
47
|
+
try { value = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { finish(failure('ptz_unavailable')); return; }
|
|
48
|
+
if (incoming.statusCode !== 200) { finish(failure(value?.reason)); return; }
|
|
49
|
+
if (value?.accepted !== true || value.direction !== direction || value.movement !== 'step') { finish(failure('ptz_unavailable')); return; }
|
|
50
|
+
finish(null, { accepted: true, direction, movement: 'step' });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
child.once('error', () => finish(failure('ptz_unavailable')));
|
|
54
|
+
signal?.addEventListener('abort', cancelled, { once: true });
|
|
55
|
+
timer = setTimeout(() => cancel('ptz_timeout'), Math.max(1, Math.min(3000, timeoutMs)));
|
|
56
|
+
child.end(body);
|
|
57
|
+
} catch { cancel('ptz_unavailable'); }
|
|
58
|
+
});
|
|
59
|
+
}
|
package/lib/ptz.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const DIRECTIONS = ['up', 'down', 'left', 'right'];
|
|
2
|
+
const LABELS = { up: '云台向上', down: '云台向下', left: '云台向左', right: '云台向右' };
|
|
3
|
+
const SAFE_REASONS = new Set(['ptz_unsupported', 'ptz_unavailable', 'ptz_rejected', 'ptz_timeout', 'ptz_busy']);
|
|
4
|
+
|
|
5
|
+
/** Optional Home app controls for a verified, finite camera direction step. */
|
|
6
|
+
export async function configurePtz({ api, accessory, runtime, selection, log } = {}) {
|
|
7
|
+
let capabilities;
|
|
8
|
+
try { capabilities = await runtime.getPtzCapabilities(); } catch { return null; }
|
|
9
|
+
if (capabilities?.supported !== true || capabilities.relativeStep !== true
|
|
10
|
+
|| !Array.isArray(capabilities.directions) || !DIRECTIONS.every(d => capabilities.directions.includes(d))) return null;
|
|
11
|
+
const expected = selection && { deviceId: selection.deviceId, homeId: selection.homeId, homeOwner: selection.homeOwner };
|
|
12
|
+
let closed = false, busy = false, lastFailure;
|
|
13
|
+
const services = [];
|
|
14
|
+
const hapError = reason => new api.hap.HapStatusError(reason === 'ptz_busy'
|
|
15
|
+
? api.hap.HAPStatus.RESOURCE_BUSY : api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
|
16
|
+
for (const direction of DIRECTIONS) {
|
|
17
|
+
const subtype = `mijia-ptz-${direction}`;
|
|
18
|
+
const service = accessory.getServiceById?.(api.hap.Service.Switch, subtype)
|
|
19
|
+
|| accessory.addService(api.hap.Service.Switch, LABELS[direction], subtype);
|
|
20
|
+
services.push(service);
|
|
21
|
+
const on = service.getCharacteristic(api.hap.Characteristic.On);
|
|
22
|
+
service.updateCharacteristic(api.hap.Characteristic.On, false);
|
|
23
|
+
on.onGet(() => false);
|
|
24
|
+
on.onSet(async value => {
|
|
25
|
+
if (value === false || value === 0) return;
|
|
26
|
+
if (value !== true && value !== 1) throw hapError('ptz_rejected');
|
|
27
|
+
if (closed) throw hapError('ptz_unavailable');
|
|
28
|
+
if (busy) throw hapError('ptz_busy');
|
|
29
|
+
busy = true;
|
|
30
|
+
try {
|
|
31
|
+
if (expected) {
|
|
32
|
+
const current = await runtime.selected();
|
|
33
|
+
if (closed || !current || Object.keys(expected).some(k => expected[k] !== current[k]))
|
|
34
|
+
throw Object.assign(new Error('ptz_unavailable'), { reason: 'ptz_unavailable' });
|
|
35
|
+
}
|
|
36
|
+
if (closed) throw Object.assign(new Error('ptz_unavailable'), { reason: 'ptz_unavailable' });
|
|
37
|
+
const result = await runtime.movePtz(direction);
|
|
38
|
+
if (result?.accepted !== true || result.direction !== direction)
|
|
39
|
+
throw Object.assign(new Error('ptz_rejected'), { reason: 'ptz_rejected' });
|
|
40
|
+
lastFailure = undefined;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const reason = SAFE_REASONS.has(error?.reason) ? error.reason : 'ptz_unavailable';
|
|
43
|
+
if (!closed && lastFailure !== reason) { lastFailure = reason; log?.warn?.(`云台控制未完成(${reason})。`); }
|
|
44
|
+
throw hapError(reason);
|
|
45
|
+
} finally {
|
|
46
|
+
busy = false;
|
|
47
|
+
setImmediate(() => { if (!closed) service.updateCharacteristic(api.hap.Characteristic.On, false); });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return { shutdown() {
|
|
52
|
+
if (closed) return;
|
|
53
|
+
closed = true;
|
|
54
|
+
for (const service of services) service.updateCharacteristic(api.hap.Characteristic.On, false);
|
|
55
|
+
} };
|
|
56
|
+
}
|
package/lib/rpc.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { StringDecoder } from 'node:string_decoder';
|
|
6
6
|
|
|
7
|
-
const METHODS = new Set(['status', 'homes', 'devices', 'select', 'pause', 'resume', 'auth.start', 'auth.status', 'auth.cancel', 'night.get', 'night.set', 'matter.pairing', 'matter.openWindow', 'matter.closeWindow']);
|
|
7
|
+
const METHODS = new Set(['status', 'homes', 'devices', 'select', 'pause', 'resume', 'auth.start', 'auth.status', 'auth.cancel', 'night.get', 'night.set', 'ptz.capabilities', 'ptz.move', 'matter.pairing', 'matter.openWindow', 'matter.closeWindow']);
|
|
8
8
|
const MAX_REQUEST = 32768;
|
|
9
9
|
const MAX_RESPONSE = 1048576;
|
|
10
10
|
const REASONS = new Set(['operation_failed', 'invalid_request', 'response_too_large', 'invalid_response', 'plugin_not_running',
|
|
@@ -17,6 +17,7 @@ const REASONS = new Set(['operation_failed', 'invalid_request', 'response_too_la
|
|
|
17
17
|
'invalid_region', 'unsafe_url', 'network_error', 'request_timeout', 'unexpected_response', 'login_expired', 'login_cancelled',
|
|
18
18
|
'login_not_approved', 'incomplete_session', 'credentials_write_failed', 'unknown_login',
|
|
19
19
|
'night_vision_unsupported', 'night_vision_unavailable', 'night_vision_rejected', 'night_vision_not_applied', 'night_vision_timeout',
|
|
20
|
+
'ptz_unsupported', 'ptz_unavailable', 'ptz_rejected', 'ptz_timeout', 'ptz_busy',
|
|
20
21
|
'matter_disabled', 'matter_not_ready', 'matter_start_failed', 'matter_port_in_use', 'matter_configuration_invalid',
|
|
21
22
|
'matter_pairing_unavailable', 'matter_window_failed', 'matter_closed']);
|
|
22
23
|
|
package/lib/runtime-factory.mjs
CHANGED
|
@@ -56,7 +56,9 @@ export async function attachServices(runtime, auth, storagePath) {
|
|
|
56
56
|
if (method === 'matter.pairing') return runtime.matterPairing?.() ?? Promise.reject(failure('matter_not_ready'));
|
|
57
57
|
if (method === 'matter.openWindow') return runtime.matterOpenWindow?.() ?? Promise.reject(failure('matter_not_ready'));
|
|
58
58
|
if (method === 'matter.closeWindow') return runtime.matterCloseWindow?.() ?? Promise.reject(failure('matter_not_ready'));
|
|
59
|
-
if (authorizing && ['homes', 'devices', 'select', 'resume', 'night.get', 'night.set'].includes(method)) throw failure('auth_in_progress');
|
|
59
|
+
if (authorizing && ['homes', 'devices', 'select', 'resume', 'night.get', 'night.set', 'ptz.capabilities', 'ptz.move'].includes(method)) throw failure('auth_in_progress');
|
|
60
|
+
if (method === 'ptz.capabilities') return runtime.getPtzCapabilities();
|
|
61
|
+
if (method === 'ptz.move') return runtime.movePtz(payload.direction);
|
|
60
62
|
if (method === 'night.get') return runtime.getNightVision();
|
|
61
63
|
if (method === 'night.set') return runtime.setNightVision(payload.mode);
|
|
62
64
|
if (method === 'homes') return runtime.discoverHomes();
|
package/lib/runtime.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
|
6
6
|
import * as fs from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { acquireRuntimeLock } from './process-lock.mjs';
|
|
9
|
+
import { ptzCapabilities, ptzModelSupported, validPtzDirection, requestPtzStep } from './ptz-control.mjs';
|
|
9
10
|
|
|
10
11
|
const REGIONS = new Set(['cn', 'ru', 'de', 'sg', 'us', 'i2']);
|
|
11
12
|
const REASONS = new Set(['not_selected', 'starting', 'ready', 'stopped', 'retrying', 'connect_timeout',
|
|
@@ -14,7 +15,8 @@ const REASONS = new Set(['not_selected', 'starting', 'ready', 'stopped', 'retryi
|
|
|
14
15
|
'device_pagination_incomplete', 'home_request_failed', 'home_request_rejected', 'discovery_failed',
|
|
15
16
|
'backend_failed', 'connection_failed', 'no_video', 'port_in_use', 'runtime_in_use', 'runtime_closed',
|
|
16
17
|
'runtime_unavailable', 'query_timeout', 'query_cancelled', 'invalid_backend_response', 'storage_failed',
|
|
17
|
-
'night_vision_unsupported', 'night_vision_unavailable', 'night_vision_rejected', 'night_vision_not_applied', 'night_vision_timeout'
|
|
18
|
+
'night_vision_unsupported', 'night_vision_unavailable', 'night_vision_rejected', 'night_vision_not_applied', 'night_vision_timeout',
|
|
19
|
+
'ptz_unsupported', 'ptz_unavailable', 'ptz_rejected', 'ptz_timeout', 'ptz_busy']);
|
|
18
20
|
REASONS.add('auth_in_progress');
|
|
19
21
|
const TRANSIENT_CODES = new Set([0, 3, 4, 5, 6]);
|
|
20
22
|
const STAGES = ['device_lookup', 'provisioning', 'relay_connect', 'authentication', 'waiting_video'];
|
|
@@ -82,7 +84,7 @@ async function terminate(child) {
|
|
|
82
84
|
export class RuntimeManager extends EventEmitter {
|
|
83
85
|
constructor({ storagePath, javaPath, jarPath, bridgePath, log, region = 'cn', quality = 1, enableAudio = true, enableTalkback = true, cloudControl,
|
|
84
86
|
spawn = spawnProcess, choosePort = freePort, terminateProcess = terminate, retryDelays = [15000, 30000, 60000],
|
|
85
|
-
connectTimeoutMs = 180000, queryTimeoutMs = 90000 } = {}) {
|
|
87
|
+
connectTimeoutMs = 180000, queryTimeoutMs = 90000, ptzRequest = requestPtzStep, ptzSupported = ptzModelSupported } = {}) {
|
|
86
88
|
super();
|
|
87
89
|
if (![storagePath, javaPath, jarPath, bridgePath].every(value => typeof value === 'string' && value.length)) {
|
|
88
90
|
throw new RuntimeError('runtime_unavailable');
|
|
@@ -101,6 +103,7 @@ export class RuntimeManager extends EventEmitter {
|
|
|
101
103
|
this.audioInfo = null;
|
|
102
104
|
this.audioSeenAt = null;
|
|
103
105
|
this.cloudControl = cloudControl; this.controlRequests = new Set();
|
|
106
|
+
this.ptzRequest = ptzRequest; this.ptzSupported = ptzSupported; this.ptzControl = null; this.ptzBusy = false; this.lastPtzAt = null;
|
|
104
107
|
this.spawn = spawn; this.choosePort = choosePort; this.terminateProcess = terminateProcess;
|
|
105
108
|
this.retryDelays = retryDelays; this.connectTimeoutMs = connectTimeoutMs; this.queryTimeoutMs = queryTimeoutMs;
|
|
106
109
|
this.state = 'idle'; this.reason = 'stopped'; this.ready = false; this.videoReceived = false;
|
|
@@ -176,7 +179,7 @@ export class RuntimeManager extends EventEmitter {
|
|
|
176
179
|
if (state === 'retrying' || state === 'blocked') {
|
|
177
180
|
this.lastFailure = this.reason; this.lastFailureAt = Date.now();
|
|
178
181
|
}
|
|
179
|
-
if (extra.ready === false) { this.audioInfo = null; this.audioSeenAt = null; }
|
|
182
|
+
if (extra.ready === false) { this.audioInfo = null; this.audioSeenAt = null; this.ptzControl = null; }
|
|
180
183
|
Object.assign(this, extra); this.emit('status', this.status());
|
|
181
184
|
}
|
|
182
185
|
audioSource() {
|
|
@@ -233,6 +236,48 @@ export class RuntimeManager extends EventEmitter {
|
|
|
233
236
|
});
|
|
234
237
|
}
|
|
235
238
|
getNightVision() { return this._nightVision(); }
|
|
239
|
+
async getPtzCapabilities() {
|
|
240
|
+
if (this.closed) throw new RuntimeError('runtime_closed');
|
|
241
|
+
if (this.paused) throw new RuntimeError('auth_in_progress');
|
|
242
|
+
const selected = this.selected();
|
|
243
|
+
return ptzCapabilities(selected?.model, Boolean(selected && this.ready && this.child && this.ptzControl), this.ptzSupported);
|
|
244
|
+
}
|
|
245
|
+
async movePtz(direction) {
|
|
246
|
+
if (!validPtzDirection(direction)) throw new RuntimeError('invalid_request');
|
|
247
|
+
if (this.closed) throw new RuntimeError('runtime_closed');
|
|
248
|
+
if (this.paused) throw new RuntimeError('auth_in_progress');
|
|
249
|
+
const selected = this.selected();
|
|
250
|
+
if (!selected) throw new RuntimeError('not_selected');
|
|
251
|
+
if (!this.ptzSupported(selected.model)) throw new RuntimeError('ptz_unsupported');
|
|
252
|
+
if (!this.ready || !this.child || !this.ptzControl) throw new RuntimeError('ptz_unavailable');
|
|
253
|
+
if (this.ptzBusy || this.lastPtzAt !== null && Date.now() - this.lastPtzAt < 1800) throw new RuntimeError('ptz_busy');
|
|
254
|
+
const generation = this.generation, region = this.region, control = this.ptzControl, controller = new AbortController();
|
|
255
|
+
const valid = () => !this.closed && !this.paused && !controller.signal.aborted && this.generation === generation
|
|
256
|
+
&& this.region === region && this.ptzControl === control
|
|
257
|
+
&& ['homeId', 'homeOwner', 'deviceId', 'model', 'region'].every(key => this.selected()?.[key] === selected[key]);
|
|
258
|
+
let timer; this.ptzBusy = true; this.controlRequests.add(controller);
|
|
259
|
+
try {
|
|
260
|
+
const task = async () => {
|
|
261
|
+
if (!this.cloudControl?.context) throw new RuntimeError('ptz_unavailable');
|
|
262
|
+
await this.cloudControl.context(selected, region);
|
|
263
|
+
if (!valid()) throw new RuntimeError('query_cancelled');
|
|
264
|
+
this.lastPtzAt = Date.now();
|
|
265
|
+
return this.ptzRequest(control, direction, { signal: controller.signal });
|
|
266
|
+
};
|
|
267
|
+
const timeout = new Promise((_, reject) => {
|
|
268
|
+
timer = setTimeout(() => { controller.abort(); reject(new RuntimeError('ptz_timeout')); }, 3000);
|
|
269
|
+
});
|
|
270
|
+
const reply = await Promise.race([task(), timeout]);
|
|
271
|
+
if (!valid()) throw new RuntimeError('query_cancelled');
|
|
272
|
+
if (reply?.accepted !== true || reply.direction !== direction || reply.movement !== 'step') throw new RuntimeError('ptz_unavailable');
|
|
273
|
+
return { accepted: true, direction, movement: 'step' };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error?.reason === 'session_expired') this._renewControlSession();
|
|
276
|
+
if (error?.capabilityRequired === true && this.ready) this._refreshControlCapability(selected, region);
|
|
277
|
+
const reason = error?.reason?.startsWith('night_vision_') ? 'ptz_unavailable' : error?.reason;
|
|
278
|
+
throw new RuntimeError(REASONS.has(reason) ? reason : 'ptz_unavailable');
|
|
279
|
+
} finally { clearTimeout(timer); controller.abort(); this.controlRequests.delete(controller); this.ptzBusy = false; }
|
|
280
|
+
}
|
|
236
281
|
setNightVision(mode) {
|
|
237
282
|
if (!['on', 'off', 'auto'].includes(mode)) return Promise.reject(new RuntimeError('invalid_request'));
|
|
238
283
|
return this._nightVision(mode);
|
|
@@ -407,6 +452,12 @@ export class RuntimeManager extends EventEmitter {
|
|
|
407
452
|
if (value?.success === false) {
|
|
408
453
|
eventFailure = REASONS.has(value.reason) ? value.reason : 'backend_failed';
|
|
409
454
|
if (Number.isInteger(value.vendor) && value.vendor >= 0 && value.vendor <= 100) this.vendor = value.vendor;
|
|
455
|
+
} else if (value?.event === 'control_ready' && value.success === true
|
|
456
|
+
&& Number.isInteger(value.controlPort) && value.controlPort >= 1024 && value.controlPort <= 65535
|
|
457
|
+
&& typeof value.controlToken === 'string' && /^[a-f0-9]{64}$/.test(value.controlToken)) {
|
|
458
|
+
this.ptzControl = { port: value.controlPort, token: value.controlToken };
|
|
459
|
+
} else if (value?.event === 'control_unavailable' && value.success === true) {
|
|
460
|
+
this.ptzControl = null;
|
|
410
461
|
} else if (value?.event === 'audio_ready' && value.success === true && this.enableAudio
|
|
411
462
|
&& Number.isInteger(value.audioPort) && value.audioPort >= 1024 && value.audioPort <= 65535
|
|
412
463
|
&& ['PCMA', 'OPUS'].includes(value.audioCodec) && [8000, 16000, 48000].includes(value.sampleRate)
|
|
@@ -490,6 +541,7 @@ export class RuntimeManager extends EventEmitter {
|
|
|
490
541
|
}
|
|
491
542
|
async stop() {
|
|
492
543
|
this.wantRunning = false; this.generation++;
|
|
544
|
+
this.ptzControl = null;
|
|
493
545
|
for (const controller of this.controlRequests) controller.abort();
|
|
494
546
|
this.controlRequests.clear();
|
|
495
547
|
clearTimeout(this.retryTimer); clearTimeout(this.bootTimer); this.retryTimer = null; this.bootTimer = null;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linanw/homebridge-mijia-remote-camera",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@linanw/homebridge-mijia-remote-camera",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.3.0",
|
|
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
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Standalone Xiaomi remote camera plugin with HomeKit and experimental Matter camera streaming",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "linanw",
|