Created a vs code extension with Claude

This commit is contained in:
2026-04-17 13:35:57 +02:00
parent 923cfd4152
commit 3e1f0f3ca9
12 changed files with 1217 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
import * as vscode from 'vscode';
import * as https from 'https';
import * as http from 'http';
import { URL } from 'url';
export interface HAEntity {
entity_id: string;
state: string;
attributes: Record<string, unknown>;
last_changed: string;
last_updated: string;
}
/** 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 HAClient {
private entities: HAEntity[] = [];
private entityMap: Map<string, HAEntity> = new Map();
private refreshTimer: ReturnType<typeof setInterval> | undefined;
private _onEntitiesUpdated = new vscode.EventEmitter<HAEntity[]>();
readonly onEntitiesUpdated = this._onEntitiesUpdated.event;
private log: vscode.OutputChannel;
constructor(log: vscode.OutputChannel) {
this.log = log;
this.startAutoRefresh();
}
private getConfig() {
const config = vscode.workspace.getConfiguration('appdaemon');
return {
url: normalizeUrl(config.get<string>('haUrl', 'http://localhost:8123')),
token: config.get<string>('haToken', ''),
refreshInterval: config.get<number>('entityRefreshInterval', 300)
};
}
private async request<T = unknown>(method: string, path: string, body?: unknown): Promise<T> {
const { url, token } = this.getConfig();
if (!token) {
throw new Error('HA token not configured — set appdaemon.haToken in settings');
}
const fullUrl = new URL(path, url);
const isHttps = fullUrl.protocol === 'https:';
const lib = isHttps ? https : http;
this.log.appendLine(`[HA] ${method} ${fullUrl.href}`);
return new Promise<T>((resolve, reject) => {
const options: http.RequestOptions = {
hostname: fullUrl.hostname,
port: parseInt(fullUrl.port, 10) || (isHttps ? 443 : 8123),
path: fullUrl.pathname + fullUrl.search,
method,
headers: {
'Authorization': `Bearer ${token}`,
'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(`[HA] ${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(`[HA] ERROR: ${err.message}`);
reject(err);
});
req.setTimeout(15000, () => {
req.destroy();
reject(new Error('Request timed out'));
});
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
async fetchEntities(): Promise<HAEntity[]> {
try {
this.entities = await this.request<HAEntity[]>('GET', '/api/states');
this.entityMap.clear();
for (const entity of this.entities) {
this.entityMap.set(entity.entity_id, entity);
}
this.log.appendLine(`[HA] Loaded ${this.entities.length} entities (${this.getDomains().length} domains)`);
this._onEntitiesUpdated.fire(this.entities);
return this.entities;
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
this.log.appendLine(`[HA] fetchEntities FAILED: ${msg}`);
vscode.window.showWarningMessage(`AppDaemon: Failed to fetch entities — ${msg}`);
return this.entities;
}
}
getEntities(): HAEntity[] {
return this.entities;
}
getEntity(entityId: string): HAEntity | undefined {
return this.entityMap.get(entityId);
}
getDomains(): string[] {
const domains = new Set<string>();
for (const entity of this.entities) {
const dot = entity.entity_id.indexOf('.');
if (dot > 0) {
domains.add(entity.entity_id.substring(0, dot));
}
}
return Array.from(domains).sort();
}
async restartHA(): Promise<void> {
await this.request('POST', '/api/services/homeassistant/restart');
}
async restartHost(): Promise<void> {
await this.request('POST', '/api/services/homeassistant/restart_host');
}
private startAutoRefresh() {
const { refreshInterval } = this.getConfig();
if (refreshInterval > 0) {
this.refreshTimer = setInterval(() => {
this.fetchEntities();
}, refreshInterval * 1000);
}
}
dispose() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
}
this._onEntitiesUpdated.dispose();
}
}