573 lines
24 KiB
TypeScript
573 lines
24 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import { HAClient } from './haClient';
|
|
import { ADClient } from './adClient';
|
|
import { StatusBarManager } from './statusBar';
|
|
import { EntityCompletionProvider, EntityHoverProvider } from './entityProvider';
|
|
import { ErrorViewer } from './errorViewer';
|
|
import { LogExplorer } from './logExplorer';
|
|
|
|
let haClient: HAClient;
|
|
let adClient: ADClient;
|
|
let statusBar: StatusBarManager;
|
|
let errorViewer: ErrorViewer;
|
|
let outputChannel: vscode.OutputChannel;
|
|
|
|
// Non-app top-level YAML keys to ignore
|
|
const NON_APP_KEYS = new Set([
|
|
'global_modules', 'global_dependencies', 'secrets',
|
|
'appdaemon', 'http', 'hadashboard', 'admin', 'api', 'plugins'
|
|
]);
|
|
|
|
// ── App parsing helpers ───────────────────────────────────────────────────────
|
|
|
|
type AppEntry = { appName: string; line: number; moduleName: string };
|
|
|
|
function parseAppEntries(text: string): AppEntry[] {
|
|
const lines = text.split('\n');
|
|
const entries: AppEntry[] = [];
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const m = lines[i].match(/^([a-z][a-z0-9_]*):\s*$/);
|
|
if (!m) { continue; }
|
|
const key = m[1];
|
|
if (NON_APP_KEYS.has(key)) { continue; }
|
|
let moduleName = '';
|
|
for (let j = i + 1; j < Math.min(i + 20, lines.length); j++) {
|
|
if (lines[j].match(/^\S/)) { break; }
|
|
const mod = lines[j].match(/^\s+module\s*:\s*(\S+)/);
|
|
if (mod) { moduleName = mod[1]; entries.push({ appName: key, line: i, moduleName }); break; }
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
/**
|
|
* Parse AppDaemon app names from a YAML document.
|
|
*/
|
|
function parseAppsFromDocument(doc: vscode.TextDocument): string[] {
|
|
if (doc.languageId !== 'yaml') { return []; }
|
|
return parseAppEntries(doc.getText()).map(e => e.appName);
|
|
}
|
|
|
|
/**
|
|
* Scan all YAML files in the workspace and return app definitions with location.
|
|
*/
|
|
async function findAllApps(): Promise<Array<{
|
|
appName: string;
|
|
uri: vscode.Uri;
|
|
line: number;
|
|
fileName: string;
|
|
moduleName: string;
|
|
}>> {
|
|
const yamlFiles = await vscode.workspace.findFiles('**/*.yaml', '**/node_modules/**');
|
|
const results: Array<{ appName: string; uri: vscode.Uri; line: number; fileName: string; moduleName: string }> = [];
|
|
|
|
for (const uri of yamlFiles) {
|
|
let doc: vscode.TextDocument;
|
|
try {
|
|
doc = await vscode.workspace.openTextDocument(uri);
|
|
} catch {
|
|
continue;
|
|
}
|
|
const fileName = vscode.workspace.asRelativePath(uri);
|
|
for (const entry of parseAppEntries(doc.getText())) {
|
|
results.push({ ...entry, uri, fileName });
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// ── Virtual Sensor parsing ─────────────────────────────────────────────────
|
|
|
|
type VirtualSensorEntry = {
|
|
sensorId: string;
|
|
line: number;
|
|
appName: string;
|
|
uri: vscode.Uri;
|
|
fileName: string;
|
|
};
|
|
|
|
type VSSectionType = 'sensors' | 'continuous_conditions' | 'averagers' | 'binary_sensors' | 'value_selectors' | 'retain_conditions' | 'default_values' | '';
|
|
|
|
function mapVirtualSensorId(key: string, section: VSSectionType): string | null {
|
|
if (section === 'default_values') {
|
|
return /^(binary_sensor|sensor)\.[a-z0-9_]+$/.test(key) ? key : null;
|
|
}
|
|
if (section === 'sensors') {
|
|
const dot = key.indexOf('.');
|
|
if (dot < 0) { return null; }
|
|
const prefix = key.slice(0, dot);
|
|
const name = key.slice(dot + 1);
|
|
switch (prefix) {
|
|
case 'sensor': case 'value_selector': case 'averager': return `sensor.${name}`;
|
|
case 'binary_sensor': case 'continuous_condition': case 'retain_condition': return `binary_sensor.${name}`;
|
|
default: return null;
|
|
}
|
|
}
|
|
if (section === 'continuous_conditions' || section === 'retain_conditions' || section === 'binary_sensors') {
|
|
return key.includes('.') ? null : `binary_sensor.${key}`;
|
|
}
|
|
if (section === 'averagers' || section === 'value_selectors') {
|
|
return key.includes('.') ? null : `sensor.${key}`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function parseVSSectionName(raw: string, indent: number): VSSectionType {
|
|
const re = new RegExp(`^\\s{${indent}}(sensors|continuous_conditions|averagers|binary_sensors|value_selectors|retain_conditions|default_values):\\s*$`);
|
|
const m = raw.match(re);
|
|
return m ? m[1] as VSSectionType : '';
|
|
}
|
|
|
|
function parseVirtualSensorsFromText(text: string): Array<{ sensorId: string; line: number; appName: string }> {
|
|
const lines = text.split('\n');
|
|
const entries: Array<{ sensorId: string; line: number; appName: string }> = [];
|
|
|
|
let currentApp = '';
|
|
let isVSApp = false; // module: virtualsensors
|
|
let vsSection: VSSectionType = ''; // active sub-section in VirtualSensorsApp at indent=2
|
|
let inVSKey = false; // in virtual_sensors: sub-block of another app
|
|
let nestedSection: VSSectionType = ''; // active sub-section in virtual_sensors: at indent=4
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const raw = lines[i];
|
|
const trimmed = raw.trimStart();
|
|
if (!trimmed || trimmed.startsWith('#')) { continue; }
|
|
const indent = raw.length - trimmed.length;
|
|
|
|
if (indent === 0) {
|
|
const m = raw.match(/^([a-z][a-z0-9_]*):\s*$/);
|
|
currentApp = m ? m[1] : '';
|
|
isVSApp = false; vsSection = ''; inVSKey = false; nestedSection = '';
|
|
continue;
|
|
}
|
|
|
|
if (indent === 2) {
|
|
vsSection = ''; inVSKey = false; nestedSection = '';
|
|
if (/^\s+module:\s*virtualsensors/.test(raw)) {
|
|
isVSApp = true;
|
|
} else if (isVSApp) {
|
|
vsSection = parseVSSectionName(raw, 2);
|
|
} else {
|
|
if (/^\s+virtual_sensors:\s*$/.test(raw)) { inVSKey = true; }
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (indent === 4) {
|
|
nestedSection = '';
|
|
if (isVSApp && vsSection) {
|
|
const km = raw.match(/^\s+([a-z][a-z0-9_.]+):/);
|
|
if (km) {
|
|
const sensorId = mapVirtualSensorId(km[1], vsSection);
|
|
if (sensorId) { entries.push({ sensorId, line: i, appName: currentApp }); }
|
|
}
|
|
} else if (inVSKey) {
|
|
nestedSection = parseVSSectionName(raw, 4);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (indent === 6 && inVSKey && nestedSection) {
|
|
const km = raw.match(/^\s+([a-z][a-z0-9_.]+):/);
|
|
if (km) {
|
|
const sensorId = mapVirtualSensorId(km[1], nestedSection);
|
|
if (sensorId) { entries.push({ sensorId, line: i, appName: currentApp }); }
|
|
}
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
async function findAllVirtualSensors(): Promise<VirtualSensorEntry[]> {
|
|
const yamlFiles = await vscode.workspace.findFiles('**/*.yaml', '**/node_modules/**');
|
|
const results: VirtualSensorEntry[] = [];
|
|
for (const uri of yamlFiles) {
|
|
let doc: vscode.TextDocument;
|
|
try { doc = await vscode.workspace.openTextDocument(uri); } catch { continue; }
|
|
const fileName = vscode.workspace.asRelativePath(uri);
|
|
for (const e of parseVirtualSensorsFromText(doc.getText())) {
|
|
results.push({ ...e, uri, fileName });
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// ── Symbol Providers ─────────────────────────────────────────────────────────
|
|
|
|
class ADDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
|
provideDocumentSymbols(doc: vscode.TextDocument): vscode.DocumentSymbol[] {
|
|
if (doc.languageId !== 'yaml') { return []; }
|
|
return parseAppEntries(doc.getText()).map(entry => {
|
|
const pos = new vscode.Position(entry.line, 0);
|
|
const range = new vscode.Range(pos, pos);
|
|
const sym = new vscode.DocumentSymbol(
|
|
entry.appName,
|
|
`module: ${entry.moduleName}`,
|
|
vscode.SymbolKind.Class,
|
|
range,
|
|
range
|
|
);
|
|
return sym;
|
|
});
|
|
}
|
|
}
|
|
|
|
class ADWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
|
|
async provideWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {
|
|
const lq = query.toLowerCase();
|
|
const [apps, sensors] = await Promise.all([findAllApps(), findAllVirtualSensors()]);
|
|
const appSymbols = apps
|
|
.filter(a => !lq || a.appName.toLowerCase().includes(lq))
|
|
.map(a => new vscode.SymbolInformation(
|
|
a.appName,
|
|
vscode.SymbolKind.Class,
|
|
`module: ${a.moduleName}`,
|
|
new vscode.Location(a.uri, new vscode.Position(a.line, 0))
|
|
));
|
|
const sensorSymbols = sensors
|
|
.filter(s => !lq || s.sensorId.toLowerCase().includes(lq))
|
|
.map(s => new vscode.SymbolInformation(
|
|
s.sensorId,
|
|
vscode.SymbolKind.Variable,
|
|
s.appName,
|
|
new vscode.Location(s.uri, new vscode.Position(s.line, 0))
|
|
));
|
|
return [...appSymbols, ...sensorSymbols];
|
|
}
|
|
}
|
|
|
|
class ADVirtualSensorDefinitionProvider implements vscode.DefinitionProvider {
|
|
// Simple cache: reset on save
|
|
private cache: VirtualSensorEntry[] | undefined;
|
|
private cacheExpiry = 0;
|
|
|
|
constructor(private haClient: HAClient) {}
|
|
|
|
invalidate() { this.cache = undefined; }
|
|
|
|
async getSensors(): Promise<VirtualSensorEntry[]> {
|
|
const now = Date.now();
|
|
if (this.cache && now < this.cacheExpiry) { return this.cache; }
|
|
this.cache = await findAllVirtualSensors();
|
|
this.cacheExpiry = now + 5000; // 5 s TTL
|
|
return this.cache;
|
|
}
|
|
|
|
async provideDefinition(
|
|
document: vscode.TextDocument,
|
|
position: vscode.Position
|
|
): Promise<vscode.Definition | undefined> {
|
|
// Match any entity ID pattern (same as hover provider)
|
|
const wordRange = document.getWordRangeAtPosition(
|
|
position,
|
|
/[a-z][a-z_]*\.[a-z0-9][a-z0-9_]*/
|
|
);
|
|
if (!wordRange) { return undefined; }
|
|
const entityId = document.getText(wordRange);
|
|
|
|
// 1. Check virtual sensor index (parsed from YAML)
|
|
const sensors = await this.getSensors();
|
|
const vsMatch = sensors.find(s => s.sensorId === entityId);
|
|
if (vsMatch) {
|
|
return new vscode.Location(vsMatch.uri, new vscode.Position(vsMatch.line, 0));
|
|
}
|
|
|
|
// 2. Fallback: check HA entity's ad_app attribute → navigate to that app
|
|
const entity = this.haClient.getEntity(entityId);
|
|
const adApp = entity?.attributes?.ad_app as string | undefined;
|
|
if (!adApp) { return undefined; }
|
|
const apps = await findAllApps();
|
|
const appMatch = apps.find(a => a.appName === adApp);
|
|
if (!appMatch) { return undefined; }
|
|
return new vscode.Location(appMatch.uri, new vscode.Position(appMatch.line, 0));
|
|
}
|
|
}
|
|
|
|
class ADVirtualSensorHoverProvider implements vscode.HoverProvider {
|
|
constructor(
|
|
private haClient: HAClient,
|
|
private defProvider: ADVirtualSensorDefinitionProvider
|
|
) {}
|
|
|
|
async provideHover(
|
|
document: vscode.TextDocument,
|
|
position: vscode.Position
|
|
): Promise<vscode.Hover | undefined> {
|
|
// Find a virtual sensor whose definition is on this exact line
|
|
const sensors = await this.defProvider.getSensors();
|
|
const match = sensors.find(
|
|
s => s.uri.fsPath === document.uri.fsPath && s.line === position.line
|
|
);
|
|
if (!match) { return undefined; }
|
|
|
|
const entity = this.haClient.getEntity(match.sensorId);
|
|
if (!entity) { return undefined; }
|
|
|
|
const md = new vscode.MarkdownString();
|
|
md.isTrusted = true;
|
|
const friendly = entity.attributes?.friendly_name as string | undefined;
|
|
md.appendMarkdown(`### ${friendly || entity.entity_id}\n\n`);
|
|
md.appendMarkdown(`| | |\n|---|---|\n`);
|
|
md.appendMarkdown(`| **Entity** | \`${entity.entity_id}\` |\n`);
|
|
md.appendMarkdown(`| **State** | **\`${entity.state}\`** |\n`);
|
|
if (entity.attributes) {
|
|
const skip = new Set(['friendly_name', 'icon', 'entity_picture', 'supported_features', 'supported_color_modes']);
|
|
for (const [key, value] of Object.entries(entity.attributes).filter(([k]) => !skip.has(k)).slice(0, 12)) {
|
|
const display = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
const truncated = display.length > 60 ? display.substring(0, 57) + '...' : display;
|
|
md.appendMarkdown(`| ${key.replace(/\|/g, '\\|')} | \`${truncated.replace(/\|/g, '\\|')}\` |\n`);
|
|
}
|
|
}
|
|
md.appendMarkdown(`\n*Last changed: ${entity.last_changed}*`);
|
|
return new vscode.Hover(md);
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
function updateContextualButton(editor: vscode.TextEditor | undefined) {
|
|
if (!editor) { statusBar.updateContextualApps([]); return; }
|
|
const apps = parseAppsFromDocument(editor.document);
|
|
statusBar.updateContextualApps(apps);
|
|
}
|
|
|
|
export async function activate(context: vscode.ExtensionContext) {
|
|
// ── Output channel for debug ─────────────────────────────────────────────
|
|
outputChannel = vscode.window.createOutputChannel('AppDaemon');
|
|
outputChannel.appendLine('AppDaemon extension activating...');
|
|
|
|
const config = vscode.workspace.getConfiguration('appdaemon');
|
|
outputChannel.appendLine(` haUrl = ${config.get('haUrl')}`);
|
|
outputChannel.appendLine(` haToken = ${config.get<string>('haToken', '') ? '***set***' : '*** NOT SET ***'}`);
|
|
outputChannel.appendLine(` adUrl = ${config.get('adUrl')}`);
|
|
|
|
// ── Clients ──────────────────────────────────────────────────────────────
|
|
haClient = new HAClient(outputChannel);
|
|
adClient = new ADClient(outputChannel);
|
|
errorViewer = new ErrorViewer();
|
|
const logExplorer = new LogExplorer(context, count => statusBar.setLogErrorCount(count));
|
|
context.subscriptions.push(logExplorer, vscode.commands.registerCommand('appdaemon.exploreLogs', () => logExplorer.show()));
|
|
statusBar = new StatusBarManager();
|
|
logExplorer.startPolling();
|
|
|
|
// ── Contextual per-file restart button ───────────────────────────────────
|
|
updateContextualButton(vscode.window.activeTextEditor);
|
|
context.subscriptions.push(
|
|
vscode.window.onDidChangeActiveTextEditor(updateContextualButton),
|
|
vscode.workspace.onDidSaveTextDocument(doc => {
|
|
if (vscode.window.activeTextEditor?.document === doc) {
|
|
updateContextualButton(vscode.window.activeTextEditor);
|
|
}
|
|
})
|
|
);
|
|
|
|
// Initial entity load (non-blocking)
|
|
haClient.fetchEntities().then(entities => {
|
|
if (entities.length > 0) {
|
|
outputChannel.appendLine(`Ready: ${entities.length} entities loaded`);
|
|
} else {
|
|
outputChannel.appendLine('Warning: 0 entities loaded — check haUrl and haToken settings');
|
|
}
|
|
});
|
|
|
|
// Try to read current production mode from AD
|
|
adClient.fetchProductionMode().catch(() => { /* ignore on startup */ });
|
|
|
|
// ── Language features (YAML + Python) ────────────────────────────────────
|
|
const selector: vscode.DocumentSelector = [
|
|
{ scheme: 'file', language: 'yaml' },
|
|
{ scheme: 'file', language: 'python' }
|
|
];
|
|
|
|
const virtualSensorDefProvider = new ADVirtualSensorDefinitionProvider(haClient);
|
|
|
|
// Invalidate definition cache on save so edits are picked up immediately
|
|
context.subscriptions.push(
|
|
vscode.workspace.onDidSaveTextDocument(doc => {
|
|
if (doc.languageId === 'yaml') { virtualSensorDefProvider.invalidate(); }
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.languages.registerCompletionItemProvider(
|
|
selector,
|
|
new EntityCompletionProvider(haClient),
|
|
'.' // trigger completion when user types a dot
|
|
),
|
|
vscode.languages.registerHoverProvider(
|
|
selector,
|
|
new EntityHoverProvider(haClient)
|
|
),
|
|
vscode.languages.registerDocumentSymbolProvider(
|
|
{ scheme: 'file', language: 'yaml' },
|
|
new ADDocumentSymbolProvider()
|
|
),
|
|
vscode.languages.registerWorkspaceSymbolProvider(
|
|
new ADWorkspaceSymbolProvider()
|
|
),
|
|
vscode.languages.registerDefinitionProvider(
|
|
{ scheme: 'file', language: 'yaml' },
|
|
virtualSensorDefProvider
|
|
),
|
|
vscode.languages.registerHoverProvider(
|
|
{ scheme: 'file', language: 'yaml' },
|
|
new ADVirtualSensorHoverProvider(haClient, virtualSensorDefProvider)
|
|
)
|
|
);
|
|
|
|
// ── Commands ─────────────────────────────────────────────────────────────
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('appdaemon.goToVirtualSensor', async () => {
|
|
const sensors = await findAllVirtualSensors();
|
|
if (sensors.length === 0) {
|
|
vscode.window.showWarningMessage('AppDaemon: No virtual sensors found in workspace');
|
|
return;
|
|
}
|
|
type SensorItem = vscode.QuickPickItem & { sensor: VirtualSensorEntry };
|
|
const items: SensorItem[] = sensors.map(s => ({
|
|
label: `$(circuit-board) ${s.sensorId}`,
|
|
description: s.fileName,
|
|
detail: `in ${s.appName}`,
|
|
sensor: s
|
|
}));
|
|
const picked = await vscode.window.showQuickPick(items, {
|
|
placeHolder: 'Go to virtual sensor…',
|
|
matchOnDescription: true,
|
|
matchOnDetail: true
|
|
});
|
|
if (!picked) { return; }
|
|
const doc = await vscode.workspace.openTextDocument(picked.sensor.uri);
|
|
const editor = await vscode.window.showTextDocument(doc);
|
|
const pos = new vscode.Position(picked.sensor.line, 0);
|
|
editor.selection = new vscode.Selection(pos, pos);
|
|
editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter);
|
|
}),
|
|
|
|
vscode.commands.registerCommand('appdaemon.goToApp', async (requestedName?: string) => {
|
|
const apps = (await findAllApps()).filter(app => !requestedName || app.appName === requestedName);
|
|
if (apps.length === 0) {
|
|
vscode.window.showWarningMessage(requestedName
|
|
? `AppDaemon: No definition found for ${requestedName}`
|
|
: 'AppDaemon: No app definitions found in workspace');
|
|
return;
|
|
}
|
|
|
|
type AppItem = vscode.QuickPickItem & { app: typeof apps[0] };
|
|
const items: AppItem[] = apps.map(app => ({
|
|
label: `$(symbol-class) ${app.appName}`,
|
|
description: app.fileName,
|
|
detail: `Line ${app.line + 1}`,
|
|
app
|
|
}));
|
|
|
|
const picked = apps.length === 1 && requestedName ? items[0] : await vscode.window.showQuickPick(items, {
|
|
placeHolder: 'Go to AppDaemon app…',
|
|
matchOnDescription: true,
|
|
matchOnDetail: false
|
|
});
|
|
|
|
if (!picked) { return; }
|
|
const doc = await vscode.workspace.openTextDocument(picked.app.uri);
|
|
const editor = await vscode.window.showTextDocument(doc);
|
|
const pos = new vscode.Position(picked.app.line, 0);
|
|
editor.selection = new vscode.Selection(pos, pos);
|
|
editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter);
|
|
}),
|
|
|
|
vscode.commands.registerCommand('appdaemon.restartCurrentFileApps', async () => {
|
|
const editor = vscode.window.activeTextEditor;
|
|
if (!editor) {
|
|
vscode.window.showWarningMessage('AppDaemon: No active file');
|
|
return;
|
|
}
|
|
const apps = parseAppsFromDocument(editor.document);
|
|
if (apps.length === 0) {
|
|
vscode.window.showWarningMessage('AppDaemon: No app definitions found in this file');
|
|
return;
|
|
}
|
|
try {
|
|
await adClient.restartApps(apps);
|
|
vscode.window.showInformationMessage(`AppDaemon: Restarted ${apps.join(', ')}`);
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
vscode.window.showErrorMessage(`AppDaemon: Restart failed — ${msg}`);
|
|
}
|
|
}),
|
|
|
|
vscode.commands.registerCommand('appdaemon.restartAD', async () => {
|
|
try {
|
|
await adClient.reloadApps();
|
|
vscode.window.showInformationMessage('AppDaemon: Apps reloaded');
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
vscode.window.showErrorMessage(`AppDaemon: Reload failed — ${msg}`);
|
|
}
|
|
}),
|
|
|
|
vscode.commands.registerCommand('appdaemon.restartHA', async () => {
|
|
const answer = await vscode.window.showWarningMessage(
|
|
'Restart Home Assistant?',
|
|
{ modal: true },
|
|
'Restart HA',
|
|
'Restart Host'
|
|
);
|
|
if (!answer) {
|
|
return;
|
|
}
|
|
try {
|
|
if (answer === 'Restart Host') {
|
|
await haClient.restartHost();
|
|
vscode.window.showInformationMessage('Home Assistant: Host restart initiated');
|
|
} else {
|
|
await haClient.restartHA();
|
|
vscode.window.showInformationMessage('Home Assistant: Restart initiated');
|
|
}
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
vscode.window.showErrorMessage(`Home Assistant: Restart failed — ${msg}`);
|
|
}
|
|
}),
|
|
|
|
vscode.commands.registerCommand('appdaemon.toggleProductionMode', async () => {
|
|
try {
|
|
await adClient.toggleProductionMode();
|
|
const mode = adClient.isProductionMode() ? 'ON' : 'OFF';
|
|
vscode.window.showInformationMessage(`AppDaemon: Production mode ${mode}`);
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
vscode.window.showErrorMessage(`AppDaemon: Toggle failed — ${msg}`);
|
|
}
|
|
}),
|
|
|
|
vscode.commands.registerCommand('appdaemon.showErrors', () => {
|
|
errorViewer.show();
|
|
}),
|
|
vscode.commands.registerCommand('appdaemon.refreshEntities', async () => {
|
|
const entities = await haClient.fetchEntities();
|
|
vscode.window.showInformationMessage(`AppDaemon: ${entities.length} entities loaded`);
|
|
}),
|
|
|
|
vscode.commands.registerCommand('appdaemon.clearErrors', () => {
|
|
errorViewer.clearDiagnostics();
|
|
vscode.window.showInformationMessage('AppDaemon: Error diagnostics cleared');
|
|
})
|
|
);
|
|
|
|
// ── Disposables ──────────────────────────────────────────────────────────
|
|
context.subscriptions.push(
|
|
outputChannel,
|
|
{ dispose: () => haClient.dispose() },
|
|
{ dispose: () => adClient.dispose() },
|
|
{ dispose: () => statusBar.dispose() },
|
|
{ dispose: () => errorViewer.dispose() }
|
|
);
|
|
}
|
|
|
|
export function deactivate() {
|
|
haClient?.dispose();
|
|
adClient?.dispose();
|
|
statusBar?.dispose();
|
|
errorViewer?.dispose();
|
|
}
|