Dashboard Studio 2.4.2: litter box only shows when the bin is full or something is wrong

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 17:15:20 +01:00
parent db6684b787
commit 0eef8593c8
8 changed files with 34 additions and 9 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog # Changelog
## 2.4.2
- **Litter box, only when needed:** the card now stays out of sight until the waste bin is full or something is wrong, and then says so (*Bin full*, or *Check the litter box*) with the alert in amber. Untick **Only show when the bin is full or something is wrong** to see it all the time. Existing litter box cards get the new behaviour.
- Alerts on a litter box also count when a sensor says what's wrong (for example *full* or *fault*), not only *on*.
## 2.4.1 ## 2.4.1
- **Litter box, more compact:** the name gets the top line, the status sits beside **Clean** and the auto-clean switch, and today's figures are one short line (3 visits · 7 min · 4.6 kg), so nothing is cut off. One row high it all fits on a single line; tall cards keep the ring. On narrow cards the auto-clean switch makes way for **Clean**. - **Litter box, more compact:** the name gets the top line, the status sits beside **Clean** and the auto-clean switch, and today's figures are one short line (3 visits · 7 min · 4.6 kg), so nothing is cut off. One row high it all fits on a single line; tall cards keep the ring. On narrow cards the auto-clean switch makes way for **Clean**.
+3 -1
View File
@@ -236,7 +236,9 @@ By default the card only shows while the appliance is running or has just finish
#### Litter box #### Litter box
Set **Appliance** to **Litter box** for a self-cleaning litter box. The card is compact: it says what the box is doing (*Ready*, *Cleaning…*, *Emptying*, *Levelling*, *Sanitizing* or *Sleeping*) with **Clean** and the **Auto** clean switch beside it, and today's visits, the time in the box and your cat's weight in one line below. It fits in one row high, where it all goes on one line; tall cards add a ring on top. On narrow cards only **Clean** stays. The card never hides itself. Set **Appliance** to **Litter box** for a self-cleaning litter box. The card is compact: it says what the box is doing (*Ready*, *Cleaning…*, *Emptying*, *Levelling*, *Sanitizing* or *Sleeping*) with **Clean** and the **Auto** clean switch beside it, and today's visits, the time in the box and your cat's weight in one line below. It fits in one row high, where it all goes on one line; tall cards add a ring on top. On narrow cards only **Clean** stays.
By default the card only shows when the waste bin is full or something is wrong: it then says *Bin full* or *Check the litter box*, with the alert in amber. For this it needs an alert (a sensor for a full bin or a fault, under **Alerts**) or a status that reports the fault. Untick **Only show when the bin is full or something is wrong** to see the card all the time. In the builder you always see it.
**Tuya** (for example the ZEDAR K1200): choose the box's *Status* sensor and the card fills in the rest of the same device: *Excretion times (day)* as the visits, *Excretion duration* as the time in the box, *Cat weight*, the *Manual clean* button and the *Auto clean* switch. Sensors for a full waste bin or a fault become alerts, shown in amber. Not every box offers every sensor, and names can differ; you can always pick the entities by hand. Other brands work too, as long as there is a status sensor. **Tuya** (for example the ZEDAR K1200): choose the box's *Status* sensor and the card fills in the rest of the same device: *Excretion times (day)* as the visits, *Excretion duration* as the time in the box, *Cat weight*, the *Manual clean* button and the *Auto clean* switch. Sensors for a full waste bin or a fault become alerts, shown in amber. Not every box offers every sensor, and names can differ; you can always pick the entities by hand. Other brands work too, as long as there is a status sensor.
+1 -1
View File
@@ -1,5 +1,5 @@
name: Dashboard Studio name: Dashboard Studio
version: "2.4.1" version: "2.4.2"
slug: dashboard_studio slug: dashboard_studio
description: Drag-and-drop dashboard builder with a free-form grid, live entity widgets and themes. description: Drag-and-drop dashboard builder with a free-form grid, live entity widgets and themes.
url: https://git.melvanveen.nl/veenm/ha-dashboard-studio url: https://git.melvanveen.nl/veenm/ha-dashboard-studio
+1
View File
@@ -66,6 +66,7 @@ class MockHA:
entity("button.zedar_k1200_manual_clean", "unknown", friendly_name="ZEDAR K1200 Manual clean"), entity("button.zedar_k1200_manual_clean", "unknown", friendly_name="ZEDAR K1200 Manual clean"),
entity("button.zedar_k1200_factory_reset", "unknown", friendly_name="ZEDAR K1200 Factory reset"), entity("button.zedar_k1200_factory_reset", "unknown", friendly_name="ZEDAR K1200 Factory reset"),
entity("switch.zedar_k1200_auto_clean", "on", friendly_name="ZEDAR K1200 Auto clean"), entity("switch.zedar_k1200_auto_clean", "on", friendly_name="ZEDAR K1200 Auto clean"),
entity("binary_sensor.zedar_k1200_waste_bin_full", "off", friendly_name="ZEDAR K1200 Waste bin full", device_class="problem"),
entity("switch.coffee_machine", "off", friendly_name="Coffee Machine"), entity("switch.coffee_machine", "off", friendly_name="Coffee Machine"),
entity("fan.office", "off", friendly_name="Office Fan"), entity("fan.office", "off", friendly_name="Office Fan"),
entity("sensor.living_room_temperature", "21.4", friendly_name="Living Room Temperature", unit_of_measurement="°C", device_class="temperature"), entity("sensor.living_room_temperature", "21.4", friendly_name="Living Room Temperature", unit_of_measurement="°C", device_class="temperature"),
+2
View File
@@ -242,6 +242,8 @@
// Media cards from before 1.5 kept one player in cfg.entity; move it into the list so it can be edited. // Media cards from before 1.5 kept one player in cfg.entity; move it into the list so it can be edited.
const fix = (w) => { const fix = (w) => {
if (w.type === "media" && w.cfg.entity) { w.cfg.entities = [...new Set([w.cfg.entity, ...(w.cfg.entities || [])])]; delete w.cfg.entity; } if (w.type === "media" && w.cfg.entity) { w.cfg.entities = [...new Set([w.cfg.entity, ...(w.cfg.entities || [])])]; delete w.cfg.entity; }
// Litter boxes from 2.4 always showed; from 2.4.2 they only show when the bin is full or something is wrong.
if (w.type === "appliance" && w.cfg.kind === "litterbox" && w.cfg.onlyAttention == null) w.cfg.onlyAttention = true;
(w.cfg.children || []).forEach(fix); (w.cfg.children || []).forEach(fix);
}; };
d.tabs.forEach((t) => t.widgets.forEach(fix)); d.tabs.forEach((t) => t.widgets.forEach(fix));
+1 -1
View File
@@ -302,7 +302,7 @@
"Clean button": "Schoonmaakknop", "Auto clean switch": "Schakelaar automatisch schoonmaken", "Choose the litter box’s status entity in the inspector.": "Kies de status-entiteit van de kattenbak in de inspector.", "Clean button": "Schoonmaakknop", "Auto clean switch": "Schakelaar automatisch schoonmaken", "Choose the litter box’s status entity in the inspector.": "Kies de status-entiteit van de kattenbak in de inspector.",
"Emptying": "Aan het legen", "Levelling": "Aan het egaliseren", "Sanitizing": "Aan het ontsmetten", "Sleeping": "Slaapt", "Ready": "Klaar voor gebruik", "Emptying": "Aan het legen", "Levelling": "Aan het egaliseren", "Sanitizing": "Aan het ontsmetten", "Sleeping": "Slaapt", "Ready": "Klaar voor gebruik",
"Bin full": "Afvalbak vol", "Check the litter box": "Kijk even bij de kattenbak", "Cat inside": "Kat in de bak", "Needs attention": "Heeft aandacht nodig", "Bin full": "Afvalbak vol", "Check the litter box": "Kijk even bij de kattenbak", "Cat inside": "Kat in de bak", "Needs attention": "Heeft aandacht nodig",
"visits today": "bezoekjes vandaag", "visits": "bezoekjes", "visit": "bezoekje", "Clean": "Schoonmaken", "Auto": "Auto", "in the box today": "in de bak vandaag", "cat’s weight": "gewicht kat", "Clean now": "Nu schoonmaken", "Auto clean": "Automatisch schoonmaken", "visits today": "bezoekjes vandaag", "visits": "bezoekjes", "visit": "bezoekje", "Only show when the bin is full or something is wrong": "Alleen tonen als de afvalbak vol is of er iets mis is", "Clean": "Schoonmaken", "Auto": "Auto", "in the box today": "in de bak vandaag", "cat’s weight": "gewicht kat", "Clean now": "Nu schoonmaken", "Auto clean": "Automatisch schoonmaken",
"Starts later": "Start later", "Action needed": "Actie nodig", "Error": "Fout", "Stopping": "Stoppen", "Door locked": "Deur vergrendeld", "Starts later": "Start later", "Action needed": "Actie nodig", "Error": "Fout", "Stopping": "Stoppen", "Door locked": "Deur vergrendeld",
"Ready at {0}": "Klaar om {0}", "Check the appliance": "Kijk even bij het apparaat", "Something went wrong": "Er ging iets mis", "Stop": "Stop", "Ready at {0}": "Klaar om {0}", "Check the appliance": "Kijk even bij het apparaat", "Something went wrong": "Er ging iets mis", "Stop": "Stop",
"Hide this card when none of its cards are showing": "Verberg deze kaart als geen van de kaarten erin zichtbaar is", "Hide this card when none of its cards are showing": "Verberg deze kaart als geen van de kaarten erin zichtbaar is",
+1 -1
View File
@@ -1610,7 +1610,7 @@ body.in-haven #btn-new { display: none; }
.lb-state { font-size: var(--c-fs-mid); } .lb-state { font-size: var(--c-fs-mid); }
.lb-figs { line-height: 22px; height: 22px; overflow: hidden; } /* figures that don't fit wrap out of sight, whole */ .lb-figs { line-height: 22px; height: 22px; overflow: hidden; } /* figures that don't fit wrap out of sight, whole */
} }
@container (max-height: 110px) and (max-width: 360px) { @container (max-height: 110px) and (max-width: 440px) {
.lb-figs { display: none; } .lb-figs { display: none; }
.lb-body { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: "ico state acts"; } .lb-body { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: "ico state acts"; }
} }
+20 -5
View File
@@ -1059,16 +1059,26 @@
// A litter box isn't a machine you start and empty: it waits, cleans itself after a visit, and now and then // A litter box isn't a machine you start and empty: it waits, cleans itself after a visit, and now and then
// needs its bin emptied. Tuya reports clean / empty / level / sleep / standby / uv; other boxes use similar words. // needs its bin emptied. Tuya reports clean / empty / level / sleep / standby / uv; other boxes use similar words.
// An alert is on when it says so ("on", "true") or names the trouble ("full", "fault"), but not "not_full".
const litterAlert = (v) => {
const s = String(v ?? "").toLowerCase();
return ALERT_ON.has(s) || (/full|vol|fault|error|fout|alarm|problem/.test(s) && !/^(not|no|niet)[_ ]/.test(s));
};
function litterPhase(c, ha) { function litterPhase(c, ha) {
const s = String(ha.states[c.status]?.state || "").toLowerCase(); const s = String(ha.states[c.status]?.state || "").toLowerCase();
const alerts = (c.alerts || []).filter((e) => ALERT_ON.has(String(ha.states[e]?.state).toLowerCase())); const alerts = (c.alerts || []).filter((e) => litterAlert(ha.states[e]?.state));
const busy = { clean: t("Cleaning…"), cleaning: t("Cleaning…"), empty: t("Emptying"), emptying: t("Emptying"), level: t("Levelling"), leveling: t("Levelling"), uv: t("Sanitizing"), sanitizing: t("Sanitizing") }[s]; const busy = { clean: t("Cleaning…"), cleaning: t("Cleaning…"), empty: t("Emptying"), emptying: t("Emptying"), level: t("Levelling"), leveling: t("Levelling"), uv: t("Sanitizing"), sanitizing: t("Sanitizing") }[s];
if (!c.status || !ha.states[c.status] || ["unavailable", "unknown"].includes(s)) return { phase: "off", word: t("Unavailable"), alerts }; if (!c.status || !ha.states[c.status] || ["unavailable", "unknown"].includes(s)) return { phase: "off", word: t("Unavailable"), alerts };
if (/fault|error|fout|full|vol/.test(s)) return { phase: "attention", word: s.includes("full") || s.includes("vol") ? t("Bin full") : t("Check the litter box"), alerts }; if (/fault|error|fout|full|vol/.test(s)) return { phase: "attention", word: s.includes("full") || s.includes("vol") ? t("Bin full") : t("Check the litter box"), alerts };
if (busy) return { phase: "cleaning", word: busy, alerts }; if (busy) return { phase: "cleaning", word: busy, alerts };
if (/sleep|slaap/.test(s)) return { phase: "sleep", word: t("Sleeping"), alerts }; if (/sleep|slaap/.test(s)) return { phase: "sleep", word: t("Sleeping"), alerts };
if (/cat|occupied|in_use|using/.test(s)) return { phase: "visit", word: t("Cat inside"), alerts }; if (/cat|occupied|in_use|using/.test(s)) return { phase: "visit", word: t("Cat inside"), alerts };
if (alerts.length) return { phase: "attention", word: t("Needs attention"), alerts }; // name the trouble when an alert says what it is (a full bin), else a general word
if (alerts.length) {
const full = alerts.some((e) => /full|vol|bag|bin|waste|afval/.test(`${e} ${friendly(ha.states[e])}`.toLowerCase()));
return { phase: "attention", word: full ? t("Bin full") : t("Check the litter box"), alerts };
}
return { phase: "ready", word: /^(standby|idle|ready|on|off|none|)$/.test(s) ? t("Ready") : st(s), alerts }; return { phase: "ready", word: /^(standby|idle|ready|on|off|none|)$/.test(s) ? t("Ready") : st(s), alerts };
} }
@@ -1137,6 +1147,7 @@
{ k: "autoClean", t: "entity", l: "Auto clean switch", domains: ["switch", "input_boolean"], when: litter }, { k: "autoClean", t: "entity", l: "Auto clean switch", domains: ["switch", "input_boolean"], when: litter },
{ k: "alerts", t: "entities", l: "Alerts, e.g. salt or rinse aid low, or a full waste bin", domains: ["sensor", "binary_sensor", "event"] }, { k: "alerts", t: "entities", l: "Alerts, e.g. salt or rinse aid low, or a full waste bin", domains: ["sensor", "binary_sensor", "event"] },
{ k: "hideWhenOff", t: "check", l: "Only show while running or finished", d: true, when: notLitter }, { k: "hideWhenOff", t: "check", l: "Only show while running or finished", d: true, when: notLitter },
{ k: "onlyAttention", t: "check", l: "Only show when the bin is full or something is wrong", d: true, when: litter },
], ],
// Picking the status fills in the other entities of the same device (Home Connect names them predictably). // Picking the status fills in the other entities of the same device (Home Connect names them predictably).
autofill: { autofill: {
@@ -1151,7 +1162,7 @@
return { return {
kind: "litterbox", visits: find("sensor", /excretion_times|times_day|visits/), timeToday: find("sensor", /excretion_(time_day|duration)|duration/), kind: "litterbox", visits: find("sensor", /excretion_times|times_day|visits/), timeToday: find("sensor", /excretion_(time_day|duration)|duration/),
catWeight: find("sensor", /cat_weight|weight/), cleanBtn: find("button", /manual_clean|clean/), autoClean: find("switch", /auto_clean/), catWeight: find("sensor", /cat_weight|weight/), cleanBtn: find("button", /manual_clean|clean/), autoClean: find("switch", /auto_clean/),
alerts: sibs.filter((e) => ["binary_sensor", "sensor"].includes(domainOf(e)) && /full|fault|error|bin|waste|litter_level/.test(e) && e !== eid), alerts: sibs.filter((e) => ["binary_sensor", "sensor"].includes(domainOf(e)) && /full|fault|error|bin|bag|waste|litter_level/.test(e) && e !== eid),
}; };
} }
return { return {
@@ -1167,8 +1178,12 @@
entities: (c) => (c.kind === "litterbox" ? [c.status, c.visits, c.timeToday, c.catWeight, c.cleanBtn, c.autoClean, ...(c.alerts || [])] entities: (c) => (c.kind === "litterbox" ? [c.status, c.visits, c.timeToday, c.catWeight, c.cleanBtn, c.autoClean, ...(c.alerts || [])]
: [c.status, c.power, c.remaining, c.progress, c.program, c.door, c.energy, c.pauseBtn, c.resumeBtn, c.stopBtn, c.powerSwitch, ...(c.alerts || [])]).filter(Boolean), : [c.status, c.power, c.remaining, c.progress, c.program, c.door, c.energy, c.pauseBtn, c.resumeBtn, c.stopBtn, c.powerSwitch, ...(c.alerts || [])]).filter(Boolean),
// a litter box is always there to look at; it never hides itself // a litter box is always there to look at; it never hides itself
autoHide: (c, { ha }) => c.kind !== "litterbox" && !!c.hideWhenOff && !!(c.status || c.power || c.remaining) && appliancePhase(c, ha) === "off" // A litter box only shows when its bin is full or something is wrong (unless that option is off);
&& !(c.alerts || []).some((e) => ALERT_ON.has(String(ha.states[e]?.state).toLowerCase())), // other appliances while running or finished, or while an alert is on.
autoHide: (c, { ha }) => (c.kind === "litterbox"
? c.onlyAttention !== false && !!c.status && litterPhase(c, ha).phase !== "attention"
: !!c.hideWhenOff && !!(c.status || c.power || c.remaining) && appliancePhase(c, ha) === "off"
&& !(c.alerts || []).some((e) => ALERT_ON.has(String(ha.states[e]?.state).toLowerCase()))),
// Two rows high: a progress ring around the appliance on the left, the story on the right. // Two rows high: a progress ring around the appliance on the left, the story on the right.
// Taller cards stack the ring on top (see the container queries in style.css). // Taller cards stack the ring on top (see the container queries in style.css).
render(c, { ha, cache }) { render(c, { ha, cache }) {