Skip to content

Instantly share code, notes, and snippets.

@flfue
Last active January 1, 2026 09:51
Show Gist options
  • Select an option

  • Save flfue/d3670e3ce74401078e8fa6657e6fd92b to your computer and use it in GitHub Desktop.

Select an option

Save flfue/d3670e3ce74401078e8fa6657e6fd92b to your computer and use it in GitHub Desktop.
Shelly 2PM cover blind script
// 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