Hi, everybody.
I’ve acquired an Aqara C1 Pet Feeder and connected it via Zigbee2MQTT, but noticed that there was a limit in the amount of automatic feeding schedules to a maximum of 4 entries. I learned this is because of the inherent 255-character limit of the entity that governs the schedule.
So, I’ve decided to create my own script, with an automation and a lovelace card that would automate a feeding schedule for my cat with up to 20 entries (I thought 20 was a decent number).
Maybe, someone out there could find it useful for their own dashboard. I’ve been using it for quite some time and it seems to work fine. But do let me know if you have any trouble.
Oh, by the way. Script UI elements are in Spanish but you can easily change these to your liking. All the code, instructions, etc. are in English so it should be fairly simple to translate.
Prerequisites
- Home Assistant OS (haven’t tested it elsewhere)
- Aqara Pet Feeder connected via zigbee2mqtt
- Mosquitto broker (already configured)
- HACS installed (optional, but recommended)
Installation Steps
1. Add Input_Text helpers
These helpers will save the schedule information. Adding them this way makes sure of data persistence.
Add three text input helpers in Settings > Devices & Services > Helpers > Add new Helper
Type: Text
Name: Pet Feeeder Schedules 1, 2 and 3.
Result: input_text_pet_feeder_schedules_1, 2 and 3
2. Upload JS Script
Create a file called pet-feeder-card.js in the config\www folder. You can use Studio Code Server or File Editor add-ons for this purpose. Then add this code and save it.
// Pet Feeder Card for Aqara Pet Feeder (via Zigbee)
// (CC) 2025 - Fernando Santos
class PetFeederCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.schedules = [];
this.editingIndex = -1;
this._initialized = false;
this._lastSaveTime = null;
}
setConfig(config) {
if (!config.entity) {
throw new Error('Please define an entity');
}
this.config = config;
this.entity = config.entity;
this.scheduleEntity1 = 'input_text.pet_feeder_schedules_1';
this.scheduleEntity2 = 'input_text.pet_feeder_schedules_2';
this.scheduleEntity3 = 'input_text.pet_feeder_schedules_3';
// Load initial schedules
if (this._hass) {
this.loadInitialSchedules();
}
}
set hass(hass) {
const oldHass = this._hass;
this._hass = hass;
// Only load schedules on first initialization
if (!this._initialized) {
this.loadInitialSchedules();
this._initialized = true;
// Set up periodic sync every 30 seconds to catch changes from other devices
setInterval(() => {
this.syncSchedules();
}, 30000);
}
}
syncSchedules() {
// Don't sync if we recently saved (within last 5 seconds)
if (this._lastSaveTime && (Date.now() - this._lastSaveTime) < 5000) {
return;
}
// Decompress function
const decompressSchedule = (sched) => {
if (sched.time && sched.portions && sched.days) {
return sched;
} else if (sched.t && sched.p && sched.d) {
const dayMap = {m:'mon',t:'tue',w:'wed',h:'thu',f:'fri',s:'sat',u:'sun'};
return {
time: sched.t,
portions: sched.p,
days: sched.d.split('').map(letter => dayMap[letter])
};
}
return sched;
};
// Reload schedules from entities to sync with other devices
let loadedSchedules = [];
[this.scheduleEntity1, this.scheduleEntity2, this.scheduleEntity3].forEach(entity => {
const scheduleState = this._hass.states[entity];
if (scheduleState && scheduleState.state && scheduleState.state !== 'unknown' && scheduleState.state !== '' && scheduleState.state !== '[]') {
try {
const parsed = JSON.parse(scheduleState.state);
if (Array.isArray(parsed) && parsed.length > 0) {
const decompressed = parsed.map(decompressSchedule);
loadedSchedules = loadedSchedules.concat(decompressed);
}
} catch (e) {
// Ignore parse errors during sync
}
}
});
// Only update if loaded data has MORE schedules or is significantly different
if (loadedSchedules.length >= this.schedules.length) {
const currentJson = JSON.stringify(this.schedules.sort((a,b) => a.time.localeCompare(b.time)));
const loadedJson = JSON.stringify(loadedSchedules.sort((a,b) => a.time.localeCompare(b.time)));
if (currentJson !== loadedJson) {
this.schedules = loadedSchedules;
this.updateScheduleList();
this.updateHeader();
}
}
}
loadInitialSchedules() {
let loadedSchedules = [];
// Decompress function: convert single letters back to full day names
const decompressSchedule = (sched) => {
// Handle both old format and new compressed format
if (sched.time && sched.portions && sched.days) {
// Old uncompressed format
return sched;
} else if (sched.t && sched.p && sched.d) {
// New compressed format
const dayMap = {m:'mon',t:'tue',w:'wed',h:'thu',f:'fri',s:'sat',u:'sun'};
return {
time: sched.t,
portions: sched.p,
days: sched.d.split('').map(letter => dayMap[letter])
};
}
return sched;
};
// Load from all three entities
[this.scheduleEntity1, this.scheduleEntity2, this.scheduleEntity3].forEach(entity => {
const scheduleState = this._hass.states[entity];
if (scheduleState && scheduleState.state && scheduleState.state !== 'unknown' && scheduleState.state !== '' && scheduleState.state !== '[]') {
try {
const parsed = JSON.parse(scheduleState.state);
if (Array.isArray(parsed) && parsed.length > 0) {
const decompressed = parsed.map(decompressSchedule);
loadedSchedules = loadedSchedules.concat(decompressed);
}
} catch (e) {
console.error(`Failed to parse schedules from ${entity}:`, e);
}
}
});
// Only update schedules if we actually loaded something
if (loadedSchedules.length > 0) {
this.schedules = loadedSchedules;
}
this.render();
}
async saveSchedules() {
// Record when we're saving to prevent sync from overwriting
this._lastSaveTime = Date.now();
console.log('=============================');
console.log('PET FEEDER: SAVING SCHEDULES');
console.log('=============================');
console.log('Total schedules to save:', this.schedules.length);
// CRITICAL: Never save if we're trying to save empty array and entities have data
if (this.schedules.length === 0) {
let hasExistingSchedules = false;
[this.scheduleEntity1, this.scheduleEntity2, this.scheduleEntity3].forEach(entity => {
const state = this._hass.states[entity];
if (state && state.state && state.state !== 'unknown' && state.state !== '' && state.state !== '[]') {
try {
const parsed = JSON.parse(state.state);
if (Array.isArray(parsed) && parsed.length > 0) {
hasExistingSchedules = true;
}
} catch (e) {
// Ignore parse errors
}
}
});
if (hasExistingSchedules) {
this.loadInitialSchedules();
return;
}
}
// Compress schedules to save space: convert days to single letters
// mon->m, tue->t, wed->w, thu->h, fri->f, sat->s, sun->u
const compressSchedule = (sched) => {
const dayMap = {mon:'m',tue:'t',wed:'w',thu:'h',fri:'f',sat:'s',sun:'u'};
return {
t: sched.time,
p: sched.portions,
d: sched.days.map(day => dayMap[day]).join('')
};
};
const compressedSchedules = this.schedules.map(compressSchedule);
// With compression, each schedule is ~35 chars, so we can fit 7 per entity
const chunk1 = compressedSchedules.slice(0, 7);
const chunk2 = compressedSchedules.slice(7, 14);
const chunk3 = compressedSchedules.slice(14, 20);
try {
await this._hass.callService('input_text', 'set_value', {
entity_id: this.scheduleEntity1,
value: JSON.stringify(chunk1)
});
await this._hass.callService('input_text', 'set_value', {
entity_id: this.scheduleEntity2,
value: JSON.stringify(chunk2)
});
await this._hass.callService('input_text', 'set_value', {
entity_id: this.scheduleEntity3,
value: JSON.stringify(chunk3)
});
this.showMessage('Horario guardado correctamente.', 'success');
} catch (e) {
console.error('Failed to save schedules:', e);
this.showMessage('Error al guardar el horario.', 'error');
}
}
showMessage(message, type = 'info') {
const event = new Event('hass-notification', {
bubbles: true,
composed: true,
});
event.detail = { message };
this.dispatchEvent(event);
}
async manualFeed() {
const portions = this.shadowRoot.getElementById('manual-portions').value;
try {
// ZNCWWSQ01LM requires setting serving_size first, then triggering feed
// First, set the serving size
await this._hass.callService('mqtt', 'publish', {
topic: `zigbee2mqtt/${this.config.device_name || 'Aqara Pet Feeder'}/set`,
payload: JSON.stringify({ serving_size: parseInt(portions) })
});
// Wait a moment for it to register
await new Promise(resolve => setTimeout(resolve, 500));
// Then trigger the feed
await this._hass.callService('mqtt', 'publish', {
topic: `zigbee2mqtt/${this.config.device_name || 'Aqara Pet Feeder'}/set`,
payload: JSON.stringify({ feed: 'START' })
});
this.showMessage(`Erogando ${portions} porci${portions > 1 ? 'ones.' : 'ón.'}`, 'success');
} catch (e) {
console.error('Failed to feed:', e);
this.showMessage('Error al activar la alimentación.', 'error');
}
}
isDuplicate(newSchedule, excludeIndex = -1) {
return this.schedules.some((schedule, index) => {
if (index === excludeIndex) return false;
// Only compare time and days, NOT portions
return schedule.time === newSchedule.time &&
JSON.stringify(schedule.days.sort()) === JSON.stringify(newSchedule.days.sort());
});
}
addSchedule() {
const time = this.shadowRoot.getElementById('schedule-time').value;
const portions = parseInt(this.shadowRoot.getElementById('schedule-portions').value);
const days = Array.from(this.shadowRoot.querySelectorAll('.day-checkbox:checked')).map(cb => cb.value);
if (!time) {
this.showMessage('Por favor selecciona una hora.', 'error');
return;
}
if (days.length === 0) {
this.showMessage('Por favor selecciona al menos un día.', 'error');
return;
}
if (this.schedules.length >= 20 && this.editingIndex === -1) {
this.showMessage('Máximo 20 horarios permitidos.', 'error');
return;
}
const newSchedule = { time, portions, days: days.sort() };
if (this.editingIndex >= 0) {
if (this.isDuplicate(newSchedule, this.editingIndex)) {
this.showMessage('Este horario ya existe.', 'error');
return;
}
this.schedules[this.editingIndex] = newSchedule;
this.editingIndex = -1;
this.showMessage('Horario actualizado.', 'success');
} else {
if (this.isDuplicate(newSchedule)) {
this.showMessage('Este horario ya existe.', 'error');
return;
}
this.schedules.push(newSchedule);
this.showMessage('Horario agregado.', 'success');
}
this.saveSchedules();
this.clearForm();
this.updateScheduleList();
this.updateHeader();
}
editSchedule(index) {
const schedule = this.schedules[index];
this.editingIndex = index;
const timeInput = this.shadowRoot.getElementById('schedule-time');
const portionsInput = this.shadowRoot.getElementById('schedule-portions');
if (timeInput) timeInput.value = schedule.time;
if (portionsInput) portionsInput.value = schedule.portions;
this.shadowRoot.querySelectorAll('.day-checkbox').forEach(cb => {
const shouldBeChecked = schedule.days.includes(cb.value);
cb.checked = shouldBeChecked;
const label = cb.closest('.day-checkbox-label');
if (shouldBeChecked) {
label.classList.add('checked');
} else {
label.classList.remove('checked');
}
});
const addBtn = this.shadowRoot.getElementById('add-schedule-btn');
const cancelBtn = this.shadowRoot.getElementById('cancel-edit-btn');
if (addBtn) addBtn.textContent = 'Actualizar Horario';
if (cancelBtn) cancelBtn.style.display = 'inline-block';
}
cancelEdit() {
this.editingIndex = -1;
this.clearForm();
const addBtn = this.shadowRoot.getElementById('add-schedule-btn');
const cancelBtn = this.shadowRoot.getElementById('cancel-edit-btn');
if (addBtn) addBtn.textContent = 'Agregar Horario';
if (cancelBtn) cancelBtn.style.display = 'none';
}
deleteSchedule(index) {
this.schedules.splice(index, 1);
this.saveSchedules();
this.showMessage('Horario eliminado.', 'success');
this.updateScheduleList();
this.updateHeader();
}
clearAllSchedules() {
if (confirm('¿Estás seguro de que quieres limpiar todos los horarios?')) {
this.schedules = [];
try {
// Clear all three entities directly
this._hass.callService('input_text', 'set_value', {
entity_id: this.scheduleEntity1,
value: '[]'
});
this._hass.callService('input_text', 'set_value', {
entity_id: this.scheduleEntity2,
value: '[]'
});
this._hass.callService('input_text', 'set_value', {
entity_id: this.scheduleEntity3,
value: '[]'
});
this.showMessage('Todos los horarios eliminados.', 'success');
this.updateScheduleList();
this.updateHeader();
} catch (e) {
console.error('Error clearing schedules:', e);
this.showMessage('Error al limpiar horarios.', 'error');
}
}
}
clearForm() {
if (this.shadowRoot.getElementById('schedule-time')) {
this.shadowRoot.getElementById('schedule-time').value = '';
}
if (this.shadowRoot.getElementById('schedule-portions')) {
this.shadowRoot.getElementById('schedule-portions').value = '1';
}
this.shadowRoot.querySelectorAll('.day-checkbox').forEach(cb => {
cb.checked = false;
cb.closest('.day-checkbox-label').classList.remove('checked');
});
}
formatDays(days) {
const dayNames = { mon: 'Lun', tue: 'Mar', wed: 'Mié', thu: 'Jue', fri: 'Vie', sat: 'Sáb', sun: 'Dom' };
if (days.length === 7) return 'Todos los Días';
if (days.length === 5 && !days.includes('sat') && !days.includes('sun')) return 'Entre Semana';
if (days.length === 2 && days.includes('sat') && days.includes('sun')) return 'Fines de Semana';
return days.map(d => dayNames[d]).join(', ');
}
updateHeader() {
const countBadge = this.shadowRoot.querySelector('.count-badge');
if (countBadge) {
countBadge.textContent = `${this.schedules.length}/20`;
}
}
updateScheduleList() {
const scheduleListContainer = this.shadowRoot.querySelector('#schedule-list-container');
if (!scheduleListContainer) return;
const sortedSchedules = [...this.schedules].sort((a, b) => {
if (a.time !== b.time) return a.time.localeCompare(b.time);
return a.days.join(',').localeCompare(b.days.join(','));
});
scheduleListContainer.innerHTML = `
<div class="section-title">
Horarios
${this.schedules.length > 0 ? `
<button class="btn-danger btn-small" style="float: right;" onclick="this.getRootNode().host.clearAllSchedules()">
Limpiar Todo
</button>
` : ''}
</div>
${sortedSchedules.length === 0 ? `
<div class="empty-state">
No hay horarios configurados. Agrega tu primer horario de alimentación arriba.
</div>
` : `
<div class="schedule-list">
${sortedSchedules.map((schedule) => {
// Find the ACTUAL index in the unsorted array
const actualIndex = this.schedules.findIndex(s =>
s === schedule
);
return `
<div class="schedule-item">
<div class="schedule-info">
<div class="schedule-time">${schedule.time}</div>
<div class="schedule-details">
${schedule.portions} porción${schedule.portions > 1 ? 'es' : ''} • ${this.formatDays(schedule.days)}
</div>
</div>
<div class="schedule-actions">
<button class="btn-secondary btn-small" onclick="this.getRootNode().host.editSchedule(${actualIndex})">
Editar
</button>
<button class="btn-danger btn-small" onclick="this.getRootNode().host.deleteSchedule(${actualIndex})">
Eliminar
</button>
</div>
</div>
`;
}).join('')}
</div>
`}
`;
}
render() {
if (!this._hass) return;
const sortedSchedules = [...this.schedules].sort((a, b) => {
if (a.time !== b.time) return a.time.localeCompare(b.time);
return a.days.join(',').localeCompare(b.days.join(','));
});
this.shadowRoot.innerHTML = `
<style>
ha-card {
padding: 16px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
gap: 12px;
}
.logo {
width: 83px;
height: 24px;
object-fit: contain;
}
.section {
margin-bottom: 24px;
padding: 16px;
background: var(--primary-background-color);
border-radius: 8px;
}
.section-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 12px;
color: var(--primary-text-color);
}
.form-row {
display: flex;
gap: 12px;
margin-bottom: 12px;
align-items: center;
flex-wrap: wrap;
}
.form-group {
display: flex;
flex-direction: column;
flex: 1;
min-width: 120px;
}
label {
font-size: 14px;
margin-bottom: 4px;
color: var(--secondary-text-color);
}
input[type="time"],
input[type="number"],
select {
padding: 8px;
border: 1px solid var(--divider-color);
border-radius: 4px;
background: var(--card-background-color);
color: var(--primary-text-color);
font-size: 14px;
}
.days-selector {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.day-checkbox-label {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: 1px solid var(--divider-color);
border-radius: 16px;
cursor: pointer;
user-select: none;
transition: all 0.2s;
}
.day-checkbox-label:hover {
background: var(--secondary-background-color);
}
.day-checkbox-label.checked {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.day-checkbox {
display: none;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-secondary {
background: var(--secondary-background-color);
color: var(--primary-text-color);
}
.btn-secondary:hover {
background: var(--divider-color);
}
.btn-danger {
background: var(--error-color);
color: white;
}
.btn-danger:hover {
opacity: 0.9;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.schedule-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.schedule-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: var(--card-background-color);
border: 1px solid var(--divider-color);
border-radius: 8px;
}
.schedule-info {
flex: 1;
}
.schedule-time {
font-size: 18px;
font-weight: 500;
margin-bottom: 4px;
}
.schedule-details {
font-size: 11px;
color: var(--secondary-text-color);
}
.schedule-actions {
display: flex;
gap: 8px;
}
.manual-feed {
display: flex;
gap: 12px;
align-items: flex-end;
}
.empty-state {
text-align: center;
padding: 24px;
color: var(--secondary-text-color);
}
.button-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
#cancel-edit-btn {
display: none;
}
.count-badge {
display: inline-block;
background: var(--primary-color);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
margin-left: 8px;
}
</style>
<ha-card>
<div class="header">
<div class="title">
<img src="https://www.aqara.com/wp-content/uploads/2023/05/aqara-logo.png" class="logo" alt="Aqara">
Pet Feeder
<span class="count-badge">${this.schedules.length}/20</span>
</div>
</div>
<div class="section">
<div class="section-title">Alimentación Manual</div>
<div class="manual-feed">
<div class="form-group">
<label>Porciones</label>
<input type="number" id="manual-portions" min="1" max="10" value="1">
</div>
<button class="btn-primary" onclick="this.getRootNode().host.manualFeed()">
Alimentar Ahora
</button>
</div>
</div>
<div class="section">
<div class="section-title">${this.editingIndex >= 0 ? 'Editar Horario' : 'Agregar Horario'}</div>
<div class="form-row">
<div class="form-group">
<label>Hora</label>
<input type="time" id="schedule-time">
</div>
<div class="form-group">
<label>Porciones</label>
<input type="number" id="schedule-portions" min="1" max="10" value="1">
</div>
</div>
<div class="form-group">
<label>Días</label>
<div class="days-selector">
${['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'].map(day => {
const dayNames = {mon: 'Lun', tue: 'Mar', wed: 'Mié', thu: 'Jue', fri: 'Vie', sat: 'Sáb', sun: 'Dom'};
return `
<label class="day-checkbox-label">
<input type="checkbox" class="day-checkbox" value="${day}">
${dayNames[day]}
</label>
`}).join('')}
</div>
</div>
<div class="button-row">
<button id="add-schedule-btn" class="btn-primary" onclick="this.getRootNode().host.addSchedule()">
Agregar Horario
</button>
<button id="cancel-edit-btn" class="btn-secondary" onclick="this.getRootNode().host.cancelEdit()">
Cancelar
</button>
</div>
</div>
<div class="section" id="schedule-list-container">
<div class="section-title">
Horarios
${this.schedules.length > 0 ? `
<button class="btn-danger btn-small" style="float: right;" onclick="this.getRootNode().host.clearAllSchedules()">
Limpiar Todo
</button>
` : ''}
</div>
${sortedSchedules.length === 0 ? `
<div class="empty-state">
No hay horarios configurados. Agrega tu primer horario de alimentación arriba.
</div>
` : `
<div class="schedule-list">
${sortedSchedules.map((schedule) => {
const actualIndex = this.schedules.findIndex(s =>
s.time === schedule.time &&
s.portions === schedule.portions &&
JSON.stringify(s.days) === JSON.stringify(schedule.days)
);
return `
<div class="schedule-item">
<div class="schedule-info">
<div class="schedule-time">${schedule.time}</div>
<div class="schedule-details">
${schedule.portions} Porci${schedule.portions > 1 ? 'ones' : 'ón'}<br>
${this.formatDays(schedule.days)}
</div>
</div>
<div class="schedule-actions">
<button class="btn-secondary btn-small" onclick="this.getRootNode().host.editSchedule(${actualIndex})">
Editar
</button>
<button class="btn-danger btn-small" onclick="this.getRootNode().host.deleteSchedule(${actualIndex})">
Eliminar
</button>
</div>
</div>
`;
}).join('')}
</div>
`}
</div>
</ha-card>
`;
// Add event listeners for day checkboxes
this.shadowRoot.querySelectorAll('.day-checkbox-label').forEach(label => {
label.addEventListener('click', (e) => {
const checkbox = label.querySelector('.day-checkbox');
checkbox.checked = !checkbox.checked;
if (checkbox.checked) {
label.classList.add('checked');
} else {
label.classList.remove('checked');
}
e.preventDefault();
});
});
}
getCardSize() {
return 9;
}
}
customElements.define('pet-feeder-card', PetFeederCard);
window.customCards = window.customCards || [];
window.customCards.push({
type: 'pet-feeder-card',
name: 'Pet Feeder Card',
description: 'A card for managing pet feeder schedules'
});
3. Load Script as Dashboard Card
Go to Settings > Dashboard > … > Resources > Add Resource
Complete with these details:
URL: /local/pet-feeder-card.js?v=1
Type: JavaScript Module
Note: the ?v=1 modifier in the URL allows you to create increments if the JS script is modified.
4. Add Automation
Create a new automation and add the following YAML code.
alias: Pet Feeder - Erogación Programada
description: Automatically feed pet. Compatible with Aqara C1 via Zigbee2MQTT.
triggers:
- minutes: "*"
trigger: time_pattern
conditions:
- condition: template
value_template: "{{ all_raw | length > 0 }}"
actions:
- repeat:
for_each: "{{ all_raw }}"
sequence:
- variables:
schedule_time: >
{% if repeat.item.t is defined %}{{ repeat.item.t }} {% elif
repeat.item.time is defined %}{{ repeat.item.time }} {% else
%}unknown{% endif %}
schedule_portions: >
{% if repeat.item.p is defined %}{{ repeat.item.p }} {% elif
repeat.item.portions is defined %}{{ repeat.item.portions }} {%
else %}1{% endif %}
schedule_days: |
{% if repeat.item.d is defined %}
{# Compressed format - decompress the day string #}
{% set day_str = repeat.item.d %}
{% set result = [] %}
{% if 'm' in day_str %}{% set result = result + ['mon'] %}{% endif %}
{% if 't' in day_str %}{% set result = result + ['tue'] %}{% endif %}
{% if 'w' in day_str %}{% set result = result + ['wed'] %}{% endif %}
{% if 'h' in day_str %}{% set result = result + ['thu'] %}{% endif %}
{% if 'f' in day_str %}{% set result = result + ['fri'] %}{% endif %}
{% if 's' in day_str %}{% set result = result + ['sat'] %}{% endif %}
{% if 'u' in day_str %}{% set result = result + ['sun'] %}{% endif %}
{{ result }}
{% elif repeat.item.days is defined %}
{# Old uncompressed format #}
{{ repeat.item.days }}
{% else %}
[]
{% endif %}
- condition: template
value_template: |
{{ schedule_time == current_time and current_day in schedule_days }}
- data:
topic: zigbee2mqtt/Aqara Pet Feeder/set
payload: |
{"serving_size": {{ schedule_portions }}}
action: mqtt.publish
- delay:
milliseconds: 500
- data:
topic: zigbee2mqtt/Aqara Pet Feeder/set
payload: "{\"feed\": \"START\"}"
action: mqtt.publish
variables:
current_time: "{{ now().strftime('%H:%M') }}"
current_day: "{{ now().strftime('%a').lower()[:3] }}"
schedules1_raw: >
{% set s1 = states('input_text.pet_feeder_schedules_1') %} {% if s1 not in
['unknown', '', '[]'] %}{{ s1 | from_json }}{% else %}[]{% endif %}
schedules2_raw: >
{% set s2 = states('input_text.pet_feeder_schedules_2') %} {% if s2 not in
['unknown', '', '[]'] %}{{ s2 | from_json }}{% else %}[]{% endif %}
schedules3_raw: >
{% set s3 = states('input_text.pet_feeder_schedules_3') %} {% if s3 not in
['unknown', '', '[]'] %}{{ s3 | from_json }}{% else %}[]{% endif %}
all_raw: "{{ schedules1_raw + schedules2_raw + schedules3_raw }}"
mode: single
5. Restart Home Assistant
6. Add Card
Add the card to your dashboard by looking for the new Pet Feeder Card and then completing the name of your Zigbee Pet Feeder. Use the YAML code below (my feeder’s name is ‘Aqara Pet Feeder’):
type: custom:pet-feeder-card
entity: sensor.pet_feeder_schedule_tracker
device_name: Aqara Pet Feeder
Some screenshots. Again, my UI is in Spanish but it’s easily editable.

