Last active
January 1, 2026 09:51
-
-
Save flfue/d3670e3ce74401078e8fa6657e6fd92b to your computer and use it in GitHub Desktop.
Shelly 2PM cover blind script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Shelly Blinds Controller Script | |
| // This script automatically controls Shelly smart blinds with scheduled actions. | |
| // It executes a morning routine and an evening routine once per day. | |
| // Each routine executes at a randomized time within a configured window, | |
| // and can open, close, or set the blinds to a specific position percentage. | |
| // Routines are managed with daily state tracking to ensure single execution per day. | |
| // Note: When the script starts, it cleans up any existing scheduled jobs related to Cover operations | |
| // === CONFIGURATION === | |
| const CONFIG = { | |
| // Morning settings - random time within window | |
| morningStartTime: "06:00", // Window start (HH:MM format) | |
| morningEndTime: "08:00", // Window end (HH:MM format) | |
| morningAction: "position", // Action: "open", "close", or "position" | |
| morningMinPosition: 85, // Minimum position percentage (0-100) when action is "position" | |
| morningMaxPosition: 100, // Maximum position percentage (0-100) when action is "position" | |
| // Evening settings - random time within window | |
| eveningStartTime: "18:00", // Window start (HH:MM format) | |
| eveningEndTime: "19:00", // Window end (HH:MM format) | |
| eveningAction: "position", // Action: "open", "close", or "position" | |
| eveningMinPosition: 0, // Minimum position percentage (0-100) when action is "position" | |
| eveningMaxPosition: 30, // Maximum position percentage (0-100) when action is "position" | |
| }; | |
| // State tracking for daily execution | |
| const STATE = { | |
| lastMorningDay: -1, | |
| lastEveningDay: -1, | |
| morningScheduleId: null, | |
| eveningScheduleId: null | |
| }; | |
| // === HELPER FUNCTIONS === | |
| /** | |
| * Execute cover action (open/close/position) | |
| */ | |
| function executeCoverAction(action, position) { | |
| let method; | |
| let params = { id: 0 }; | |
| if (action === "open") { | |
| method = "Cover.Open"; | |
| } else if (action === "close") { | |
| method = "Cover.Close"; | |
| } else if (action === "position" && position !== undefined) { | |
| method = "Cover.GoToPosition"; | |
| params.pos = position; | |
| } else { | |
| console.log("Error: Invalid action or missing position"); | |
| return; | |
| } | |
| Shelly.call( | |
| method, | |
| params, | |
| function(result) { | |
| if (!result) { | |
| const actionStr = action === "position" ? "position " + position + "%" : action; | |
| console.log("Cover action '" + actionStr + "' executed successfully"); | |
| } else if (result.error) { | |
| console.log("Error executing cover action: " + result.error); | |
| } else { | |
| const actionStr = action === "position" ? "position " + position + "%" : action; | |
| console.log("Cover action '" + actionStr + "' executed"); | |
| } | |
| } | |
| ); | |
| } | |
| /** | |
| * Generate random position within a range (0-100) | |
| */ | |
| function getRandomPosition(minPos, maxPos) { | |
| return minPos + Math.floor(Math.random() * (maxPos - minPos + 1)); | |
| } | |
| /** | |
| * Parse time string (HH:MM) and return minutes since midnight | |
| */ | |
| function timeToMinutes(timeStr) { | |
| const parts = timeStr.split(":"); | |
| return parseInt(parts[0]) * 60 + parseInt(parts[1]); | |
| } | |
| /** | |
| * Get current time in minutes since midnight | |
| */ | |
| function getCurrentTimeMinutes() { | |
| const now = new Date(); | |
| return now.getHours() * 60 + now.getMinutes(); | |
| } | |
| /** | |
| * Generate random time within a window (in HH:MM format) | |
| */ | |
| function getRandomTimeInWindow(startTimeStr, endTimeStr) { | |
| const startMinutes = timeToMinutes(startTimeStr); | |
| const endMinutes = timeToMinutes(endTimeStr); | |
| const randomMinutes = startMinutes + Math.floor(Math.random() * (endMinutes - startMinutes)); | |
| const hours = Math.floor(randomMinutes / 60); | |
| const minutes = randomMinutes % 60; | |
| return (hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes; | |
| } | |
| /** | |
| * Get current day of year | |
| */ | |
| function getCurrentDayOfYear() { | |
| const now = new Date(); | |
| const start = new Date(now.getFullYear(), 0, 1); | |
| const diff = now - start; | |
| const oneDay = 1000 * 60 * 60 * 24; | |
| return Math.floor(diff / oneDay); | |
| } | |
| // === SCHEDULED EVENTS === | |
| /** | |
| * Convert HH:MM to cron format (MM HH * * *) | |
| */ | |
| function timeToCron(timeStr) { | |
| const parts = timeStr.split(":"); | |
| const hour = parts[0]; | |
| const minute = parts[1]; | |
| return minute + " " + hour + " * * *"; | |
| } | |
| /** | |
| * Delete a schedule by ID | |
| */ | |
| function deleteSchedule(scheduleId, routineType) { | |
| if (scheduleId === null) { | |
| return; | |
| } | |
| Shelly.call( | |
| "Schedule.Delete", | |
| { id: scheduleId }, | |
| function(result) { | |
| if (!result) { | |
| console.log("[SUCCESS] " + routineType + " schedule (ID: " + scheduleId + ") deleted"); | |
| } else if (result.error) { | |
| console.log("[ERROR] Error deleting " + routineType + " schedule: " + result.error); | |
| } else { | |
| console.log("[SUCCESS] " + routineType + " schedule (ID: " + scheduleId + ") deleted"); | |
| } | |
| } | |
| ); | |
| } | |
| /** | |
| * Clean up all scheduled cron jobs related to Cover operations | |
| */ | |
| function cleanupCoverSchedules() { | |
| console.log("[INFO] Cleaning up all Cover-related scheduled jobs..."); | |
| Shelly.call( | |
| "Schedule.List", | |
| {}, | |
| function(result) { | |
| if (!result) { | |
| console.log("[INFO] No response from Schedule.List"); | |
| return; | |
| } | |
| let scheduleList = []; | |
| // Check if result has a 'jobs' property (Shelly API returns {jobs: [...], rev: N}) | |
| if (result.jobs && Array.isArray(result.jobs)) { | |
| scheduleList = result.jobs; | |
| } else if (Array.isArray(result)) { | |
| scheduleList = result; | |
| } else if (typeof result === 'object') { | |
| // Iterate through all properties and collect schedule objects | |
| for (let key in result) { | |
| if (result.hasOwnProperty(key)) { | |
| let item = result[key]; | |
| // Check if this item looks like a schedule (has id and calls properties) | |
| if (item && typeof item === 'object' && item.id !== undefined && item.calls !== undefined) { | |
| scheduleList.push(item); | |
| } | |
| } | |
| } | |
| } | |
| console.log("[DEBUG] Total schedules found: " + scheduleList.length); | |
| // Filter to only Cover-related schedules | |
| let coverSchedules = []; | |
| scheduleList.forEach(function(schedule) { | |
| if (schedule.calls && Array.isArray(schedule.calls)) { | |
| schedule.calls.forEach(function(call) { | |
| if (call.method === "Cover.Open" || call.method === "Cover.Close" || call.method === "Cover.GoToPosition") { | |
| coverSchedules.push(schedule); | |
| } | |
| }); | |
| } | |
| }); | |
| console.log("[DEBUG] Found " + coverSchedules.length + " Cover-related schedules to delete"); | |
| // Delete sequentially to avoid "Too many calls in progress" error | |
| if (coverSchedules.length > 0) { | |
| deleteSchedulesSequentially(coverSchedules, 0); | |
| } else { | |
| console.log("[INFO] No Cover-related schedules found"); | |
| } | |
| } | |
| ); | |
| } | |
| /** | |
| * Delete schedules sequentially to avoid API rate limiting | |
| */ | |
| function deleteSchedulesSequentially(schedules, index) { | |
| if (index >= schedules.length) { | |
| console.log("[SUCCESS] Finished cleaning up all Cover-related schedules"); | |
| return; | |
| } | |
| let schedule = schedules[index]; | |
| console.log("[DEBUG] Deleting schedule " + (index + 1) + " of " + schedules.length + " (ID: " + schedule.id + ")"); | |
| Shelly.call( | |
| "Schedule.Delete", | |
| { id: schedule.id }, | |
| function(deleteResult) { | |
| if (!deleteResult) { | |
| console.log("[SUCCESS] Deleted schedule ID: " + schedule.id); | |
| } else if (deleteResult.error) { | |
| console.log("[ERROR] Error deleting schedule ID: " + schedule.id + ": " + deleteResult.error); | |
| } else { | |
| console.log("[SUCCESS] Deleted schedule ID: " + schedule.id); | |
| } | |
| // Delete the next schedule | |
| deleteSchedulesSequentially(schedules, index + 1); | |
| } | |
| ); | |
| } | |
| /** | |
| * Schedule a routine at a specific time | |
| */ | |
| function scheduleRoutine(timeStr, action, position, isEvening) { | |
| const routineType = isEvening ? "Evening" : "Morning"; | |
| const cronTime = timeToCron(timeStr); | |
| let method; | |
| let params = { id: 0 }; | |
| if (action === "open") { | |
| method = "Cover.Open"; | |
| } else if (action === "close") { | |
| method = "Cover.Close"; | |
| } else if (action === "position") { | |
| method = "Cover.GoToPosition"; | |
| params.pos = position; | |
| } | |
| console.log("[DEBUG] Scheduled time: " + timeStr); | |
| console.log("[DEBUG] Cron format: " + cronTime); | |
| const actionStr = action === "position" ? action + " " + position + "%" : action; | |
| console.log("[DEBUG] Action: " + actionStr); | |
| Shelly.call( | |
| "Schedule.Create", | |
| { | |
| enable: true, | |
| timespec: cronTime, | |
| calls: [ | |
| { | |
| method: method, | |
| params: params | |
| } | |
| ] | |
| }, | |
| function(result) { | |
| if (!result) { | |
| console.log("[SUCCESS] " + routineType + " schedule created for " + timeStr + " - Action: " + actionStr); | |
| } else if (result.error) { | |
| console.log("[ERROR] Error creating " + routineType + " schedule at " + timeStr + ": " + result.error); | |
| } else { | |
| console.log("[SUCCESS] " + routineType + " schedule created for " + timeStr + " - Action: " + actionStr); | |
| // Store the schedule ID if available | |
| if (result.id !== undefined) { | |
| if (isEvening) { | |
| STATE.eveningScheduleId = result.id; | |
| } else { | |
| STATE.morningScheduleId = result.id; | |
| } | |
| } | |
| } | |
| } | |
| ); | |
| } | |
| /** | |
| * Setup morning routine with random time | |
| */ | |
| function setupMorningRoutine() { | |
| const currentDay = getCurrentDayOfYear(); | |
| // Only setup once per day | |
| if (STATE.lastMorningDay !== currentDay) { | |
| // Delete previous day's schedule | |
| deleteSchedule(STATE.morningScheduleId, "Morning"); | |
| STATE.morningScheduleId = null; | |
| STATE.lastMorningDay = currentDay; | |
| const randomTime = getRandomTimeInWindow(CONFIG.morningStartTime, CONFIG.morningEndTime); | |
| const randomTimeMinutes = timeToMinutes(randomTime); | |
| const currentTimeMinutes = getCurrentTimeMinutes(); | |
| let actionStr = CONFIG.morningAction; | |
| let position = null; | |
| if (CONFIG.morningAction === "position") { | |
| position = getRandomPosition(CONFIG.morningMinPosition, CONFIG.morningMaxPosition); | |
| actionStr = CONFIG.morningAction + " " + position + "%"; | |
| } | |
| console.log("Morning window: " + CONFIG.morningStartTime + " - " + CONFIG.morningEndTime + ", scheduled for: " + randomTime + " (" + actionStr + ")"); | |
| // Check if the scheduled time has already passed today | |
| if (randomTimeMinutes < currentTimeMinutes) { | |
| console.log("[INFO] Scheduled time " + randomTime + " is in the past, executing immediately"); | |
| executeCoverAction(CONFIG.morningAction, position); | |
| } else { | |
| scheduleRoutine(randomTime, CONFIG.morningAction, position, false); | |
| } | |
| } | |
| } | |
| /** | |
| * Setup evening routine with random time | |
| */ | |
| function setupEveningRoutine() { | |
| const currentDay = getCurrentDayOfYear(); | |
| // Only setup once per day | |
| if (STATE.lastEveningDay !== currentDay) { | |
| // Delete previous day's schedule | |
| deleteSchedule(STATE.eveningScheduleId, "Evening"); | |
| STATE.eveningScheduleId = null; | |
| STATE.lastEveningDay = currentDay; | |
| const randomTime = getRandomTimeInWindow(CONFIG.eveningStartTime, CONFIG.eveningEndTime); | |
| const randomTimeMinutes = timeToMinutes(randomTime); | |
| const currentTimeMinutes = getCurrentTimeMinutes(); | |
| let actionStr = CONFIG.eveningAction; | |
| let position = null; | |
| if (CONFIG.eveningAction === "position") { | |
| position = getRandomPosition(CONFIG.eveningMinPosition, CONFIG.eveningMaxPosition); | |
| actionStr = CONFIG.eveningAction + " " + position + "%"; | |
| } | |
| console.log("Evening window: " + CONFIG.eveningStartTime + " - " + CONFIG.eveningEndTime + ", scheduled for: " + randomTime + " (" + actionStr + ")"); | |
| // Check if the scheduled time has already passed today | |
| if (randomTimeMinutes < currentTimeMinutes) { | |
| console.log("[INFO] Scheduled time " + randomTime + " is in the past, executing immediately"); | |
| executeCoverAction(CONFIG.eveningAction, position); | |
| } else { | |
| scheduleRoutine(randomTime, CONFIG.eveningAction, position, true); | |
| } | |
| } | |
| } | |
| // Clean up all Cover-related scheduled jobs when the script starts | |
| cleanupCoverSchedules(); | |
| setupMorningRoutine(); | |
| setupEveningRoutine(); | |
| // Re-check at midnight to setup next day's schedule | |
| Timer.set( | |
| 10 * 60 * 1000, // Check every 10 minutes | |
| true, | |
| function() { | |
| setupMorningRoutine(); | |
| setupEveningRoutine(); | |
| } | |
| ); | |
| console.log("Blinds controller initialized with random time windows"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment