export function mount({ root, state, host }) { state = state || {}; const __cleanup = []; const __setInterval = (...args) => { const id = window.setInterval(...args); __cleanup.push(() => window.clearInterval(id)); return id; }; const __setTimeout = (...args) => { const id = window.setTimeout(...args); __cleanup.push(() => window.clearTimeout(id)); return id; }; if (!(root instanceof HTMLElement)) return; const valueEl = root.querySelector("[data-co2-value]"); const statusEl = root.querySelector("[data-co2-status]"); const updatedEl = root.querySelector("[data-co2-updated]"); if (!(valueEl instanceof HTMLElement) || !(statusEl instanceof HTMLElement) || !(updatedEl instanceof HTMLElement)) return; const configuredToolName = typeof state.tool_name === 'string' ? state.tool_name.trim() : ''; const matchName = typeof state.match_name === 'string' ? state.match_name.trim() : 'Bedroom-Esp-Sensor CO2'; const INTERVAL_RAW = Number(state.refresh_ms); const INTERVAL_MS = Number.isFinite(INTERVAL_RAW) && INTERVAL_RAW >= 1000 ? INTERVAL_RAW : 15000; const setStatus = (label, color) => { statusEl.textContent = label; statusEl.style.color = color; }; const setUpdatedNow = () => { updatedEl.textContent = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", }); }; const parseValue = (raw) => { const n = Number(raw); return Number.isFinite(n) ? Math.round(n) : null; }; const computeScore = (value) => { if (!Number.isFinite(value)) return 0; if (value >= 1600) return 96; if (value >= 1400) return 90; if (value >= 1200) return 82; if (value >= 1000) return 68; if (value >= 900) return 54; if (value >= 750) return 34; return 16; }; const stripQuotes = (value) => { const text = String(value ?? '').trim(); if ((text.startsWith("'") && text.endsWith("'")) || (text.startsWith('"') && text.endsWith('"'))) { return text.slice(1, -1); } return text; }; const normalizeText = (value) => String(value || '').trim().toLowerCase(); const parseLiveContextEntries = (payloadText) => { const text = String(payloadText || '').replace(/\r/g, ''); const startIndex = text.indexOf('- names: '); const relevant = startIndex >= 0 ? text.slice(startIndex) : text; const entries = []; let current = null; let inAttributes = false; const pushCurrent = () => { if (current) entries.push(current); current = null; inAttributes = false; }; for (const rawLine of relevant.split('\n')) { if (rawLine.startsWith('- names: ')) { pushCurrent(); current = { name: stripQuotes(rawLine.slice(9)), domain: '', state: '', attributes: {}, }; continue; } if (!current) continue; const trimmed = rawLine.trim(); if (!trimmed) continue; if (trimmed === 'attributes:') { inAttributes = true; continue; } if (rawLine.startsWith(' domain:')) { current.domain = stripQuotes(rawLine.slice(rawLine.indexOf(':') + 1)); inAttributes = false; continue; } if (rawLine.startsWith(' state:')) { current.state = stripQuotes(rawLine.slice(rawLine.indexOf(':') + 1)); inAttributes = false; continue; } if (inAttributes && rawLine.startsWith(' ')) { const separatorIndex = rawLine.indexOf(':'); if (separatorIndex >= 0) { const key = rawLine.slice(4, separatorIndex).trim(); const value = stripQuotes(rawLine.slice(separatorIndex + 1)); current.attributes[key] = value; } continue; } inAttributes = false; } pushCurrent(); return entries; }; const extractLiveContextText = (toolResult) => { if (toolResult && toolResult.parsed && typeof toolResult.parsed === 'object') { const parsed = toolResult.parsed; if (typeof parsed.result === 'string') return parsed.result; } if (typeof toolResult?.content === 'string') { try { const parsed = JSON.parse(toolResult.content); if (parsed && typeof parsed === 'object' && typeof parsed.result === 'string') { return parsed.result; } } catch { return toolResult.content; } return toolResult.content; } return ''; }; const resolveToolName = async () => { if (configuredToolName) return configuredToolName; if (!host.listTools) return 'mcp_home_assistant_GetLiveContext'; try { const tools = await host.listTools(); const liveContextTool = Array.isArray(tools) ? tools.find((tool) => /(^|_)GetLiveContext$/i.test(String(tool?.name || ''))) : null; return liveContextTool?.name || 'mcp_home_assistant_GetLiveContext'; } catch { return 'mcp_home_assistant_GetLiveContext'; } }; const refresh = async () => { setStatus("Refreshing", "var(--theme-status-muted)"); try { const toolName = await resolveToolName(); const toolResult = await host.callTool(toolName, {}); const entries = parseLiveContextEntries(extractLiveContextText(toolResult)); const entry = entries.find((item) => normalizeText(item.name) === normalizeText(matchName)); if (!entry) throw new Error(`Missing sensor ${matchName}`); const value = parseValue(entry.state); if (value === null) throw new Error("Invalid sensor payload"); valueEl.textContent = String(value); if (value >= 1200) setStatus("High", "var(--theme-status-danger)"); else if (value >= 900) setStatus("Elevated", "var(--theme-status-warning)"); else setStatus("Good", "var(--theme-status-live)"); setUpdatedNow(); host.setLiveContent({ kind: 'sensor', tool_name: toolName, match_name: entry.name, value, display_value: String(value), unit: entry.attributes?.unit_of_measurement || 'ppm', status: statusEl.textContent || null, updated_at: updatedEl.textContent || null, score: computeScore(value), }); } catch (err) { valueEl.textContent = "--"; setStatus("Unavailable", "var(--theme-status-danger)"); updatedEl.textContent = String(err); host.setLiveContent({ kind: 'sensor', tool_name: configuredToolName || 'mcp_home_assistant_GetLiveContext', match_name: matchName, value: null, display_value: '--', unit: 'ppm', status: 'Unavailable', updated_at: String(err), error: String(err), score: 0, }); } }; host.setRefreshHandler(() => { void refresh(); }); void refresh(); __setInterval(() => { void refresh(); }, INTERVAL_MS); return { destroy() { host.setRefreshHandler(null); host.setLiveContent(null); host.clearSelection(); for (const cleanup of __cleanup.splice(0)) cleanup(); }, }; }