feat: add card examples and speed up rtc connect
This commit is contained in:
parent
04afead5af
commit
23fd806e6d
41 changed files with 3327 additions and 3 deletions
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"key": "live-weather-01545",
|
||||
"title": "Live Weather 01545",
|
||||
"notes": "Use for daily weather card in zip 01545. Pull data through the Home Assistant GetLiveContext MCP tool and match the OpenWeatherMap sensor entries.",
|
||||
"created_at": "2026-03-09T00:00:00+00:00",
|
||||
"updated_at": "2026-03-11T04:12:48.601255+00:00",
|
||||
"deprecated": true
|
||||
}
|
||||
250
examples/cards/templates/live-weather-01545/template.html
Normal file
250
examples/cards/templates/live-weather-01545/template.html
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
<div data-weather-card="01545" style="font-family: var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace); background: #ffffff; color: #111827; border: 1px solid #e5e7eb; border-radius: 16px; box-shadow: 0 8px 24px rgba(17, 24, 39, 0.12); padding: 24px; max-width: 560px;">
|
||||
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom: 10px;">
|
||||
<div>
|
||||
<h2 style="margin:0; font-size:1.15rem; font-weight:700; color:#111827;">Weather 01545</h2>
|
||||
<div style="margin-top:4px; font-size:0.84rem; color:#6b7280;">Source: Home Assistant (weather.openweathermap)</div>
|
||||
</div>
|
||||
<span data-weather-status style="font-size:0.82rem; color:#6b7280;">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:baseline; gap:10px; margin-bottom:10px;">
|
||||
<span data-weather-temp style="font-size:2.6rem; font-weight:800; line-height:1; letter-spacing:-0.03em;">--</span>
|
||||
<span data-weather-unit style="font-size:1rem; font-weight:600; color:#4b5563;">°F</span>
|
||||
</div>
|
||||
|
||||
<div data-weather-condition style="font-size:1rem; font-weight:600; color:#1f2937; margin-bottom:8px; text-transform:capitalize;">--</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:8px; font-size:0.9rem; color:#374151;">
|
||||
<div>Humidity: <strong data-weather-humidity>--</strong></div>
|
||||
<div>Wind: <strong data-weather-wind>--</strong></div>
|
||||
<div>Pressure: <strong data-weather-pressure>--</strong></div>
|
||||
<div>Updated: <strong data-weather-updated>--</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(() => {
|
||||
const script = document.currentScript;
|
||||
const root = script?.closest('[data-nanobot-card-root]');
|
||||
const state = window.__nanobotGetCardState?.(script) || {};
|
||||
if (!(root instanceof HTMLElement)) return;
|
||||
|
||||
const tempEl = root.querySelector("[data-weather-temp]");
|
||||
const unitEl = root.querySelector("[data-weather-unit]");
|
||||
const condEl = root.querySelector("[data-weather-condition]");
|
||||
const humidityEl = root.querySelector("[data-weather-humidity]");
|
||||
const windEl = root.querySelector("[data-weather-wind]");
|
||||
const pressureEl = root.querySelector("[data-weather-pressure]");
|
||||
const updatedEl = root.querySelector("[data-weather-updated]");
|
||||
const statusEl = root.querySelector("[data-weather-status]");
|
||||
|
||||
if (!(tempEl instanceof HTMLElement) || !(unitEl instanceof HTMLElement) || !(condEl instanceof HTMLElement) ||
|
||||
!(humidityEl instanceof HTMLElement) || !(windEl instanceof HTMLElement) || !(pressureEl instanceof HTMLElement) ||
|
||||
!(updatedEl instanceof HTMLElement) || !(statusEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configuredToolName = typeof state.tool_name === 'string' ? state.tool_name.trim() : '';
|
||||
const providerPrefix = typeof state.provider_prefix === 'string' ? state.provider_prefix.trim() : 'OpenWeatherMap';
|
||||
const temperatureName = typeof state.temperature_name === 'string' ? state.temperature_name.trim() : `${providerPrefix} Temperature`;
|
||||
const humidityName = typeof state.humidity_name === 'string' ? state.humidity_name.trim() : `${providerPrefix} Humidity`;
|
||||
const pressureName = typeof state.pressure_name === 'string' ? state.pressure_name.trim() : `${providerPrefix} Pressure`;
|
||||
const windName = typeof state.wind_name === 'string' ? state.wind_name.trim() : `${providerPrefix} Wind speed`;
|
||||
const conditionLabel = typeof state.condition_label === 'string' ? state.condition_label.trim() : `${providerPrefix} live context`;
|
||||
const refreshMsRaw = Number(state.refresh_ms);
|
||||
const REFRESH_MS = Number.isFinite(refreshMsRaw) && refreshMsRaw >= 60000 ? refreshMsRaw : 24 * 60 * 60 * 1000;
|
||||
|
||||
const setStatus = (label, color) => {
|
||||
statusEl.textContent = label;
|
||||
statusEl.style.color = color;
|
||||
};
|
||||
|
||||
const nowLabel = () => new Date().toLocaleString([], {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
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 (!window.__nanobotListTools) return 'mcp_home_assistant_GetLiveContext';
|
||||
try {
|
||||
const tools = await window.__nanobotListTools();
|
||||
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 refresh = async () => {
|
||||
setStatus("Refreshing", "#6b7280");
|
||||
try {
|
||||
const toolName = await resolveToolName();
|
||||
const toolResult = await window.__nanobotCallTool?.(toolName, {});
|
||||
const entries = parseLiveContextEntries(extractLiveContextText(toolResult)).filter((entry) => normalizeText(entry.domain) === 'sensor');
|
||||
const temperatureEntry = findEntry(entries, [temperatureName]);
|
||||
const humidityEntry = findEntry(entries, [humidityName]);
|
||||
const pressureEntry = findEntry(entries, [pressureName]);
|
||||
const windEntry = findEntry(entries, [windName]);
|
||||
|
||||
const tempNum = Number(temperatureEntry?.state);
|
||||
tempEl.textContent = Number.isFinite(tempNum) ? String(Math.round(tempNum)) : "--";
|
||||
unitEl.textContent = String(temperatureEntry?.attributes?.unit_of_measurement || "°F");
|
||||
condEl.textContent = conditionLabel;
|
||||
|
||||
const humidity = Number(humidityEntry?.state);
|
||||
humidityEl.textContent = Number.isFinite(humidity) ? `${Math.round(humidity)}%` : "--";
|
||||
|
||||
const windSpeed = Number(windEntry?.state);
|
||||
const windUnit = String(windEntry?.attributes?.unit_of_measurement || "mph");
|
||||
windEl.textContent = Number.isFinite(windSpeed) ? `${windSpeed} ${windUnit}` : "--";
|
||||
|
||||
const pressureNum = Number(pressureEntry?.state);
|
||||
pressureEl.textContent = Number.isFinite(pressureNum)
|
||||
? `${pressureNum} ${String(pressureEntry?.attributes?.unit_of_measurement || "")}`.trim()
|
||||
: "--";
|
||||
|
||||
updatedEl.textContent = nowLabel();
|
||||
setStatus("Live", "#047857");
|
||||
window.__nanobotSetCardLiveContent?.(script, {
|
||||
kind: 'weather',
|
||||
tool_name: toolName,
|
||||
provider_prefix: providerPrefix,
|
||||
temperature: Number.isFinite(tempNum) ? Math.round(tempNum) : null,
|
||||
temperature_unit: unitEl.textContent || null,
|
||||
humidity: Number.isFinite(humidity) ? Math.round(humidity) : null,
|
||||
wind: windEl.textContent || null,
|
||||
pressure: pressureEl.textContent || null,
|
||||
condition: condEl.textContent || null,
|
||||
updated_at: updatedEl.textContent || null,
|
||||
status: statusEl.textContent || null,
|
||||
});
|
||||
} catch (err) {
|
||||
setStatus("Unavailable", "#b91c1c");
|
||||
updatedEl.textContent = String(err);
|
||||
window.__nanobotSetCardLiveContent?.(script, {
|
||||
kind: 'weather',
|
||||
tool_name: configuredToolName || 'mcp_home_assistant_GetLiveContext',
|
||||
provider_prefix: providerPrefix,
|
||||
temperature: null,
|
||||
humidity: null,
|
||||
wind: null,
|
||||
pressure: null,
|
||||
condition: null,
|
||||
updated_at: String(err),
|
||||
status: 'Unavailable',
|
||||
error: String(err),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.__nanobotSetCardRefresh?.(script, () => {
|
||||
void refresh();
|
||||
});
|
||||
void refresh();
|
||||
window.setInterval(() => {
|
||||
void refresh();
|
||||
}, REFRESH_MS);
|
||||
})();
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue