Dashboard Studio 1.10.0: phone dashboards, room card, changelog
- Phone dashboards: a 4-column layout designed at phone width, with conversion both ways; on phones the top bar is hidden and comes back with a touch-and-hold on an empty spot - Room card built from Home Assistant areas (area, device and entity registries): climate, lights on with an all-lights toggle, thermostat, open doors and windows, motion, media, and device buttons - CHANGELOG.md for Home Assistant's Changelog tab, served at /api/changelog and shown under About > What's new Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -477,6 +477,73 @@
|
||||
lit: (c, { ha, cache }) => (ha.states[mediaSelected(mediaIds(c), ha, cache || {})]?.state === "playing" ? "on" : false),
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------- room
|
||||
// Everything in one Home Assistant area at a glance: climate, what's on or open, and quick controls.
|
||||
function roomEntities(c, ha) {
|
||||
const ids = [...(ha?.reg?.byArea?.[c.area] || []), ...(c.entities || [])];
|
||||
return [...new Set(ids)].filter((e) => ha?.states?.[e]);
|
||||
}
|
||||
const TOGGLES = ["light", "switch", "fan", "input_boolean", "cover", "lock"];
|
||||
|
||||
W.room = {
|
||||
label: "Room", icon: "sofa", size: [4, 4],
|
||||
fields: [
|
||||
{ k: "area", t: "area", l: "Room" },
|
||||
{ k: "name", t: "text", l: "Name", ph: "Room name" },
|
||||
{ k: "icon", t: "icon", l: "Icon", d: "sofa" },
|
||||
{ k: "temperature", t: "entity", l: "Temperature sensor", domains: ["sensor"], ph: "Automatic" },
|
||||
{ k: "humidity", t: "entity", l: "Humidity sensor", domains: ["sensor"] },
|
||||
{ k: "entities", t: "entities", l: "Extra entities (or all of them, without a room)" },
|
||||
{ k: "devices", t: "check", l: "Show devices", d: true },
|
||||
{ k: "image", t: "text", l: "Background image URL (optional)", ph: "/local/living-room.jpg" },
|
||||
],
|
||||
entities: (c, ha) => [...roomEntities(c, ha), c.temperature, c.humidity].filter(Boolean),
|
||||
render(c, { ha }) {
|
||||
const area = ha.reg?.areas?.find((a) => a.area_id === c.area);
|
||||
const ids = roomEntities(c, ha);
|
||||
if (!area && !ids.length) return `<div class="empty">${t("Choose a room or add entities in the inspector.")}</div>`;
|
||||
const S = (e) => ha.states[e], dom = (e) => domainOf(e), dc = (e) => S(e).attributes.device_class;
|
||||
const name = c.name || area?.name || t("Room");
|
||||
const pick = (explicit, fromArea, cls) => (explicit && S(explicit) ? explicit : fromArea && S(fromArea) ? fromArea : ids.find((e) => dom(e) === "sensor" && dc(e) === cls));
|
||||
const temp = pick(c.temperature, area?.temperature_entity_id, "temperature");
|
||||
const hum = pick(c.humidity, area?.humidity_entity_id, "humidity");
|
||||
|
||||
// Count and show the actual lamps: leave out light groups, whose lamps are already listed.
|
||||
const isGroup = (e) => Array.isArray(S(e).attributes.entity_id);
|
||||
const lights = ids.filter((e) => dom(e) === "light" && !isGroup(e));
|
||||
const lightsOn = lights.filter((e) => S(e).state === "on");
|
||||
const open = ids.filter((e) => dom(e) === "binary_sensor" && ["door", "window", "opening", "garage_door"].includes(dc(e)) && S(e).state === "on");
|
||||
const motion = ids.filter((e) => dom(e) === "binary_sensor" && ["motion", "occupancy", "presence"].includes(dc(e)) && S(e).state === "on");
|
||||
const playing = ids.filter((e) => dom(e) === "media_player" && S(e).state === "playing");
|
||||
const climate = ids.find((e) => dom(e) === "climate");
|
||||
|
||||
// "Living Room Lamp" → "Lamp" inside the Living Room card
|
||||
const prefix = new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*`, "i");
|
||||
const short = (e) => friendly(S(e)).replace(prefix, "") || friendly(S(e));
|
||||
const chips = [];
|
||||
if (lights.length) chips.push(`<button class="room-chip wbtn ${lightsOn.length ? "on" : ""}" data-act="svc-many" data-v="light.${lightsOn.length ? "turn_off" : "turn_on"}" data-ids="${esc((lightsOn.length ? lightsOn : lights).join(","))}" aria-label="${esc(lightsOn.length ? t("Turn off all lights in {0}", name) : t("Turn on all lights in {0}", name))}">${Icons.svg("bulb")}<span>${lightsOn.length ? t("{0} on", lightsOn.length) : t("Lights off")}</span></button>`);
|
||||
if (climate) { const cs = S(climate); chips.push(`<button class="room-chip wbtn" data-act="more" data-e="${esc(climate)}">${Icons.svg("thermometer")}<span>${esc(st(cs.state))}${cs.attributes.temperature != null ? ` · ${esc(cs.attributes.temperature)}°` : ""}</span></button>`); }
|
||||
for (const e of open) chips.push(`<button class="room-chip wbtn warn" data-act="more" data-e="${esc(e)}">${icon(S(e))}<span>${esc(short(e))} ${t("open")}</span></button>`);
|
||||
if (motion.length) chips.push(`<span class="room-chip">${Icons.svg("person")}<span>${t("Motion")}</span></span>`);
|
||||
for (const e of playing) chips.push(`<button class="room-chip wbtn" data-act="more" data-e="${esc(e)}">${Icons.svg("music")}<span>${esc(S(e).attributes.media_title || t("Playing"))}</span></button>`);
|
||||
|
||||
const devices = c.devices !== false ? ids.filter((e) => TOGGLES.includes(dom(e)) && !isGroup(e)).slice(0, 12) : [];
|
||||
const climateText = [temp && fmtState(S(temp)), hum && fmtState(S(hum))].filter(Boolean).join(" · ");
|
||||
|
||||
return `<div class="room ${c.image ? "on-image" : ""}" ${c.image ? `style="--room-img:url('${esc(c.image).replace(/'/g, "%27")}')"` : ""}>
|
||||
<div class="room-head"><span class="badge">${icon(null, c.icon || "sofa")}</span>
|
||||
<span class="tile-text"><span class="name room-name">${esc(name)}</span>${climateText ? `<span class="state num">${esc(climateText)}</span>` : ""}</span></div>
|
||||
${chips.length ? `<div class="room-chips">${chips.join("")}</div>` : ""}
|
||||
${devices.length ? `<div class="room-devices">${devices.map((e) => `<button class="room-dev wbtn ${isOn(S(e)) ? "on" : ""}" data-act="toggle" data-e="${esc(e)}" aria-pressed="${isOn(S(e))}">
|
||||
<span class="room-dev-ico">${icon(S(e))}</span><span class="room-dev-text"><span class="room-dev-name">${esc(short(e))}</span><span class="room-dev-state">${esc(fmtState(S(e)))}</span></span></button>`).join("")}</div>` : ""}
|
||||
</div>`;
|
||||
},
|
||||
lit: (c, { ha }) => {
|
||||
const ids = roomEntities(c, ha);
|
||||
return ids.some((e) => domainOf(e) === "light" && ha.states[e].state === "on") ? "lamp" : false;
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------- vacuum
|
||||
// One layout that grows with the card: compact on small cards; a robot with a battery ring,
|
||||
// stats and labelled buttons on bigger ones (see the container queries in style.css).
|
||||
|
||||
Reference in New Issue
Block a user