How to display a Fullscreen schedule view as a Dashboard?

@chairstacker
For my base case, I’m only using a single schedule for my central heating (but have since updated the code to handle editing of all schedules present). The time slots are when i want the heating to come on so between 05:00 to 07:30 then it’s off until the next time slot.
I use helpers and automations to turn on my Tuya based central heating control thermostats, which have the room temperatures programmed in to them. But i also have temperature control in the HA automations too. This way my heating will not even turn on with the schedule unless the house average temperature is below a helper input value that I set manually (22 degrees) in my case.

you can create a button within your dashboard that you dont have the side bar menu displayed. then edit the button yaml to this:

show_name: true
show_icon: true
type: button
name: Schedule Editor
icon: mdi:calendar
tap_action:
  action: navigate
  navigation_path: /schedule-editor
icon_height: 25px

OK guy here is a further updated code, this one is as above with the drop down picklist for which schedule you want to edit (supports multiple schedules). Also this one you have a “back” button on the top right. It is implimented so that on users who dont have the side bar, they can return to the Dashboard View that you set in the updated code. It currently refers to my Dashboard view so you will need to change the path for your requirements. Edit rows 759 to 789 with your URL path. (Not changing these lines makes no difference as i found out by installing the component on my second home system. The back button just reverted me to my default home dashboard).

To allow non Admn users to access the Schedule Editor, use the code that I posted above to @chairstacker

Here is the updated Code for the inti.py file (split in to two parts due to the 32000 character limitation):

Part 1:

"""Schedule Editor Integration."""
import logging
import json
import os
from aiohttp import web

from homeassistant.core import HomeAssistant
from homeassistant.components.http import HomeAssistantView
from homeassistant.components import frontend

_LOGGER = logging.getLogger(__name__)

DOMAIN = "schedule_editor"


async def async_setup(hass: HomeAssistant, config: dict):
    """Set up the Schedule Editor component."""
    
    _LOGGER.info("Setting up Schedule Editor")
    
    # Register API endpoints first
    hass.http.register_view(ScheduleGetView(hass))
    hass.http.register_view(ScheduleSaveView(hass))
    hass.http.register_view(SchedulePanelView(hass))
    hass.http.register_view(ScheduleRestartView(hass))
    
    # Register as iframe panel (not async)
    frontend.async_register_built_in_panel(
        hass,
        component_name="iframe",
        sidebar_title="Heating Schedule Editor",
        sidebar_icon="mdi:calendar-clock",
        frontend_url_path="schedule-editor",
        config={
            "url": "/api/schedule_editor/panel",
        },
        require_admin=False,
    )
    
    _LOGGER.info("Schedule Editor setup complete")
    
    return True


class SchedulePanelView(HomeAssistantView):
    """View to serve the schedule editor HTML."""
    
    url = "/api/schedule_editor/panel"
    name = "api:schedule_editor:panel"
    requires_auth = False
    
    def __init__(self, hass):
        """Initialize."""
        self.hass = hass
    
    async def get(self, request):
        """Serve the editor HTML."""
        html_content = """
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Central Heating Schedule Editor</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #111;
            color: #fff;
            padding: 12px;
        }
        .container { max-width: 1200px; margin: 0 auto; }
        .header {
            display: flex;
            flex-direction: column;
            gap: 12px;
            margin-bottom: 16px;
            padding: 16px;
            background: #1a1a1a;
            border-radius: 8px;
            position: relative;
        }
        .header-top {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .btn-back {
            padding: 8px 16px;
            background: #555;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            font-family: inherit;
            display: inline-flex;
            align-items: center;
            gap: 6px;
            white-space: nowrap;
        }
        .btn-back:hover {
            background: #666;
        }
        .btn-back:active {
            background: #777;
        }
        h1 { 
            font-size: 20px;
            text-align: center;
            flex: 1;
        }
        .file-selector {
            display: flex;
            align-items: center;
            gap: 8px;
            flex-wrap: wrap;
        }
        .file-selector label {
            color: #999;
            font-size: 14px;
            white-space: nowrap;
        }
        .file-selector select {
            flex: 1;
            min-width: 150px;
            padding: 8px 12px;
            background: #2a2a2a;
            border: 1px solid #444;
            border-radius: 4px;
            color: #fff;
            font-size: 14px;
            cursor: pointer;
        }
        @media (min-width: 768px) {
            body { padding: 20px; }
            .header { padding: 20px; }
            h1 { 
                font-size: 24px;
                text-align: left;
            }
        }
        .btn-small {
            padding: 8px 12px;
            background: #555;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            flex-shrink: 0;
        }
        .btn-small:hover {
            background: #666;
        }
        .btn {
            padding: 12px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            width: 100%;
        }
        .btn-primary { background: #03a9f4; color: white; }
        .btn-primary:hover { background: #0288d1; }
        @media (min-width: 768px) {
            .btn {
                width: auto;
            }
        }
        .message {
            padding: 12px;
            border-radius: 4px;
            margin-bottom: 12px;
            text-align: center;
            display: none;
            font-size: 14px;
        }
        .message.show { display: block; }
        .message.success { background: #4caf50; }
        .message.error { background: #f44336; }
        .day-card {
            background: #1a1a1a;
            border-radius: 8px;
            padding: 12px;
            margin-bottom: 12px;
        }
        .day-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 12px;
        }
        .day-header h3 {
            font-size: 16px;
            text-transform: capitalize;
        }
        .btn-add {
            padding: 6px 10px;
            background: #03a9f4;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 11px;
            white-space: nowrap;
        }
        @media (min-width: 768px) {
            .day-card {
                padding: 16px;
                margin-bottom: 16px;
            }
            .day-header h3 {
                font-size: 18px;
            }
            .btn-add {
                padding: 6px 12px;
                font-size: 12px;
            }
        }
        .time-slot {
            display: flex;
            align-items: center;
            gap: 6px;
            padding: 8px;
            background: #2a2a2a;
            border-radius: 4px;
            margin-bottom: 8px;
            flex-wrap: wrap;
        }
        .time-slot label {
            color: #999;
            font-size: 11px;
        }
        .time-input {
            padding: 8px 6px;
            border: 1px solid #444;
            border-radius: 4px;
            background: #333;
            color: #fff;
            font-size: 14px;
            min-width: 90px;
        }
        .btn-delete {
            padding: 6px 10px;
            background: #f44336;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-left: auto;
            font-size: 16px;
        }
        @media (min-width: 768px) {
            .time-slot {
                gap: 8px;
                flex-wrap: nowrap;
            }
            .time-slot label {
                font-size: 12px;
            }
            .time-input {
                padding: 6px;
            }
            .btn-delete {
                padding: 6px 12px;
            }
        }
        .no-slots {
            color: #666;
            font-style: italic;
            font-size: 13px;
        }
        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.8);
            align-items: center;
            justify-content: center;
            z-index: 1000;
            padding: 20px;
        }
        .modal.show {
            display: flex;
        }
        .modal-content {
            background: #1a1a1a;
            border-radius: 8px;
            padding: 20px;
            max-width: 500px;
            width: 100%;
            border: 1px solid #333;
        }
        .modal-content h2 {
            margin-bottom: 12px;
            color: #fff;
            font-size: 18px;
        }
        .modal-content p {
            margin-bottom: 16px;
            color: #ccc;
            line-height: 1.5;
            font-size: 14px;
        }
        @media (min-width: 768px) {
            .modal-content {
                padding: 24px;
            }
            .modal-content h2 {
                margin-bottom: 16px;
                font-size: 20px;
            }
            .modal-content p {
                margin-bottom: 24px;
                font-size: 15px;
            }
        }
        .modal-buttons {
            display: flex;
            gap: 10px;
            justify-content: flex-end;
            flex-direction: column;
        }
        @media (min-width: 480px) {
            .modal-buttons {
                flex-direction: row;
                gap: 12px;
            }
        }
        .btn-secondary {
            background: #555;
            color: white;
            padding: 12px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        .btn-secondary:hover {
            background: #666;
        }
        .btn-danger {
            background: #f44336;
            color: white;
            padding: 12px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        .btn-danger:hover {
            background: #d32f2f;
        }
        .quick-actions {
            background: #1a1a1a;
            border-radius: 8px;
            padding: 12px;
            margin-bottom: 16px;
        }
        .quick-actions h3 {
            margin-bottom: 12px;
            color: #fff;
            font-size: 16px;
        }
        .quick-actions-buttons {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }
        .btn-copy {
            padding: 10px 14px;
            background: #673ab7;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            white-space: nowrap;
        }
        .btn-copy:hover {
            background: #7e57c2;
        }
        .btn-delete-schedule {
            padding: 10px 14px;
            background: #f44336;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            white-space: nowrap;
        }
        .btn-delete-schedule:hover {
            background: #d32f2f;
        }
        @media (min-width: 768px) {
            .quick-actions {
                padding: 16px;
                margin-bottom: 24px;
            }
            .btn-copy, .btn-delete-schedule {
                padding: 8px 16px;
                font-size: 13px;
            }
            .btn-delete-schedule {
                margin-left: auto;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <div class="header-top">
                <h1>Schedule Editor</h1>
                <button class="btn-back" onclick="navigateBack()">
                    <span>←</span>
                    <span>Back</span>
                </button>
            </div>
            <button class="btn btn-primary" onclick="saveSchedule()">Save Schedule</button>
            <div class="file-selector">
                <label for="schedule-select">Schedule:</label>
                <select id="schedule-select" onchange="loadSelectedSchedule()">
                    <option value="schedule">Default Schedule</option>
                </select>
                <button class="btn btn-small" onclick="refreshScheduleList()">↻</button>
            </div>
        </div>
        <div id="message" class="message"></div>
        
        <!-- Quick Copy Buttons -->
        <div class="quick-actions">
            <h3>Quick Copy</h3>
            <div class="quick-actions-buttons">
                <button class="btn btn-copy" onclick="copyMondayToWeekdays()">Copy Mon → Tue-Fri</button>
                <button class="btn btn-copy" onclick="copyFridayToSaturday()">Copy Fri → Sat</button>
                <button class="btn btn-copy" onclick="copySaturdayToSunday()">Copy Sat → Sun</button>
                <button class="btn btn-delete-schedule" onclick="confirmDeleteSchedule()">Delete Schedule</button>
            </div>
        </div>
        
        <!-- Delete Confirmation Modal -->
        <div id="delete-modal" class="modal">
            <div class="modal-content">
                <h2>Delete Schedule?</h2>
                <p id="delete-message">Are you sure you want to delete this schedule?</p>
                <p style="color: #f44336; font-weight: bold; margin-top: 12px;" id="delete-name"></p>
                <div class="modal-buttons">
                    <button class="btn btn-secondary" onclick="closeDeleteModal()">Cancel</button>
                    <button class="btn btn-danger" onclick="deleteSchedule()">Delete</button>
                </div>
            </div>
        </div>
        
        <!-- Restart Warning Modal -->
        <div id="restart-modal" class="modal">
            <div class="modal-content">
                <h2>Restart Home Assistant?</h2>
                <p>The schedule has been saved successfully. Would you like to restart Home Assistant now to apply the changes?</p>
                <div class="modal-buttons">
                    <button class="btn btn-secondary" onclick="closeRestartModal()">Not Now</button>
                    <button class="btn btn-danger" onclick="restartHomeAssistant()">Restart Now</button>
                </div>
            </div>
        </div>
        
        <div id="schedule-container"></div>
    </div>

    <script>
        let scheduleData = null;
        let currentScheduleIndex = 0;
        const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];

        async function loadSchedule() {
            try {
                const response = await fetch('/api/schedule_editor/get');
                if (!response.ok) {
                    throw new Error('HTTP ' + response.status);
                }
                scheduleData = await response.json();
                updateScheduleList();
                renderSchedule();
            } catch (error) {
                console.error('Load error:', error);
                showMessage('Error loading schedule: ' + error.message, 'error');
            }
        }

        function updateScheduleList() {
            const select = document.getElementById('schedule-select');
            select.innerHTML = '';
            
            scheduleData.data.items.forEach((item, index) => {
                const option = document.createElement('option');
                option.value = index;
                option.textContent = item.name || item.id;
                if (index === currentScheduleIndex) {
                    option.selected = true;
                }
                select.appendChild(option);
            });
        }

        function loadSelectedSchedule() {
            const select = document.getElementById('schedule-select');
            currentScheduleIndex = parseInt(select.value);
            renderSchedule();
            const scheduleName = scheduleData.data.items[currentScheduleIndex].name;
            showMessage('Loaded: ' + scheduleName, 'success');
        }

        function refreshScheduleList() {
            loadSchedule();
        }

        function getAuthToken() {
            // Try to get token from parent window (Home Assistant)
            try {
                if (window.parent && window.parent !== window) {
                    const haAuth = window.parent.localStorage.getItem('hassTokens');
                    if (haAuth) {
                        const tokens = JSON.parse(haAuth);
                        return tokens.access_token;
                    }
                }
            } catch (e) {
                console.error('Could not access parent auth:', e);
            }
            
            // Fallback: try local storage
            try {
                const haAuth = localStorage.getItem('hassTokens');
                if (haAuth) {
                    const tokens = JSON.parse(haAuth);
                    return tokens.access_token;
                }
            } catch (e) {
                console.error('Could not access local auth:', e);
            }
            
            return '';
        }

        function renderSchedule() {
            const container = document.getElementById('schedule-container');
            container.innerHTML = '';
            
            const schedule = scheduleData.data.items[currentScheduleIndex];
            
            days.forEach(day => {
                const dayCard = document.createElement('div');
                dayCard.className = 'day-card';
                
                const header = document.createElement('div');
                header.className = 'day-header';
                header.innerHTML = `
                    <h3>${day}</h3>
                    <button class="btn-add" onclick="addTimeSlot('${day}')">+ Add Slot</button>
                `;
                dayCard.appendChild(header);
                
                const slots = schedule[day] || [];
                if (slots.length > 0) {
                    slots.forEach((slot, index) => {
                        const slotDiv = document.createElement('div');
                        slotDiv.className = 'time-slot';
                        slotDiv.innerHTML = `
                            <label>From:</label>
                            <input type="time" class="time-input" value="${slot.from.substring(0, 5)}" 
                                   onchange="updateTimeSlot('${day}', ${index}, 'from', this.value)">
                            <label>To:</label>
                            <input type="time" class="time-input" value="${slot.to.substring(0, 5)}" 
                                   onchange="updateTimeSlot('${day}', ${index}, 'to', this.value)">
                            <button class="btn-delete" onclick="removeTimeSlot('${day}', ${index})">×</button>
                        `;
                        dayCard.appendChild(slotDiv);
                    });
                } else {
                    const noSlots = document.createElement('p');
                    noSlots.className = 'no-slots';
                    noSlots.textContent = 'No time slots';
                    dayCard.appendChild(noSlots);
                }
                
                container.appendChild(dayCard);
            });
        }

        function addTimeSlot(day) {
            const schedule = scheduleData.data.items[currentScheduleIndex];
            if (!schedule[day]) schedule[day] = [];
            schedule[day].push({ from: "09:00:00", to: "17:00:00" });
            renderSchedule();
        }

        function removeTimeSlot(day, index) {
            scheduleData.data.items[currentScheduleIndex][day].splice(index, 1);
            renderSchedule();
        }

        function updateTimeSlot(day, index, field, value) {
            scheduleData.data.items[currentScheduleIndex][day][index][field] = value + ":00";
        }

        function copyMondayToWeekdays() {
            const schedule = scheduleData.data.items[currentScheduleIndex];
            const mondaySlots = schedule.monday ? JSON.parse(JSON.stringify(schedule.monday)) : [];
            
            ['tuesday', 'wednesday', 'thursday', 'friday'].forEach(day => {
                schedule[day] = JSON.parse(JSON.stringify(mondaySlots));
            });
            
            renderSchedule();
            showMessage('Monday schedule copied to Tuesday-Friday', 'success');
        }

        function copyFridayToSaturday() {
            const schedule = scheduleData.data.items[currentScheduleIndex];
            const fridaySlots = schedule.friday ? JSON.parse(JSON.stringify(schedule.friday)) : [];
            
            schedule.saturday = JSON.parse(JSON.stringify(fridaySlots));
            
            renderSchedule();
            showMessage('Friday schedule copied to Saturday', 'success');
        }

        function copySaturdayToSunday() {
            const schedule = scheduleData.data.items[currentScheduleIndex];
            const saturdaySlots = schedule.saturday ? JSON.parse(JSON.stringify(schedule.saturday)) : [];
            
            schedule.sunday = JSON.parse(JSON.stringify(saturdaySlots));
            
            renderSchedule();
            showMessage('Saturday schedule copied to Sunday', 'success');
        }

        async function saveSchedule() {
            try {
                const response = await fetch('/api/schedule_editor/save', {
                    method: 'POST',
                    headers: { 
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(scheduleData)
                });
                
                if (response.ok) {
                    const scheduleName = scheduleData.data.items[currentScheduleIndex].name;
                    showMessage('Schedule "' + scheduleName + '" saved successfully!', 'success');
                    // Show restart modal after successful save
                    setTimeout(() => {
                        showRestartModal();
                    }, 1000);
                } else {
                    const errorText = await response.text();
                    showMessage('Error saving schedule: ' + response.status, 'error');
                }
            } catch (error) {
                console.error('Save error:', error);
                showMessage('Error saving schedule: ' + error.message, 'error');
            }
        }

        function showRestartModal() {
            document.getElementById('restart-modal').classList.add('show');
        }

        function closeRestartModal() {
            document.getElementById('restart-modal').classList.remove('show');
        }

        function confirmDeleteSchedule() {
            const schedule = scheduleData.data.items[currentScheduleIndex];
            const scheduleName = schedule.name || schedule.id;
            
            document.getElementById('delete-name').textContent = scheduleName;
            document.getElementById('delete-modal').classList.add('show');
        }

        function closeDeleteModal() {
            document.getElementById('delete-modal').classList.remove('show');
        }

        function deleteSchedule() {
            const scheduleName = scheduleData.data.items[currentScheduleIndex].name;
            
            // Remove the schedule from the array
            scheduleData.data.items.splice(currentScheduleIndex, 1);
            
            // Check if there are any schedules left
            if (scheduleData.data.items.length === 0) {
                showMessage('Cannot delete the last schedule!', 'error');
                closeDeleteModal();
                // Reload to restore the schedule
                loadSchedule();
                return;
            }
            
            // Reset to first schedule if we deleted the current one
            if (currentScheduleIndex >= scheduleData.data.items.length) {
                currentScheduleIndex = 0;
            }
            
            closeDeleteModal();
            updateScheduleList();
            renderSchedule();
            showMessage('Schedule "' + scheduleName + '" deleted. Click Save to confirm.', 'success');
        }

        async function restartHomeAssistant() {
            closeRestartModal();
            showMessage('Restarting Home Assistant...', 'success');
            
            try {
                const response = await fetch('/api/schedule_editor/restart', {
                    method: 'POST'
                });
                
                if (response.ok) {
                    showMessage('Home Assistant is restarting. Please wait...', 'success');
                } else {
                    showMessage('Could not restart Home Assistant', 'error');
                }
            } catch (error) {
                // This is expected as HA will disconnect during restart
                showMessage('Home Assistant is restarting. This page will reload automatically...', 'success');
                
                // Try to reload the page after 30 seconds
                setTimeout(() => {
                    window.location.reload();
                }, 30000);
            }
        }

        function showMessage(text, type) {
            const msg = document.getElementById('message');
            msg.textContent = text;
            msg.className = `message ${type} show`;
            setTimeout(() => {
                msg.className = 'message';
            }, 3000);
        }

        function navigateBack() {
            console.log('Back button clicked!');
            
            try {
                if (window.parent && window.parent !== window) {
                    console.log('Parent window detected');
                    
                    // Try accessing Home Assistant's history API directly
                    if (window.parent.history) {
                        console.log('Trying parent.history');
                        window.parent.history.pushState(null, '', '/lovelace-hauxtona7/default_view');
                        
                        // Trigger popstate event
                        const popStateEvent = new PopStateEvent('popstate', { state: null });
                        window.parent.dispatchEvent(popStateEvent);
                    }
                    
                    // Try Home Assistant specific navigation
                    if (window.parent.document && window.parent.document.querySelector('home-assistant')) {
                        console.log('Found home-assistant element');
                        const ha = window.parent.document.querySelector('home-assistant');
                        if (ha && ha.navigate) {
                            console.log('Calling ha.navigate');
                            ha.navigate('/lovelace-hauxtona7/default_view');
                        }
                    }
                    
                    // Fire a custom event on parent
                    const navEvent = new CustomEvent('hass-navigate', {
                        detail: { path: '/lovelace-hauxtona7/default_view' },
                        bubbles: true,
                        composed: true
                    });
                    window.parent.dispatchEvent(navEvent);
                    
                } else {
                    console.log('No parent window, navigating directly');
                    window.location.href = '/lovelace-hauxtona7/default_view';
                }
            } catch (error) {
                console.error('Navigation error:', error);
            }
        }

        loadSchedule();
    </script>
</body>
</html>
        """
        return web.Response(text=html_content, content_type='text/html')


class ScheduleGetView(HomeAssistantView):
    """View to get the schedule data."""
    
    url = "/api/schedule_editor/get"
    name = "api:schedule_editor:get"
    requires_auth = False
    
    def __init__(self, hass):
        """Initialize."""
        self.hass = hass
    
    async def get(self, request):
        """Get schedule data."""
        schedule_path = self.hass.config.path(".storage/schedule")
        
        try:
            if os.path.exists(schedule_path):
                with open(schedule_path, 'r') as f:
                    data = json.load(f)
                return web.json_response(data)
            else:
                # Return default schedule if file doesn't exist
                default_schedule = {
                    "version": 1,
                    "minor_version": 1,
                    "key": "schedule",
                    "data": {
                        "items": [{
                            "id": "central_heating_2",
                            "name": "Central Heating Schedule",
                            "icon": "mdi:home-clock-outline",
                            "monday": [],
                            "tuesday": [],
                            "wednesday": [],
                            "thursday": [],
                            "friday": [],
                            "saturday": [],
                            "sunday": []
                        }]
                    }
                }
                return web.json_response(default_schedule)
        except Exception as e:
            _LOGGER.error(f"Error loading schedule: {e}")
            return web.json_response({"error": str(e)}, status=500)



Part two:

class ScheduleSaveView(HomeAssistantView):
    """View to save the schedule data."""
    
    url = "/api/schedule_editor/save"
    name = "api:schedule_editor:save"
    requires_auth = False
    
    def __init__(self, hass):
        """Initialize."""
        self.hass = hass
    
    async def post(self, request):
        """Save schedule data."""
        schedule_path = self.hass.config.path(".storage/schedule")
        
        try:
            data = await request.json()
            
            # Create backup of existing file
            if os.path.exists(schedule_path):
                backup_path = f"{schedule_path}.backup"
                with open(schedule_path, 'r') as f:
                    backup_data = f.read()
                with open(backup_path, 'w') as f:
                    f.write(backup_data)
                _LOGGER.info("Created backup of schedule")
            
            # Save new data
            with open(schedule_path, 'w') as f:
                json.dump(data, f, indent=2)
            
            _LOGGER.info("Schedule saved successfully")
            
            # Reload the schedule integration
            try:
                await self.hass.services.async_call(
                    "homeassistant",
                    "reload_config_entry",
                    {"entry_id": "schedule"},
                    blocking=True
                )
                _LOGGER.info("Schedule integration reloaded")
            except Exception as reload_error:
                _LOGGER.warning(f"Could not reload schedule automatically: {reload_error}")
                # Try alternative reload method
                try:
                    # Fire an event to trigger schedule reload
                    self.hass.bus.async_fire("schedule_updated")
                    _LOGGER.info("Fired schedule_updated event")
                except Exception as e:
                    _LOGGER.warning(f"Could not fire reload event: {e}")
            
            return web.json_response({"success": True, "message": "Schedule saved and reloaded successfully"})
        except Exception as e:
            _LOGGER.error(f"Error saving schedule: {e}")
            return web.json_response({"error": str(e)}, status=500)


class ScheduleRestartView(HomeAssistantView):
    """View to restart Home Assistant."""
    
    url = "/api/schedule_editor/restart"
    name = "api:schedule_editor:restart"
    requires_auth = False
    
    def __init__(self, hass):
        """Initialize."""
        self.hass = hass
    
    async def post(self, request):
        """Restart Home Assistant."""
        try:
            _LOGGER.info("Restarting Home Assistant via Schedule Editor")
            
            # Call the homeassistant.restart service
            await self.hass.services.async_call(
                "homeassistant",
                "restart",
                blocking=False
            )
            
            return web.json_response({"success": True, "message": "Home Assistant is restarting"})
        except Exception as e:
            _LOGGER.error(f"Error restarting Home Assistant: {e}")
            return web.json_response({"error": str(e)}, status=500)

Understood - sounds much more complicated now :face_with_open_eyes_and_hand_over_mouth:

Thanks for the code - that’s very helpful :+1:

I think it depends on your situation and how much control in HA you want. What type of thermostats are you using? Another probably better approach would be a Zigbee value on every radiator and TGDC just an ON/OFF type control switch.

If you want any help just let me know and i can share more details or automations.

I have a completely different setup: 2-Zone Forced Air

I’ve had it set up for quite a while and the Climate Schedule integration I linked above works great for me.

I was just wondering why you’d manage a complete on/off setup based on calendar entries - now I know :wink:

I just created something that might help you: Graphical weekly scheduler - Configuration / Frontend - Home Assistant Community – a couple of screenshots are there as well