140 lines
6.3 KiB
TypeScript
140 lines
6.3 KiB
TypeScript
import type { ISynologyDsmCommand, ISynologyDsmCommandResult, ISynologyDsmConfig, ISynologyDsmEvent, ISynologyDsmSnapshot } from './synology_dsm.types.js';
|
|
import { SynologyDsmMapper } from './synology_dsm.mapper.js';
|
|
|
|
type TSynologyDsmEventHandler = (eventArg: ISynologyDsmEvent) => void;
|
|
|
|
export class SynologyDsmClient {
|
|
private currentSnapshot?: ISynologyDsmSnapshot;
|
|
private readonly eventHandlers = new Set<TSynologyDsmEventHandler>();
|
|
|
|
constructor(private readonly config: ISynologyDsmConfig) {}
|
|
|
|
public async getSnapshot(): Promise<ISynologyDsmSnapshot> {
|
|
if (this.config.nativeClient) {
|
|
this.currentSnapshot = this.normalizeSnapshot(await this.config.nativeClient.getSnapshot(), 'provider');
|
|
return this.cloneSnapshot(this.currentSnapshot);
|
|
}
|
|
|
|
if (this.config.snapshotProvider) {
|
|
const provided = await this.config.snapshotProvider();
|
|
if (provided) {
|
|
this.currentSnapshot = this.normalizeSnapshot(provided, 'provider');
|
|
return this.cloneSnapshot(this.currentSnapshot);
|
|
}
|
|
}
|
|
|
|
if (!this.currentSnapshot) {
|
|
this.currentSnapshot = SynologyDsmMapper.toSnapshot(this.config, this.config.connected ?? this.config.online ?? this.hasManualData());
|
|
if (!this.hasManualData()) {
|
|
this.currentSnapshot = {
|
|
...this.currentSnapshot,
|
|
connected: false,
|
|
error: this.config.host
|
|
? 'Synology DSM live HTTP API access is not implemented by this dependency-free TypeScript port. Provide nativeClient, snapshotProvider, or snapshot/manual data.'
|
|
: 'Synology DSM setup requires a NAS host plus nativeClient/snapshotProvider, or snapshot/manual data.',
|
|
};
|
|
}
|
|
}
|
|
return this.cloneSnapshot(this.currentSnapshot);
|
|
}
|
|
|
|
public onEvent(handlerArg: TSynologyDsmEventHandler): () => void {
|
|
this.eventHandlers.add(handlerArg);
|
|
return () => this.eventHandlers.delete(handlerArg);
|
|
}
|
|
|
|
public async refresh(): Promise<ISynologyDsmCommandResult> {
|
|
try {
|
|
this.currentSnapshot = undefined;
|
|
const snapshot = await this.getSnapshot();
|
|
const success = snapshot.connected || this.hasManualData() || Boolean(this.config.nativeClient || this.config.snapshotProvider);
|
|
this.emit({ type: success ? 'snapshot_refreshed' : 'refresh_failed', snapshot, error: success ? undefined : snapshot.error, timestamp: Date.now() });
|
|
return success ? { success: true, data: snapshot } : { success: false, error: snapshot.error, data: snapshot };
|
|
} catch (errorArg) {
|
|
const error = errorArg instanceof Error ? errorArg.message : String(errorArg);
|
|
const snapshot = SynologyDsmMapper.toSnapshot({ ...this.config, snapshot: this.currentSnapshot, online: false }, false);
|
|
this.currentSnapshot = { ...snapshot, error };
|
|
this.emit({ type: 'refresh_failed', snapshot: this.currentSnapshot, error, timestamp: Date.now() });
|
|
return { success: false, error, data: this.cloneSnapshot(this.currentSnapshot) };
|
|
}
|
|
}
|
|
|
|
public async sendCommand(commandArg: ISynologyDsmCommand): Promise<ISynologyDsmCommandResult> {
|
|
this.emit({ type: 'command_mapped', command: commandArg, deviceId: commandArg.deviceId, entityId: commandArg.entityId, timestamp: Date.now() });
|
|
|
|
const executor = this.config.commandExecutor || this.config.nativeClient?.executeCommand?.bind(this.config.nativeClient);
|
|
if (!executor) {
|
|
const result: ISynologyDsmCommandResult = {
|
|
success: false,
|
|
error: this.unsupportedCommandMessage(commandArg),
|
|
data: { command: commandArg },
|
|
};
|
|
this.emit({ type: 'command_failed', command: commandArg, data: result, deviceId: commandArg.deviceId, entityId: commandArg.entityId, timestamp: Date.now() });
|
|
return result;
|
|
}
|
|
|
|
try {
|
|
const result = this.commandResult(await executor(commandArg), commandArg);
|
|
this.emit({ type: result.success ? 'command_executed' : 'command_failed', command: commandArg, data: result, deviceId: commandArg.deviceId, entityId: commandArg.entityId, timestamp: Date.now() });
|
|
return result;
|
|
} catch (errorArg) {
|
|
const result: ISynologyDsmCommandResult = { success: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg), data: { command: commandArg } };
|
|
this.emit({ type: 'command_failed', command: commandArg, data: result, deviceId: commandArg.deviceId, entityId: commandArg.entityId, timestamp: Date.now() });
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public async destroy(): Promise<void> {
|
|
await this.config.nativeClient?.destroy?.();
|
|
this.eventHandlers.clear();
|
|
}
|
|
|
|
private normalizeSnapshot(snapshotArg: ISynologyDsmSnapshot, sourceArg: ISynologyDsmSnapshot['source']): ISynologyDsmSnapshot {
|
|
const normalized = SynologyDsmMapper.toSnapshot({ ...this.config, snapshot: this.cloneSnapshot(snapshotArg) }, snapshotArg.connected);
|
|
return { ...normalized, source: snapshotArg.source || sourceArg };
|
|
}
|
|
|
|
private hasManualData(): boolean {
|
|
return Boolean(
|
|
this.config.snapshot
|
|
|| this.config.system
|
|
|| this.config.information
|
|
|| this.config.utilization
|
|
|| this.config.storage
|
|
|| this.config.network
|
|
|| this.config.volumes?.length
|
|
|| this.config.disks?.length
|
|
|| this.config.cameras?.length
|
|
|| this.config.switches
|
|
|| this.config.update
|
|
|| this.config.security
|
|
);
|
|
}
|
|
|
|
private commandResult(resultArg: unknown, commandArg: ISynologyDsmCommand): ISynologyDsmCommandResult {
|
|
if (this.isCommandResult(resultArg)) {
|
|
return resultArg;
|
|
}
|
|
return { success: true, data: resultArg ?? { command: commandArg } };
|
|
}
|
|
|
|
private isCommandResult(valueArg: unknown): valueArg is ISynologyDsmCommandResult {
|
|
return Boolean(valueArg && typeof valueArg === 'object' && 'success' in valueArg);
|
|
}
|
|
|
|
private unsupportedCommandMessage(commandArg: ISynologyDsmCommand): string {
|
|
const action = commandArg.action.replace(/_/g, ' ');
|
|
return `Synology DSM live ${action} command execution is not faked by this native TypeScript port. Provide commandExecutor or nativeClient.executeCommand for live DSM system, Surveillance Station switch, or camera actions.`;
|
|
}
|
|
|
|
private emit(eventArg: ISynologyDsmEvent): void {
|
|
for (const handler of this.eventHandlers) {
|
|
handler(eventArg);
|
|
}
|
|
}
|
|
|
|
private cloneSnapshot<T extends ISynologyDsmSnapshot>(snapshotArg: T): T {
|
|
return JSON.parse(JSON.stringify(snapshotArg)) as T;
|
|
}
|
|
}
|