Files
ad_toolbox/vscode-appdaemon/src/adClient.ts

161 lines
5.7 KiB
TypeScript

import * as vscode from 'vscode';
import * as https from 'https';
import * as http from 'http';
import { URL } from 'url';
/** Ensure URL has http(s):// prefix and no trailing slash */
function normalizeUrl(raw: string): string {
let url = raw.trim().replace(/\/+$/, '');
if (!/^https?:\/\//i.test(url)) {
url = 'http://' + url;
}
return url;
}
export class ADClient {
private productionMode = false;
private _onProductionModeChanged = new vscode.EventEmitter<boolean>();
readonly onProductionModeChanged = this._onProductionModeChanged.event;
private log: vscode.OutputChannel;
constructor(log: vscode.OutputChannel) {
this.log = log;
}
private getConfig() {
const config = vscode.workspace.getConfiguration('appdaemon');
return {
url: normalizeUrl(config.get<string>('adUrl', 'http://localhost:5050'))
};
}
private async request<T = unknown>(method: string, path: string, body?: unknown): Promise<T> {
const { url } = this.getConfig();
const fullUrl = new URL(path, url);
const isHttps = fullUrl.protocol === 'https:';
const lib = isHttps ? https : http;
this.log.appendLine(`[AD] ${method} ${fullUrl.href}`);
return new Promise<T>((resolve, reject) => {
const options: http.RequestOptions = {
hostname: fullUrl.hostname,
port: parseInt(fullUrl.port, 10) || (isHttps ? 443 : 5050),
path: fullUrl.pathname + fullUrl.search,
method,
headers: {
'Content-Type': 'application/json'
}
};
const req = lib.request(options, (res) => {
let data = '';
res.on('data', (chunk: Buffer) => { data += chunk; });
res.on('end', () => {
this.log.appendLine(`[AD] ${res.statusCode} (${data.length} bytes)`);
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(data) as T);
} catch {
resolve(data as unknown as T);
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
}
});
});
req.on('error', (err) => {
this.log.appendLine(`[AD] ERROR: ${err.message}`);
reject(err);
});
req.setTimeout(10000, () => {
req.destroy();
reject(new Error('Request timed out'));
});
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
/**
* Restart a specific list of AppDaemon apps by name.
*/
async restartApps(appNames: string[]): Promise<void> {
this.log.appendLine(`[AD] Restarting specific apps: ${appNames.join(', ')}`);
await Promise.all(appNames.map(app =>
this.request('POST', '/api/appdaemon/service/admin/app/restart', { app })
));
}
/**
* Reload all AppDaemon apps by fetching the app list then restarting each one.
*/
async reloadApps(): Promise<void> {
// Fetch app names from admin state
type AdminState = { state: Record<string, unknown> };
const resp = await this.request<AdminState>('GET', '/api/appdaemon/state/admin');
const stateMap = resp?.state ?? (resp as unknown as Record<string, unknown>);
const appNames = Object.keys(stateMap)
.filter(k => k.startsWith('app.'))
.map(k => k.slice(4)); // strip "app." prefix
if (appNames.length === 0) {
throw new Error('No apps found in AppDaemon state');
}
this.log.appendLine(`[AD] Restarting ${appNames.length} apps: ${appNames.join(', ')}`);
await Promise.all(appNames.map(app =>
this.request('POST', '/api/appdaemon/service/admin/app/restart', { app })
));
}
/**
* Set AppDaemon production mode on/off via the admin API.
*/
async setProductionMode(mode: boolean): Promise<void> {
await this.request('POST', '/api/appdaemon/service/admin/production_mode/set', { mode });
this.productionMode = mode;
this._onProductionModeChanged.fire(mode);
}
/**
* Try to read the current production mode from AppDaemon state.
*/
async fetchProductionMode(): Promise<boolean> {
try {
const resp = await this.request<{ state: Record<string, { state?: unknown }> }>('GET', '/api/appdaemon/state/admin');
const state = resp?.state ?? resp as unknown as Record<string, { state?: unknown }>;
if (state && typeof state === 'object') {
for (const key of Object.keys(state)) {
if (key.includes('production_mode')) {
const val = (state[key] as { state?: unknown })?.state;
this.productionMode = val === true || val === 'True' || val === 'true';
this._onProductionModeChanged.fire(this.productionMode);
return this.productionMode;
}
}
}
} catch {
// Fallback to cached value
}
return this.productionMode;
}
isProductionMode(): boolean {
return this.productionMode;
}
async toggleProductionMode(): Promise<void> {
await this.setProductionMode(!this.productionMode);
}
dispose() {
this._onProductionModeChanged.dispose();
}
}