diff --git a/src/ScadaLink.Commons/Types/Flattening/FlattenedConfiguration.cs b/src/ScadaLink.Commons/Types/Flattening/FlattenedConfiguration.cs
index 8548af8..500d6d2 100644
--- a/src/ScadaLink.Commons/Types/Flattening/FlattenedConfiguration.cs
+++ b/src/ScadaLink.Commons/Types/Flattening/FlattenedConfiguration.cs
@@ -126,4 +126,12 @@ public sealed record ResolvedScript
public TimeSpan? MinTimeBetweenRuns { get; init; }
public string Source { get; init; } = "Template";
+
+ ///
+ /// Where the script sits in the composition tree. Seeded into ScriptGlobals
+ /// so the script's Attributes / Children / Parent
+ /// accessors resolve canonical names with the right path-prefix.
+ ///
+ public ScadaLink.Commons.Types.Scripts.ScriptScope Scope { get; init; }
+ = ScadaLink.Commons.Types.Scripts.ScriptScope.Root;
}
diff --git a/src/ScadaLink.Commons/Types/Scripts/ScriptScope.cs b/src/ScadaLink.Commons/Types/Scripts/ScriptScope.cs
new file mode 100644
index 0000000..fd2a930
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/Scripts/ScriptScope.cs
@@ -0,0 +1,17 @@
+namespace ScadaLink.Commons.Types.Scripts;
+
+///
+/// Where a compiled script sits in the composition tree. Computed at
+/// flattening time and seeded into the script's globals at execution time
+/// so Attributes["X"] / Parent.X can prepend the right
+/// path-prefix when delegating to the existing flat
+/// Instance.GetAttribute / Instance.SetAttribute /
+/// Instance.CallScript APIs.
+///
+public sealed record ScriptScope(string SelfPath, string? ParentPath)
+{
+ /// Scope for a script directly on the root template (no compositions).
+ public static readonly ScriptScope Root = new("", null);
+
+ public bool HasParent => ParentPath != null;
+}
diff --git a/src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs b/src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs
index 52e4339..0af75c2 100644
--- a/src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs
+++ b/src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs
@@ -38,6 +38,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
private TimeSpan? _minTimeBetweenRuns;
private DateTimeOffset _lastExecutionTime = DateTimeOffset.MinValue;
private int _executionCounter;
+ private readonly Commons.Types.Scripts.ScriptScope _scope;
public ITimerScheduler Timers { get; set; } = null!;
@@ -63,6 +64,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
_healthCollector = healthCollector;
_serviceProvider = serviceProvider;
_minTimeBetweenRuns = scriptConfig.MinTimeBetweenRuns;
+ _scope = scriptConfig.Scope;
// Parse trigger configuration
_triggerConfig = ParseTriggerConfig(scriptConfig.TriggerType, scriptConfig.TriggerConfiguration);
@@ -215,6 +217,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
replyTo,
correlationId,
_logger,
+ _scope,
_healthCollector,
_serviceProvider));
diff --git a/src/ScadaLink.SiteRuntime/Actors/ScriptExecutionActor.cs b/src/ScadaLink.SiteRuntime/Actors/ScriptExecutionActor.cs
index c5e4c99..f969afc 100644
--- a/src/ScadaLink.SiteRuntime/Actors/ScriptExecutionActor.cs
+++ b/src/ScadaLink.SiteRuntime/Actors/ScriptExecutionActor.cs
@@ -33,6 +33,7 @@ public class ScriptExecutionActor : ReceiveActor
IActorRef replyTo,
string correlationId,
ILogger logger,
+ Commons.Types.Scripts.ScriptScope scope,
ISiteHealthCollector? healthCollector = null,
IServiceProvider? serviceProvider = null)
{
@@ -43,7 +44,7 @@ public class ScriptExecutionActor : ReceiveActor
ExecuteScript(
scriptName, instanceName, compiledScript, parameters, callDepth,
instanceActor, sharedScriptLibrary, options, replyTo, correlationId,
- self, parent, logger, healthCollector, serviceProvider);
+ self, parent, logger, scope, healthCollector, serviceProvider);
}
private static void ExecuteScript(
@@ -60,6 +61,7 @@ public class ScriptExecutionActor : ReceiveActor
IActorRef self,
IActorRef parent,
ILogger logger,
+ Commons.Types.Scripts.ScriptScope scope,
ISiteHealthCollector? healthCollector,
IServiceProvider? serviceProvider)
{
@@ -102,7 +104,8 @@ public class ScriptExecutionActor : ReceiveActor
{
Instance = context,
Parameters = new ScriptParameters(parameters ?? new Dictionary()),
- CancellationToken = cts.Token
+ CancellationToken = cts.Token,
+ Scope = scope
};
var state = await compiledScript.RunAsync(globals, cts.Token);
diff --git a/src/ScadaLink.SiteRuntime/Scripts/ScopeAccessors.cs b/src/ScadaLink.SiteRuntime/Scripts/ScopeAccessors.cs
new file mode 100644
index 0000000..8b60f4b
--- /dev/null
+++ b/src/ScadaLink.SiteRuntime/Scripts/ScopeAccessors.cs
@@ -0,0 +1,105 @@
+namespace ScadaLink.SiteRuntime.Scripts;
+
+///
+/// Scope-aware view onto the instance's attributes, anchored at a path prefix.
+/// Attributes["X"] on the root scope resolves to canonical name "X";
+/// on a composition with prefix "TempSensor" it resolves to "TempSensor.X".
+/// Reads block on the actor Ask; async variants are provided for callers
+/// that prefer to await explicitly.
+///
+public class AttributeAccessor
+{
+ private readonly ScriptRuntimeContext _ctx;
+
+ /// Canonical-name prefix, e.g. "" for root or "TempSensor" for a composition.
+ public string ScopePrefix { get; }
+
+ public AttributeAccessor(ScriptRuntimeContext ctx, string prefix)
+ {
+ _ctx = ctx;
+ ScopePrefix = prefix;
+ }
+
+ public string Resolve(string key) =>
+ ScopePrefix.Length == 0 ? key : ScopePrefix + "." + key;
+
+ public object? this[string key]
+ {
+ get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult();
+ set => _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
+ }
+
+ public Task