/** * Climate Scheduler Custom Panel * Modern Home Assistant custom panel implementation (replaces legacy iframe approach) */ // Version checking - detect if browser cache is stale (async function () { try { const scriptUrl = document.currentScript?.src || new URL(import.meta.url).href; const loadedVersion = new URL(scriptUrl).searchParams.get('v'); // Fetch the current server version const response = await fetch('/climate_scheduler/static/.version'); if (response.ok) { const serverVersion = (await response.text()).trim().split(',')[0]; // Compare versions - check the aren't None and if they don't match, user has stale cache if ((loadedVersion && serverVersion) && loadedVersion !== serverVersion) { console.warn('[Climate Scheduler] Version mismatch detected. Loaded:', loadedVersion, 'Server:', serverVersion); // Store in sessionStorage to avoid showing repeatedly const notificationKey = 'climate_scheduler_refresh_shown'; const shownVersion = sessionStorage.getItem(notificationKey); if (shownVersion !== serverVersion) { // Show persistent notification const event = new CustomEvent('hass-notification', { bubbles: true, cancelable: false, composed: true, detail: { message: 'Climate Scheduler has been updated. Please refresh your browser (Ctrl+F5 or Cmd+Shift+R) to load the new version.', duration: 0 // Persistent notification } }); document.body.dispatchEvent(event); // Mark as shown for this session sessionStorage.setItem(notificationKey, serverVersion); console.info('[Climate Scheduler] Refresh notification displayed'); } } } } catch (e) { console.warn('[Climate Scheduler] Version check failed:', e); } })(); // Load other JavaScript files const loadScript = (src) => { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.onload = () => resolve(); script.onerror = reject; document.head.appendChild(script); }); }; // Track if scripts are loaded let scriptsLoaded = false; const getVersion = () => { const scriptUrl = import.meta.url; const version = new URL(scriptUrl).searchParams.get('v'); if (!version) return null; // If version has comma (dev: "tag,timestamp"), use timestamp for cache busting if (version.includes(',')) { const parts = version.split(','); return parts[1]; // timestamp } // Otherwise use version as-is (HACS tag or production tag) return version; }; // Load dependencies in order const loadScripts = () => { if (scriptsLoaded) return Promise.resolve(); // Determine base path from where panel.js was loaded const scriptUrl = import.meta.url; const url = new URL(scriptUrl); // Remove panel.js and query params to get base path const basePath = url.origin + url.pathname.substring(0, url.pathname.lastIndexOf('/')); const version = getVersion(); return Promise.all([ loadScript(`${basePath}/utils.js?v=${version}`), loadScript(`${basePath}/ha-api.js?v=${version}`) ]).then(() => { return loadScript(`${basePath}/app.js?v=${version}`); }).then(() => { scriptsLoaded = true; }).catch(error => { console.error('Failed to load Climate Scheduler scripts:', error); throw error; }); }; class ClimateSchedulerPanel extends HTMLElement { constructor() { super(); this._hass = null; this.narrow = false; this.panel = null; } // Declare properties that Home Assistant looks for static get properties() { return { hass: { type: Object }, narrow: { type: Boolean }, route: { type: Object }, panel: { type: Object } }; } async connectedCallback() { this.render(); // Store reference to this panel element globally so app.js can query within it window.climateSchedulerPanelRoot = this; // Wait for scripts to load before initializing try { await loadScripts(); // Small delay to ensure DOM is fully rendered await new Promise(resolve => setTimeout(resolve, 100)); // Update version info in footer const versionElement = this.querySelector('#version-info'); if (versionElement) { try { const scriptUrl = import.meta.url; const versionParam = new URL(scriptUrl).searchParams.get('v'); let version = ''; if (versionParam) { if (versionParam.includes(',')) { // Has timestamp - dev deployment: "tag,timestamp" const parts = versionParam.split(','); const tag = (parts[0] || 'unknown').replace(/^v/, ''); version = `v${tag} (dev)`; } else { // No timestamp - production: just tag const tag = versionParam.replace(/^v/, ''); version = `v${tag}`; } } else { version = '(manual)'; } versionElement.textContent = `Climate Scheduler ${version}`; } catch (e) { console.warn('Failed to determine version:', e); versionElement.textContent = 'Climate Scheduler'; } } // Initialize the app when panel is loaded and scripts are ready if (window.initClimateSchedulerApp) { window.initClimateSchedulerApp(this.hass); } } catch (error) { console.error('Failed to initialize Climate Scheduler:', error); } } set hass(value) { this._hass = value; // Apply theme based on Home Assistant theme mode if (value && value.themes) { const isDark = value.themes.darkMode; if (isDark) { // Dark mode is default, remove attribute document.documentElement.removeAttribute('data-theme'); this.removeAttribute('data-theme'); } else { // Light mode needs explicit attribute document.documentElement.setAttribute('data-theme', 'light'); this.setAttribute('data-theme', 'light'); } } // Pass hass object to app if it's already initialized if (window.updateHassConnection && value) { window.updateHassConnection(value); } } get hass() { return this._hass; } render() { if (!this.querySelector('.container')) { // Load CSS using same base path detection as scripts const scriptUrl = import.meta.url; const url = new URL(scriptUrl); const basePath = url.origin + url.pathname.substring(0, url.pathname.lastIndexOf('/')); const version = getVersion(); const styleLink = document.createElement('link'); styleLink.rel = 'stylesheet'; styleLink.href = `${basePath}/styles.css?v=${version}`; this.appendChild(styleLink); // Create container div for content const container = document.createElement('div'); container.innerHTML = `
Set the default temperature schedule used when clearing or creating new schedules
Choose what information to display when hovering over the graph
Temperature rounding when dragging nodes on the graph
Step size for temperature input fields and up/down buttons
Step size for humidity slider in node settings dialog
Automatically create sensors to track heating/cooling rates for performance analysis
When enabled, creates sensor.climate_scheduler_[name]_rate for each thermostat to track temperature change rate (°C/h)
Configure which days are workdays for 5/2 mode scheduling
Checking if Workday integration is installed...