Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ __pycache__/
*.venv/
env/
venv/

*.log
# Node
node_modules/
npm-debug.log
Expand Down
4,472 changes: 0 additions & 4,472 deletions app.log

This file was deleted.

18 changes: 0 additions & 18 deletions config/system_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,6 @@
{
"name": "TasmotaRelayDriver",
"driver_class": "MQTTTasmotaRelayDriver",
"connection_info": {
"server_name": "TasmotaRelayDriver"
},
"datapoints": [
{ "name": "RELAY_1_STATUS", "type": "ON_OFF" },
{ "name": "RELAY_2_STATUS", "type": "ON_OFF" }
Expand Down Expand Up @@ -142,9 +139,6 @@
},
{
"command_datapoints": [],
"connection_info": {
"server_name": "OPCUAServerDriver"
},
"datapoints": [
{
"name": "CameraDriver@CAMERA_1_POSITION",
Expand Down Expand Up @@ -193,9 +187,6 @@
"type": "CAMERA_POSITION"
}
],
"connection_info": {
"server_name": "CameraDriver"
},
"datapoints": [
{
"name": "CAMERA_1_POSITION",
Expand Down Expand Up @@ -240,9 +231,6 @@
"type": "START_STOP_CMD"
}
],
"connection_info": {
"server_name": "WaterTank"
},
"datapoints": [
{
"name": "TANK",
Expand Down Expand Up @@ -271,9 +259,6 @@
"type": "START_STOP_CMD"
}
],
"connection_info": {
"server_name": "StressTest"
},
"datapoints": [
{
"name": "TEST",
Expand Down Expand Up @@ -1297,9 +1282,6 @@
"type": "SWITCH_CONTROL_CMD"
}
],
"connection_info": {
"server_name": "TrainTestDriver"
},
"datapoints": [
{
"name": "RIGHT_SWITCH_CONTROL",
Expand Down
6 changes: 3 additions & 3 deletions src/openscada_lite/common/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def get_streams(self):
"""
return self._config.get("streams", [])

def _get_svg_folder(self) -> str:
def get_svg_folder(self) -> str:
"""
Internal: Returns the SVG folder path from config or defaults to './svg'.
"""
Expand All @@ -213,7 +213,7 @@ def get_svg_files(self) -> list:
if svg_files:
return svg_files
logger.debug("No svg_files in config, scanning folder.")
svg_folder = self._get_svg_folder()
svg_folder = self.get_svg_folder()
if not os.path.exists(svg_folder):
logger.debug(f"SVG folder does not exist: {svg_folder}")
# folder missing — return empty list instead of crashing
Expand All @@ -226,7 +226,7 @@ def get_animation_datapoint_map(self) -> dict:
Parses all SVG files and returns a map:
{datapoint_identifier: [(svg_name, element_id, animation_type), ...]}
"""
svg_folder = self._get_svg_folder()
svg_folder = self.get_svg_folder()
svg_files = self.get_svg_files()
datapoint_map = {}
for fname in svg_files:
Expand Down
8 changes: 4 additions & 4 deletions src/openscada_lite/modules/animation/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __init__(self, model, socketio, module_name: str, router: APIRouter):

# Load SVG files from config
self.svg_files = Config.get_instance().get_svg_files()
self.svg_folder = Config.get_instance().get_svg_folder()

def register_local_routes(self, router: APIRouter):
@router.get("/animation/svgs", tags=[self.base_event], operation_id="getSvgs")
Expand All @@ -65,10 +66,9 @@ async def list_svgs():
},
)
async def svg(filename: str):
logger.debug(f"Requested SVG file: {filename}")
svg_dir = Path(__file__).parent.parent.parent.parent.parent / "config" / "svg"
logger.debug(f"SVG directory: {svg_dir}")
file = svg_dir / filename
logger.debug(f"Requested SVG file: {filename}")
logger.debug(f"SVG directory: {self.svg_folder}")
file = Path(self.svg_folder) / filename
if file.exists():
return FileResponse(file, media_type="text/plain") # Ensure correct media type
return JSONResponse(content={"error": "File not found"}, status_code=404)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def __init__(self):
driver_cls = DRIVER_REGISTRY.get(cfg["driver_class"])
if not driver_cls:
raise ValueError(f"Unknown driver class: {cfg['driver_class']}")
driver_instance: DriverProtocol = driver_cls(**cfg.get("connection_info", {}))
driver_instance: DriverProtocol = driver_cls(cfg['name'])
driver_instance.initialize(cfg.get("params", {}))
driver_instance.subscribe(datapoint_objs)
self.driver_instances[cfg["name"]] = driver_instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import AnimationsTab from "./components/AnimationsTab";
import GisIconsTab from "./components/GisIconsTab";
import StreamsTab from "./components/StreamsTab";
import FrontendTab from "./components/FrontendTab";
import SchedulerTab from "./components/SchedulerTab";
import { Api, ContentType } from "generatedApi";

// Lazy-load AnimationTestTab
Expand Down Expand Up @@ -240,7 +241,8 @@ export default function App() {
'GIS Icons',
'Animation Test',
'Streams',
'Frontend' // <-- Add this line
'Frontend',
'Scheduler' // <-- Add this line
]}
active={activeTab}
onChange={setActiveTab}
Expand Down Expand Up @@ -270,6 +272,9 @@ export default function App() {
<div className={activeTab === 'Frontend' ? 'tab-content active' : 'tab-content'}>
<FrontendTab config={config} setConfig={c => { setConfig(c); setDirty(true); }} />
</div>
<div className={activeTab === 'Scheduler' ? 'tab-content active' : 'tab-content'}>
<SchedulerTab config={config} setConfig={c => { setConfig(c); setDirty(true); }} />
</div>
{activeTab === 'Animation Test' && (
<Suspense fallback={<div style={{padding: 40, textAlign: "center"}}>Loading Animation Test...</div>}>
<AnimationTestTab />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,22 +96,20 @@ export default function DriversTab({ config, setConfig }) {
<label>Driver Class:</label>
<input value={drivers[selected].driver_class || ''} onChange={e => updateField('driver_class', e.target.value)} />

<label>Connection Info:</label>
<div style={{ border: '1px solid #ccc', padding: 6, borderRadius: 4 }}>
{Object.entries(drivers[selected].connection_info || {}).map(([k, v]) => (
<div key={k} style={{ display: 'flex', gap: 4, marginBottom: 4, alignItems: 'center' }}>
<input style={{ flex: 1 }} value={k} disabled={k === 'server_name'} />
<input style={{ flex: 2 }} value={v} onChange={e => updateConnectionInfo(k, e.target.value)} />
{k !== 'server_name' && <button onClick={() => removeConnectionKey(k)}>Del</button>}
</div>
))}
<button onClick={() => {
const key = prompt('Key:');
if (!key || key === 'server_name') return;
const value = prompt('Value:');
updateConnectionInfo(key, value);
}}>+ param</button>
</div>
<label>Params (JSON):</label>
<textarea
style={{ width: '100%', minHeight: 120, fontFamily: 'monospace' }}
value={JSON.stringify(drivers[selected].params || {}, null, 2)}
onChange={e => {
let val = e.target.value;
try {
const parsed = JSON.parse(val);
updateField('params', parsed);
} catch {
// Optionally show error or ignore until valid JSON
}
}}
/>
</div>

<div style={{ marginTop: 12 }}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React, { useState, useEffect } from "react";

export default function SchedulerTab({ config, setConfig }) {
// Find the schedule module config
const module = (config.modules || []).find(m => m.name === "schedule");
const schedules = module?.config?.schedules || [];
const [selectedIdx, setSelectedIdx] = useState(null);
const [editing, setEditing] = useState(null);

useEffect(() => {
if (selectedIdx !== null && schedules[selectedIdx]) {
setEditing({ ...schedules[selectedIdx] });
} else {
setEditing(null);
}
}, [selectedIdx, schedules]);

function saveSchedule() {
if (selectedIdx === null || !editing) return;
const copy = structuredClone(config);
const mod = copy.modules.find(m => m.name === "schedule");
mod.config.schedules[selectedIdx] = editing;
setConfig(copy);
}

function addSchedule() {
const copy = structuredClone(config);
const mod = copy.modules.find(m => m.name === "schedule");
if (!mod.config.schedules) mod.config.schedules = [];
mod.config.schedules.push({
schedule_id: "new_schedule",
cron: "0 0 * * *",
actions: []
});
setConfig(copy);
setSelectedIdx(mod.config.schedules.length - 1);
}

function removeSchedule() {
if (selectedIdx === null) return;
if (!window.confirm("Delete selected schedule?")) return;
const copy = structuredClone(config);
const mod = copy.modules.find(m => m.name === "schedule");
mod.config.schedules.splice(selectedIdx, 1);
setConfig(copy);
setSelectedIdx(null);
}

function updateField(field, value) {
setEditing(prev => ({ ...prev, [field]: value }));
}

function updateAction(idx, value) {
setEditing(prev => {
const actions = [...prev.actions];
actions[idx] = value;
return { ...prev, actions };
});
}

function addAction() {
setEditing(prev => ({
...prev,
actions: [...(prev.actions || []), ""]
}));
}

function removeAction(idx) {
setEditing(prev => ({
...prev,
actions: prev.actions.filter((_, i) => i !== idx)
}));
}

if (!module) return <div style={{ padding: 12 }}>No schedule module found in config.</div>;

return (
<div style={{ padding: 12 }}>
<div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
<div style={{ flex: 1 }}>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th style={{ borderBottom: "1px solid #ccc" }}>Schedule ID</th>
<th style={{ borderBottom: "1px solid #ccc" }}>Cron</th>
<th style={{ borderBottom: "1px solid #ccc" }}>Actions</th>
</tr>
</thead>
<tbody>
{schedules.map((s, i) => (
<tr
key={s.schedule_id}
style={{
background: selectedIdx === i ? "#e3f2fd" : undefined,
cursor: "pointer"
}}
onClick={() => setSelectedIdx(i)}
>
<td>{s.schedule_id}</td>
<td>{s.cron}</td>
<td>{(s.actions || []).length}</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{ width: 120, display: "flex", flexDirection: "column", gap: 6 }}>
<button onClick={addSchedule}>+</button>
<button onClick={removeSchedule} disabled={selectedIdx === null}>-</button>
</div>
</div>
{editing && (
<div style={{ marginTop: 12 }}>
<h3>Edit Schedule: {editing.schedule_id}</h3>
<div style={{ display: "grid", gridTemplateColumns: "120px 1fr", gap: 8 }}>
<label>Schedule ID:</label>
<input
value={editing.schedule_id}
onChange={e => updateField("schedule_id", e.target.value)}
onBlur={saveSchedule}
/>
<label>Cron:</label>
<input
value={editing.cron}
onChange={e => updateField("cron", e.target.value)}
onBlur={saveSchedule}
placeholder="e.g. 0 17 * * *"
/>
<label>Actions:</label>
<div>
{(editing.actions || []).map((a, idx) => (
<div key={idx} style={{ display: "flex", gap: 4, marginBottom: 4 }}>
<input
style={{ flex: 1 }}
value={a}
onChange={e => updateAction(idx, e.target.value)}
onBlur={saveSchedule}
/>
<button onClick={() => removeAction(idx)}>-</button>
</div>
))}
<button onClick={addAction}>Add Action</button>
</div>
</div>
</div>
)}
</div>
);
}
Loading