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 subtitleEl = root.querySelector('[data-weather-subtitle]'); const tempEl = root.querySelector('[data-weather-temp]'); const unitEl = root.querySelector('[data-weather-unit]'); const humidityEl = root.querySelector('[data-weather-humidity]'); const windEl = root.querySelector('[data-weather-wind]'); const rainEl = root.querySelector('[data-weather-rain]'); const uvEl = root.querySelector('[data-weather-uv]'); const statusEl = root.querySelector('[data-weather-status]'); if (!(subtitleEl instanceof HTMLElement) || !(tempEl instanceof HTMLElement) || !(unitEl instanceof HTMLElement) || !(humidityEl instanceof HTMLElement) || !(windEl instanceof HTMLElement) || !(rainEl instanceof HTMLElement) || !(uvEl instanceof HTMLElement) || !(statusEl instanceof HTMLElement)) return; const subtitle = typeof state.subtitle === 'string' ? state.subtitle : ''; const configuredToolName = typeof state.tool_name === 'string' ? state.tool_name.trim() : ''; const configuredForecastToolName = typeof state.forecast_tool_name === 'string' ? state.forecast_tool_name.trim() : 'exec'; const forecastCommand = typeof state.forecast_command === 'string' ? state.forecast_command.trim() : ''; const providerPrefix = typeof state.provider_prefix === 'string' ? state.provider_prefix.trim() : ''; const temperatureName = typeof state.temperature_name === 'string' ? state.temperature_name.trim() : ''; const humidityName = typeof state.humidity_name === 'string' ? state.humidity_name.trim() : ''; const uvName = typeof state.uv_name === 'string' ? state.uv_name.trim() : ''; const morningStartHourRaw = Number(state.morning_start_hour); const morningEndHourRaw = Number(state.morning_end_hour); const morningScoreRaw = Number(state.morning_score); const defaultScoreRaw = Number(state.default_score); const refreshMsRaw = Number(state.refresh_ms); const refreshMs = Number.isFinite(refreshMsRaw) && refreshMsRaw >= 60000 ? refreshMsRaw : 24 * 60 * 60 * 1000; const morningStartHour = Number.isFinite(morningStartHourRaw) ? morningStartHourRaw : 6; const morningEndHour = Number.isFinite(morningEndHourRaw) ? morningEndHourRaw : 11; const morningScore = Number.isFinite(morningScoreRaw) ? morningScoreRaw : 84; const defaultScore = Number.isFinite(defaultScoreRaw) ? defaultScoreRaw : 38; subtitleEl.textContent = subtitle || providerPrefix || 'Waiting for weather data'; const updateLiveContent = (snapshot) => { host.setLiveContent(snapshot); }; const setStatus = (label, color) => { statusEl.textContent = label; statusEl.style.color = color; }; 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: '', areas: '', 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 (rawLine.startsWith(' areas:')) { current.areas = 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 stripExecFooter = (value) => String(value || '').replace(/\n+\s*Exit code:\s*\d+\s*$/i, '').trim(); const extractExecJson = (toolResult) => { const parsedText = stripExecFooter(toolResult?.content); if (!parsedText) return null; try { return JSON.parse(parsedText); } catch { return null; } }; 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 findEntry = (entries, candidates) => { const normalizedCandidates = candidates.map((value) => normalizeText(value)).filter(Boolean); if (normalizedCandidates.length === 0) return null; const exactMatch = entries.find((entry) => normalizedCandidates.includes(normalizeText(entry.name))); if (exactMatch) return exactMatch; return entries.find((entry) => { const entryName = normalizeText(entry.name); return normalizedCandidates.some((candidate) => entryName.includes(candidate)); }) || null; }; const resolveForecastBundle = async () => { if (!forecastCommand) return null; const toolResult = await host.callTool(configuredForecastToolName || 'exec', { command: forecastCommand, max_output_chars: 200000, }); const payload = extractExecJson(toolResult); return payload && typeof payload === 'object' ? payload : null; }; const firstForecastEntry = (bundle, key, metricKey = '') => { const source = bundle && typeof bundle === 'object' ? bundle[key] : null; const forecast = source && typeof source === 'object' && Array.isArray(source.forecast) ? source.forecast : []; if (!metricKey) { return forecast.length > 0 && forecast[0] && typeof forecast[0] === 'object' ? forecast[0] : null; } return forecast.find((entry) => entry && typeof entry === 'object' && entry[metricKey] !== null && entry[metricKey] !== undefined) || null; }; const estimateUvIndex = (cloudCoverage) => { const now = new Date(); const hour = now.getHours() + now.getMinutes() / 60; const daylightPhase = Math.sin(((hour - 6) / 12) * Math.PI); if (!Number.isFinite(daylightPhase) || daylightPhase <= 0) return 0; const normalizedCloudCoverage = Number.isFinite(cloudCoverage) ? Math.min(Math.max(cloudCoverage, 0), 100) : null; const cloudFactor = normalizedCloudCoverage === null ? 1 : Math.max(0.2, 1 - normalizedCloudCoverage * 0.0065); return Math.max(0, Math.round(7 * daylightPhase * cloudFactor)); }; const computeWeatherScore = () => { const now = new Date(); const hour = now.getHours() + now.getMinutes() / 60; if (hour >= morningStartHour && hour < morningEndHour) return morningScore; return defaultScore; }; const refresh = async () => { const resolvedToolName = await resolveToolName(); if (!resolvedToolName) { const errorText = 'Missing tool_name'; setStatus('No tool', 'var(--theme-status-danger)'); updateLiveContent({ kind: 'weather', subtitle: subtitleEl.textContent || null, tool_name: null, temperature: null, temperature_unit: String(state.unit || '°F'), humidity: null, wind: null, rain: null, uv: null, status: 'No tool', error: errorText, }); return; } setStatus('Refreshing', 'var(--theme-status-muted)'); try { const [toolResult, forecastBundle] = await Promise.all([ host.callTool(resolvedToolName, {}), resolveForecastBundle(), ]); const entries = parseLiveContextEntries(extractLiveContextText(toolResult)).filter((entry) => normalizeText(entry.domain) === 'sensor'); const prefix = providerPrefix || 'OpenWeatherMap'; const temperatureEntry = findEntry(entries, [ temperatureName, `${prefix} Temperature`, ]); const humidityEntry = findEntry(entries, [ humidityName, `${prefix} Humidity`, ]); const uvSensorEntry = findEntry(entries, [ uvName, `${prefix} UV index`, ]); const temperature = Number(temperatureEntry?.state); tempEl.textContent = Number.isFinite(temperature) ? String(Math.round(temperature)) : '--'; unitEl.textContent = String(temperatureEntry?.attributes?.unit_of_measurement || state.unit || '°F'); const humidity = Number(humidityEntry?.state); humidityEl.textContent = Number.isFinite(humidity) ? `${Math.round(humidity)}%` : '--'; const nwsEntry = firstForecastEntry(forecastBundle, 'nws'); const uvEntry = firstForecastEntry(forecastBundle, 'uv', 'uv_index'); const nwsSource = forecastBundle && typeof forecastBundle === 'object' && forecastBundle.nws && typeof forecastBundle.nws === 'object' ? forecastBundle.nws : null; const uvSource = forecastBundle && typeof forecastBundle === 'object' && forecastBundle.uv && typeof forecastBundle.uv === 'object' ? forecastBundle.uv : null; const windSpeed = Number(nwsEntry?.wind_speed); const windUnit = String(nwsSource?.wind_speed_unit || 'mph'); windEl.textContent = Number.isFinite(windSpeed) ? `${Math.round(windSpeed)} ${windUnit}` : '--'; const rainChance = Number(nwsEntry?.precipitation_probability); rainEl.textContent = Number.isFinite(rainChance) ? `${Math.round(rainChance)}%` : '--'; const liveUvValue = Number(uvSensorEntry?.state); const forecastUvValue = Number(uvEntry?.uv_index); const cloudCoverage = Number.isFinite(Number(nwsEntry?.cloud_coverage)) ? Number(nwsEntry?.cloud_coverage) : Number(uvSource?.forecast?.[0]?.cloud_coverage); const estimatedUvValue = estimateUvIndex(cloudCoverage); const uvValue = Number.isFinite(liveUvValue) ? liveUvValue : (Number.isFinite(forecastUvValue) ? forecastUvValue : estimatedUvValue); const uvEstimated = !Number.isFinite(liveUvValue) && !Number.isFinite(forecastUvValue); uvEl.textContent = Number.isFinite(uvValue) ? `${uvEstimated && uvValue > 0 ? '~' : ''}${Math.round(uvValue)}` : '--'; subtitleEl.textContent = subtitle || prefix || 'Weather'; setStatus('Live', 'var(--theme-status-live)'); updateLiveContent({ kind: 'weather', subtitle: subtitleEl.textContent || null, tool_name: resolvedToolName, temperature: Number.isFinite(temperature) ? Math.round(temperature) : null, temperature_unit: unitEl.textContent || null, humidity: Number.isFinite(humidity) ? Math.round(humidity) : null, wind: windEl.textContent || null, rain: rainEl.textContent || null, uv: Number.isFinite(uvValue) ? Math.round(uvValue) : null, uv_estimated: uvEstimated, score: computeWeatherScore(), status: 'Live', }); } catch (error) { const errorText = String(error); setStatus('Unavailable', 'var(--theme-status-danger)'); tempEl.textContent = '--'; unitEl.textContent = String(state.unit || '°F'); humidityEl.textContent = '--'; windEl.textContent = '--'; rainEl.textContent = '--'; uvEl.textContent = '--'; updateLiveContent({ kind: 'weather', subtitle: subtitleEl.textContent || null, tool_name: resolvedToolName, temperature: null, temperature_unit: unitEl.textContent || null, humidity: null, wind: null, rain: null, uv: null, score: computeWeatherScore(), status: 'Unavailable', error: errorText, }); } }; host.setRefreshHandler(() => { void refresh(); }); void refresh(); __setInterval(() => { void refresh(); }, refreshMs); return { destroy() { host.setRefreshHandler(null); host.setLiveContent(null); host.clearSelection(); for (const cleanup of __cleanup.splice(0)) cleanup(); }, }; }