Since I’m not really happy with the backup function in homey, I did ask Gemini to help create a script to get all flows and split them up, in separated folders/files as backup.
Just run the “api script” in the api-playground:
Homey Developer Tools.
Then run the local “split script” file in cli/terminal, like this: “python3 split_flows.py” to split them in to standard and advanced separated flows: folders/files.
Update: 4
Made the api script output flow names more unique, in the script only.
So the split script is change aswell
This Api Script will generate a file that saves to your computer, with all the flow output in a .txt file named “all_flows.txt”
Now, all you need to do is run the split script file in the same folder as the saved file “all_flows.txt”
There is a long name version, and a short name version
Long name: name + id in the flow name
Short name: name only in the flow name
Api Script
async function exportHomeyFlows() {
try {
let output = "";
// Fetch folders and flows
const flowFolders = await Homey.flow.getFlowFolders().catch(() => ({}));
const flows = await Homey.flow.getFlows();
const advancedFlows = await Homey.flow.getAdvancedFlows();
// Helper function to robustly resolve nested folder paths
function getFolderPath(folderId) {
if (!folderId) return "Root";
let path = [];
let currentId = folderId;
let safetyCounter = 0; // Prevent infinite loops if data is circular
while (currentId && flowFolders[currentId] && safetyCounter < 10) {
const folder = flowFolders[currentId];
path.unshift(folder.name);
// Check various possible parent property names used in different Homey API versions
currentId = folder.folder || folder.parent || folder.parentFolder;
safetyCounter++;
}
// If the folder ID exists in our list but wasn't caught above, or couldn't resolve
if (path.length === 0 && flowFolders[folderId]) {
return flowFolders[folderId].name;
}
return path.length > 0 ? path.join(" / ") : "Root";
}
output += `--- FOUND ${Object.keys(flows).length} STANDARD FLOWS ---\n\n`;
// Output Standard Flows
for (const id in flows) {
const flow = flows[id];
const folderName = getFolderPath(flow.folder);
const orderedFlow = { type: "standard", folderName: folderName, ...flow };
const safeFolderName = folderName.replace(/[/\\?%*:|"<>]/g, '-');
const safeFlowName = flow.name.replace(/[/\\?%*:|"<>]/g, '-');
output += `_.:._ START_FLOW: [${safeFolderName}] ${safeFlowName} (${flow.id}) _.:._\n`;
output += JSON.stringify(orderedFlow, null, 2) + "\n";
output += `_.:._ END_FLOW _.:._\n\n`;
}
output += `--- FOUND ${Object.keys(advancedFlows).length} ADVANCED FLOWS ---\n\n`;
// Output Advanced Flows
for (const id in advancedFlows) {
const aflow = advancedFlows[id];
const folderName = getFolderPath(aflow.folder);
const orderedAflow = { type: "advanced", folderName: folderName, ...aflow };
const safeFolderName = folderName.replace(/[/\\?%*:|"<>]/g, '-');
const safeFlowName = aflow.name.replace(/[/\\?%*:|"<>]/g, '-');
output += `_.:._ START_ADVANCED_FLOW: [${safeFolderName}] ${safeFlowName} (${aflow.id}) _.:._\n`;
output += JSON.stringify(orderedAflow, null, 2) + "\n";
output += `_.:._ END_ADVANCED_FLOW _.:._\n\n`;
}
output += "--- EXPORT COMPLETE ---";
// --- AUTO DOWNLOAD MECHANISM ---
const blob = new Blob([output], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `all_flows.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
return "File download triggered successfully!";
} catch (error) {
return "Error fetching flows: " + error.message;
}
}
exportHomeyFlows();
Split Script : Long Name
import json
import os
import re
input_file = "all_flows.txt"
if not os.path.exists(input_file):
print(f"Error: {input_file} not found.")
exit(1)
STD_DIR = "Standard_Flows"
ADV_DIR = "Advanced_Flows"
os.makedirs(STD_DIR, exist_ok=True)
os.makedirs(ADV_DIR, exist_ok=True)
with open(input_file, "r", encoding="utf-8") as f:
content = f.read()
content = content.replace("_.:._ START_ADVANCED_FLOW: ", "_.:._ START_FLOW: ")
blocks = content.split("_.:._ START_FLOW: ")
count = 0
# Matches: [Folder] Name (ID)
header_pattern = re.compile(r"^\[(.*?)\]\s+(.*?)\s+\(([0-9a-fA-F-]+)\)$")
for block in blocks:
if "_.:._" not in block:
continue
header, _, body = block.partition("\n")
raw_header = header.replace("_.:._", "").strip()
json_str, _, _ = body.partition("_.:._")
json_str = json_str.strip()
if not json_str:
continue
try:
flow_data = json.loads(json_str)
flow_type = flow_data.get("type", "unknown")
# Use the uncorrupted hierarchical path stored inside the JSON object
folder_name = flow_data.get("folderName", "Root")
match = header_pattern.match(raw_header)
if match:
_, flow_name, flow_id = match.groups()
else:
flow_name = raw_header
flow_id = flow_data.get("id", "unknown")
if flow_type == "standard":
base_dir = STD_DIR
extension = ".homeystdflow"
elif flow_type == "advanced":
base_dir = ADV_DIR
extension = ".homeyadvflow"
else:
base_dir = "."
extension = ".json"
# Split the hierarchical path by " / " to build true nested subfolders
folder_parts = folder_name.split(" / ")
clean_parts = [
"".join(c if c.isalnum() or c in "._-" else "_" for c in part).strip("_")
for part in folder_parts
]
clean_parts = [p if p else "Root" for p in clean_parts]
# Build the nested directory path safely
target_dir = os.path.join(base_dir, *clean_parts)
os.makedirs(target_dir, exist_ok=True)
clean_name = "".join(c if c.isalnum() or c in "._-" else "_" for c in flow_name).strip("_")
# Base filename using name
filename = os.path.join(target_dir, f"{clean_name}_{flow_id}{extension}")
# Fallback conflict resolution: if filename exists, append incremental counter
counter = 1
base_filename = filename
while os.path.exists(filename):
counter += 1
name_part, ext_part = os.path.splitext(base_filename)
filename = f"{name_part}_{counter}{ext_part}"
with open(filename, "w", encoding="utf-8") as out_f:
json.dump(flow_data, out_f, indent=2, ensure_ascii=False)
print(f"Saved: {filename}")
count += 1
except json.JSONDecodeError as e:
print(f"Failed to parse flow '{raw_header}': {e}")
except Exception as e:
continue
print(f"\nDone! Successfully processed {count} flows.")
Split Script : Short Name
import json
import os
import re
input_file = "all_flows.txt"
if not os.path.exists(input_file):
print(f"Error: {input_file} not found.")
exit(1)
STD_DIR = "Standard_Flows"
ADV_DIR = "Advanced_Flows"
os.makedirs(STD_DIR, exist_ok=True)
os.makedirs(ADV_DIR, exist_ok=True)
with open(input_file, "r", encoding="utf-8") as f:
content = f.read()
content = content.replace("_.:._ START_ADVANCED_FLOW: ", "_.:._ START_FLOW: ")
blocks = content.split("_.:._ START_FLOW: ")
count = 0
# Matches: [Folder] Name (ID)
header_pattern = re.compile(r"^\[(.*?)\]\s+(.*?)\s+\(([0-9a-fA-F-]+)\)$")
for block in blocks:
if "_.:._" not in block:
continue
header, _, body = block.partition("\n")
raw_header = header.replace("_.:._", "").strip()
json_str, _, _ = body.partition("_.:._")
json_str = json_str.strip()
if not json_str:
continue
try:
flow_data = json.loads(json_str)
flow_type = flow_data.get("type", "unknown")
# Use the uncorrupted hierarchical path stored inside the JSON object
folder_name = flow_data.get("folderName", "Root")
match = header_pattern.match(raw_header)
if match:
_, flow_name, flow_id = match.groups()
else:
flow_name = raw_header
flow_id = flow_data.get("id", "unknown")
if flow_type == "standard":
base_dir = STD_DIR
extension = ".homeystdflow"
elif flow_type == "advanced":
base_dir = ADV_DIR
extension = ".homeyadvflow"
else:
base_dir = "."
extension = ".json"
# Split the hierarchical path by " / " to build true nested subfolders
folder_parts = folder_name.split(" / ")
clean_parts = [
"".join(c if c.isalnum() or c in "._-" else "_" for c in part).strip("_")
for part in folder_parts
]
clean_parts = [p if p else "Root" for p in clean_parts]
# Build the nested directory path safely
target_dir = os.path.join(base_dir, *clean_parts)
os.makedirs(target_dir, exist_ok=True)
clean_name = "".join(c if c.isalnum() or c in "._-" else "_" for c in flow_name).strip("_")
# Base filename using name
filename = os.path.join(target_dir, f"{clean_name}{extension}")
# Fallback conflict resolution: if filename exists, append incremental counter
counter = 1
base_filename = filename
while os.path.exists(filename):
counter += 1
name_part, ext_part = os.path.splitext(base_filename)
filename = f"{name_part}_{counter}{ext_part}"
with open(filename, "w", encoding="utf-8") as out_f:
json.dump(flow_data, out_f, indent=2, ensure_ascii=False)
print(f"Saved: {filename}")
count += 1
except json.JSONDecodeError as e:
print(f"Failed to parse flow '{raw_header}': {e}")
except Exception as e:
continue
print(f"\nDone! Successfully processed {count} flows.")