i was little curious and went a litte more ahead (not 100% succes yet)
store this in www/grid-schedule-card.js
import { LitElement, html, css } from "https://unpkg.com/[email protected]/lit-element.js?module";
class GridScheduleCard extends LitElement {
static get properties() {
return {
hass: { type: Object },
config: { type: Object },
_localSchedule: { type: Object }
};
}
constructor() {
super();
this._localSchedule = null;
}
setConfig(config) {
if (!config.entity) {
throw new Error("Definieer een schedule entiteit in de YAML");
}
this.config = {
slot_minutes: 60,
color_on: '#4CAF50',
color_off: '#F44336',
title: 'Verwarming Schema',
...config
};
}
shouldUpdate(changedProps) {
if (changedProps.has('hass') && this.hass && this.config.entity) {
const entityState = this.hass.states[this.config.entity];
// Als er nog geen lokale staat is, synchroniseer met HA state
if (entityState && !this._localSchedule) {
this._localSchedule = JSON.parse(JSON.stringify(entityState.attributes || {}));
}
}
return true;
}
static get styles() {
return css`
ha-card {
padding: 16px;
}
.header-row {
display: flex;
margin-left: 45px;
margin-bottom: 6px;
font-size: 10px;
color: var(--secondary-text-color, #8e8e93);
}
.header-slot {
text-align: center;
}
.day-row {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.day-label {
width: 45px;
font-weight: bold;
font-size: 12px;
}
.grid-container {
display: flex;
flex-grow: 1;
gap: 2px;
}
.slot {
flex: 1;
height: 28px;
border-radius: 3px;
cursor: pointer;
user-select: none;
transition: background-color 0.15s ease, opacity 0.1s;
}
.slot:hover {
opacity: 0.75;
}
`;
}
render() {
if (!this.hass || !this.config) return html``;
const days = ['Ma', 'Di', 'Wo', 'Do', 'Vr', 'Za', 'Zo'];
const dayKeys = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
const totalSlots = Math.floor((24 * 60) / this.config.slot_minutes);
const slotsPerHour = 60 / this.config.slot_minutes;
// Gebruik lokale state voor directe visuele feedback, fallback op HA state
const entityState = this.hass.states[this.config.entity];
const scheduleData = this._localSchedule || (entityState ? entityState.attributes : {}) || {};
return html`
<ha-card .header="${this.config.title}">
<div class="header-row">
${Array.from({ length: 24 }).map((_, hour) => html`
<div class="header-slot" style="flex: ${slotsPerHour}">${hour}h</div>
`)}
</div>
${dayKeys.map((dayKey, dayIdx) => html`
<div class="day-row">
<div class="day-label">${days[dayIdx]}</div>
<div class="grid-container">
${Array.from({ length: totalSlots }).map((_, slotIdx) => {
const isActive = this._isSlotActive(scheduleData[dayKey], slotIdx);
return html`
<div
class="slot"
style="background-color: ${isActive ? this.config.color_on : this.config.color_off}"
@click="${(e) => this._handleSlotClick(e, dayKey, slotIdx, isActive)}"
title="${this._getSlotTitle(slotIdx)}"
></div>
`;
})}
</div>
</div>
`)}
</ha-card>
`;
}
_isSlotActive(dayRanges, slotIdx) {
if (!dayRanges || !Array.isArray(dayRanges)) return false;
const slotStart = slotIdx * this.config.slot_minutes;
const slotEnd = slotStart + this.config.slot_minutes;
return dayRanges.some(range => {
const rangeStart = this._timeToMinutes(range.from);
const rangeEnd = this._timeToMinutes(range.to);
return slotStart < rangeEnd && slotEnd > rangeStart;
});
}
async _handleSlotClick(event, dayKey, slotIdx, currentState) {
event.stopPropagation();
const totalSlots = Math.floor((24 * 60) / this.config.slot_minutes);
const entityState = this.hass.states[this.config.entity];
const scheduleAttrs = this._localSchedule || (entityState ? entityState.attributes : {}) || {};
// 1. Verkrijg de huidige staat van alle slots op de geklikte dag
const slotStates = Array.from({ length: totalSlots }).map((_, idx) =>
this._isSlotActive(scheduleAttrs[dayKey], idx)
);
// 2. Toggle status van het specifieke slot
slotStates[slotIdx] = !currentState;
// 3. Omzetten naar aaneengesloten intervallen (from / to)
const newDayRanges = [];
let inRange = false;
let startMin = 0;
for (let i = 0; i < totalSlots; i++) {
if (slotStates[i] && !inRange) {
inRange = true;
startMin = i * this.config.slot_minutes;
} else if (!slotStates[i] && inRange) {
inRange = false;
newDayRanges.push({
from: this._minutesToTime(startMin),
to: this._minutesToTime(i * this.config.slot_minutes)
});
}
}
if (inRange) {
newDayRanges.push({
from: this._minutesToTime(startMin),
to: "24:00"
});
}
// 4. Samenstellen van het volledige payload object
const dayKeys = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
const payloadDays = {};
dayKeys.forEach(k => {
const ranges = (k === dayKey) ? newDayRanges : (scheduleAttrs[k] || []);
payloadDays[k] = ranges.map(r => ({
from: r.from.substring(0, 5),
to: r.to === "24:00:00" ? "24:00" : r.to.substring(0, 5)
}));
});
// Direct de lokale staat bijwerken voor directe visuele kleuraanpassing op het scherm
this._localSchedule = {
...scheduleAttrs,
...payloadDays
};
this.requestUpdate();
const cleanScheduleId = this.config.entity.replace(/^schedule\./, '');
const scheduleName = scheduleAttrs.friendly_name || (entityState ? entityState.attributes.friendly_name : cleanScheduleId);
try {
await this.hass.callWS({
type: "schedule/update",
schedule_id: cleanScheduleId,
name: scheduleName,
...payloadDays
});
console.log("[GridScheduleCard] WS update succesvol verwerkt!");
} catch (err) {
console.error("[GridScheduleCard] Fout bij opslaan in WS:", err);
}
}
_timeToMinutes(timeStr) {
if (!timeStr) return 0;
const parts = timeStr.split(':').map(Number);
return parts[0] * 60 + parts[1];
}
_minutesToTime(minutes) {
const h = String(Math.floor(minutes / 60)).padStart(2, '0');
const m = String(minutes % 60).padStart(2, '0');
return `${h}:${m}`;
}
_getSlotTitle(slotIdx) {
const start = this._minutesToTime(slotIdx * this.config.slot_minutes);
const end = this._minutesToTime((slotIdx + 1) * this.config.slot_minutes);
return `${start} - ${end}`;
}
}
customElements.define('grid-schedule-card', GridScheduleCard);
add dashboard resource
/local/grid-schedule-card.js?v=2.2
add a card on you dash:
type: custom:grid-schedule-card
entity: schedule.kerst
title: Verwarming Schema
slot_minutes: 60
color_on: '#4CAF50'
color_off: '#F44336'