Add security classification, alarm detection, historical data access, and primitive grouping
Wire Galaxy security_classification to OPC UA AccessLevel (ReadOnly for SecuredWrite/VerifiedWrite/ViewOnly). Use deployed package chain for attribute queries to exclude undeployed attributes. Group primitive attributes under their parent variable node (merged Variable+Object). Add is_historized and is_alarm detection via HistoryExtension/AlarmExtension primitives. Implement OPC UA HistoryRead backed by Wonderware Historian Runtime database. Implement AlarmConditionState nodes driven by InAlarm with condition refresh support. Add historyread and alarms CLI commands for testing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,22 @@ Example for `TestMachine_001.MachineID` (`is_array=0`):
|
||||
- ValueRank: -1
|
||||
- ArrayDimensions: (not set)
|
||||
|
||||
## Security Classification
|
||||
|
||||
Galaxy attributes have a `security_classification` column that controls the access level required for writes. The attributes query returns this value for each attribute.
|
||||
|
||||
| security_classification | Galaxy Level | OPC UA Access | Description |
|
||||
|-------------------------|--------------|---------------|-------------|
|
||||
| 0 | FreeAccess | ReadWrite | No security restrictions |
|
||||
| 1 | Operate | ReadWrite | Normal operating level (default) |
|
||||
| 2 | SecuredWrite | ReadOnly | Requires elevated write access |
|
||||
| 3 | VerifiedWrite | ReadOnly | Requires verified/confirmed write access |
|
||||
| 4 | Tune | ReadWrite | Tuning-level access |
|
||||
| 5 | Configure | ReadWrite | Configuration-level access |
|
||||
| 6 | ViewOnly | ReadOnly | Read-only, no writes permitted |
|
||||
|
||||
Most attributes default to `Operate` (1). Higher values indicate more restrictive write access. `ViewOnly` (6) attributes should be exposed as read-only in OPC UA (`AccessLevel = CurrentRead` only, no `CurrentWrite`).
|
||||
|
||||
## DateTime Conversion
|
||||
|
||||
Galaxy `Time` (mx_data_type=6) stores DateTime values. OPC UA DateTime is defined as the number of 100-nanosecond intervals since January 1, 1601 (UTC). Ensure the conversion accounts for:
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
-- Galaxy Object User-Defined Attributes/Tags for OPC UA Server
|
||||
-- Returns user-defined (dynamic) attributes for automation objects.
|
||||
-- Returns user-defined (dynamic) attributes for deployed automation objects.
|
||||
-- These are the attributes defined on templates and inherited by instances
|
||||
-- via the derived_from_gobject_id chain (e.g., MachineID, MoveInFlag).
|
||||
-- via the deployed package derivation chain (e.g., MachineID, MoveInFlag).
|
||||
--
|
||||
-- Use full_tag_reference for read/write operations against the runtime.
|
||||
-- Join with hierarchy.sql results on gobject_id to place attributes in the OPC UA browse tree.
|
||||
--
|
||||
-- For system/primitive attributes as well, see attributes_extended.sql.
|
||||
--
|
||||
-- Only attributes that existed at deploy time are included. The CTE walks
|
||||
-- package.derived_from_package_id starting from each instance's deployed_package_id,
|
||||
-- then joins dynamic_attribute on package_id to filter out post-deploy additions.
|
||||
-- When the same attribute appears at multiple levels, only the shallowest
|
||||
-- (most-derived) version is kept.
|
||||
--
|
||||
-- Historization detection: an attribute is historized when a primitive_instance
|
||||
-- with a matching name exists in the deployed package chain and its primitive_definition
|
||||
-- has primitive_name = 'HistoryExtension'.
|
||||
--
|
||||
-- Array dimensions are extracted from the mx_value hex string on the template's
|
||||
-- dynamic_attribute row (bytes 5-6, little-endian uint16 at hex positions 13-16).
|
||||
--
|
||||
@@ -16,48 +26,98 @@
|
||||
-- 5 = String, 6 = Time (DateTime), 7 = ElapsedTime (TimeSpan),
|
||||
-- 8 = (reference), 13 = (enumeration), 14 = (custom), 15 = InternationalizedString, 16 = (custom)
|
||||
|
||||
;WITH template_chain AS (
|
||||
-- Start from each non-template instance
|
||||
SELECT g.gobject_id, g.derived_from_gobject_id, 0 AS depth
|
||||
;WITH deployed_package_chain AS (
|
||||
-- Start from each deployed instance's deployed package
|
||||
SELECT
|
||||
g.gobject_id,
|
||||
p.package_id,
|
||||
p.derived_from_package_id,
|
||||
0 AS depth
|
||||
FROM gobject g
|
||||
INNER JOIN package p
|
||||
ON p.package_id = g.deployed_package_id
|
||||
WHERE g.is_template = 0
|
||||
AND g.deployed_package_id <> 0
|
||||
UNION ALL
|
||||
-- Walk up the template derivation chain
|
||||
SELECT tc.gobject_id, t.derived_from_gobject_id, tc.depth + 1
|
||||
FROM template_chain tc
|
||||
INNER JOIN gobject t ON t.gobject_id = tc.derived_from_gobject_id
|
||||
WHERE tc.derived_from_gobject_id <> 0 AND tc.depth < 10
|
||||
-- Walk up the package derivation chain
|
||||
SELECT
|
||||
dpc.gobject_id,
|
||||
p.package_id,
|
||||
p.derived_from_package_id,
|
||||
dpc.depth + 1
|
||||
FROM deployed_package_chain dpc
|
||||
INNER JOIN package p
|
||||
ON p.package_id = dpc.derived_from_package_id
|
||||
WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10
|
||||
)
|
||||
SELECT DISTINCT
|
||||
g.gobject_id,
|
||||
g.tag_name,
|
||||
da.attribute_name,
|
||||
g.tag_name + '.' + da.attribute_name
|
||||
+ CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END
|
||||
AS full_tag_reference,
|
||||
da.mx_data_type,
|
||||
dt.description AS data_type_name,
|
||||
da.is_array,
|
||||
CASE WHEN da.is_array = 1
|
||||
THEN CONVERT(int, CONVERT(varbinary(2),
|
||||
SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2))
|
||||
ELSE NULL
|
||||
END AS array_dimension,
|
||||
da.mx_attribute_category,
|
||||
da.security_classification
|
||||
FROM template_chain tc
|
||||
INNER JOIN dynamic_attribute da
|
||||
ON da.gobject_id = tc.derived_from_gobject_id
|
||||
INNER JOIN gobject g
|
||||
ON g.gobject_id = tc.gobject_id
|
||||
INNER JOIN template_definition td
|
||||
ON td.template_definition_id = g.template_definition_id
|
||||
LEFT JOIN data_type dt
|
||||
ON dt.mx_data_type = da.mx_data_type
|
||||
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
|
||||
AND g.is_template = 0
|
||||
AND g.deployed_package_id <> 0
|
||||
AND da.attribute_name NOT LIKE '[_]%'
|
||||
AND da.attribute_name NOT LIKE '%.Description'
|
||||
AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24)
|
||||
ORDER BY g.tag_name, da.attribute_name;
|
||||
SELECT
|
||||
gobject_id,
|
||||
tag_name,
|
||||
attribute_name,
|
||||
full_tag_reference,
|
||||
mx_data_type,
|
||||
data_type_name,
|
||||
is_array,
|
||||
array_dimension,
|
||||
mx_attribute_category,
|
||||
security_classification,
|
||||
is_historized,
|
||||
is_alarm
|
||||
FROM (
|
||||
SELECT
|
||||
dpc.gobject_id,
|
||||
g.tag_name,
|
||||
da.attribute_name,
|
||||
g.tag_name + '.' + da.attribute_name
|
||||
+ CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END
|
||||
AS full_tag_reference,
|
||||
da.mx_data_type,
|
||||
dt.description AS data_type_name,
|
||||
da.is_array,
|
||||
CASE WHEN da.is_array = 1
|
||||
THEN CONVERT(int, CONVERT(varbinary(2),
|
||||
SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2))
|
||||
ELSE NULL
|
||||
END AS array_dimension,
|
||||
da.mx_attribute_category,
|
||||
da.security_classification,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM deployed_package_chain dpc2
|
||||
INNER JOIN primitive_instance pi
|
||||
ON pi.package_id = dpc2.package_id
|
||||
AND pi.primitive_name = da.attribute_name
|
||||
INNER JOIN primitive_definition pd
|
||||
ON pd.primitive_definition_id = pi.primitive_definition_id
|
||||
AND pd.primitive_name = 'HistoryExtension'
|
||||
WHERE dpc2.gobject_id = dpc.gobject_id
|
||||
) THEN 1 ELSE 0 END AS is_historized,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM deployed_package_chain dpc2
|
||||
INNER JOIN primitive_instance pi
|
||||
ON pi.package_id = dpc2.package_id
|
||||
AND pi.primitive_name = da.attribute_name
|
||||
INNER JOIN primitive_definition pd
|
||||
ON pd.primitive_definition_id = pi.primitive_definition_id
|
||||
AND pd.primitive_name = 'AlarmExtension'
|
||||
WHERE dpc2.gobject_id = dpc.gobject_id
|
||||
) THEN 1 ELSE 0 END AS is_alarm,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY dpc.gobject_id, da.attribute_name
|
||||
ORDER BY dpc.depth
|
||||
) AS rn
|
||||
FROM deployed_package_chain dpc
|
||||
INNER JOIN dynamic_attribute da
|
||||
ON da.package_id = dpc.package_id
|
||||
INNER JOIN gobject g
|
||||
ON g.gobject_id = dpc.gobject_id
|
||||
INNER JOIN template_definition td
|
||||
ON td.template_definition_id = g.template_definition_id
|
||||
LEFT JOIN data_type dt
|
||||
ON dt.mx_data_type = da.mx_data_type
|
||||
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
|
||||
AND da.attribute_name NOT LIKE '[_]%'
|
||||
AND da.attribute_name NOT LIKE '%.Description'
|
||||
AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24)
|
||||
) ranked
|
||||
WHERE rn = 1
|
||||
ORDER BY tag_name, attribute_name;
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
-- Galaxy Object Attributes/Tags for OPC UA Server
|
||||
-- Returns all runtime-readable attributes for automation objects.
|
||||
-- Returns all runtime-readable attributes for deployed automation objects.
|
||||
-- Use full_tag_reference for read/write operations against the runtime.
|
||||
-- Join with hierarchy.sql results on gobject_id to place attributes in the OPC UA browse tree.
|
||||
--
|
||||
-- Two sources of attributes:
|
||||
-- 1. attribute_definition (via primitive_instance) — system/primitive attributes
|
||||
-- Derived from internal_runtime_attributes view logic.
|
||||
-- Joined via the instance's deployed_package_id to exclude undeployed changes.
|
||||
-- 2. dynamic_attribute — user-defined attributes (e.g., MachineID, MoveInFlag)
|
||||
-- Defined on templates, inherited by instances via derived_from_gobject_id chain.
|
||||
-- Requires recursive CTE to walk the template derivation hierarchy.
|
||||
-- Walked via the deployed package derivation chain (package.derived_from_package_id)
|
||||
-- to only include attributes that existed at deploy time.
|
||||
-- When the same attribute appears at multiple levels (e.g., instance override and
|
||||
-- base template), only the shallowest (most-derived) version is kept.
|
||||
--
|
||||
-- Historization detection: a dynamic attribute is historized when a primitive_instance
|
||||
-- with a matching name exists in the deployed package chain and its primitive_definition
|
||||
-- has primitive_name = 'HistoryExtension'.
|
||||
--
|
||||
-- Attribute category filter (mx_attribute_category):
|
||||
-- 2-11, 24 = runtime readable attributes
|
||||
@@ -24,19 +30,88 @@
|
||||
-- 5 = String, 6 = Time (DateTime), 7 = ElapsedTime (TimeSpan),
|
||||
-- 8 = (reference), 13 = (enumeration), 14 = (custom), 15 = InternationalizedString, 16 = (custom)
|
||||
|
||||
;WITH template_chain AS (
|
||||
-- Start from each non-template instance
|
||||
SELECT g.gobject_id, g.derived_from_gobject_id, 0 AS depth
|
||||
;WITH deployed_package_chain AS (
|
||||
-- Start from each deployed instance's deployed package
|
||||
SELECT
|
||||
g.gobject_id,
|
||||
p.package_id,
|
||||
p.derived_from_package_id,
|
||||
0 AS depth
|
||||
FROM gobject g
|
||||
INNER JOIN package p
|
||||
ON p.package_id = g.deployed_package_id
|
||||
WHERE g.is_template = 0
|
||||
AND g.deployed_package_id <> 0
|
||||
UNION ALL
|
||||
-- Walk up the template derivation chain
|
||||
SELECT tc.gobject_id, t.derived_from_gobject_id, tc.depth + 1
|
||||
FROM template_chain tc
|
||||
INNER JOIN gobject t ON t.gobject_id = tc.derived_from_gobject_id
|
||||
WHERE tc.derived_from_gobject_id <> 0 AND tc.depth < 10
|
||||
-- Walk up the package derivation chain
|
||||
SELECT
|
||||
dpc.gobject_id,
|
||||
p.package_id,
|
||||
p.derived_from_package_id,
|
||||
dpc.depth + 1
|
||||
FROM deployed_package_chain dpc
|
||||
INNER JOIN package p
|
||||
ON p.package_id = dpc.derived_from_package_id
|
||||
WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10
|
||||
),
|
||||
-- Rank dynamic attributes: shallowest (most-derived) wins per object + attribute
|
||||
ranked_dynamic AS (
|
||||
SELECT
|
||||
dpc.gobject_id,
|
||||
g.tag_name,
|
||||
da.attribute_name,
|
||||
g.tag_name + '.' + da.attribute_name
|
||||
+ CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END
|
||||
AS full_tag_reference,
|
||||
da.mx_data_type,
|
||||
dt.description AS data_type_name,
|
||||
da.is_array,
|
||||
CASE WHEN da.is_array = 1
|
||||
THEN CONVERT(int, CONVERT(varbinary(2),
|
||||
SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2))
|
||||
ELSE NULL
|
||||
END AS array_dimension,
|
||||
da.mx_attribute_category,
|
||||
da.security_classification,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM deployed_package_chain dpc2
|
||||
INNER JOIN primitive_instance pi
|
||||
ON pi.package_id = dpc2.package_id
|
||||
AND pi.primitive_name = da.attribute_name
|
||||
INNER JOIN primitive_definition pd
|
||||
ON pd.primitive_definition_id = pi.primitive_definition_id
|
||||
AND pd.primitive_name = 'HistoryExtension'
|
||||
WHERE dpc2.gobject_id = dpc.gobject_id
|
||||
) THEN 1 ELSE 0 END AS is_historized,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM deployed_package_chain dpc2
|
||||
INNER JOIN primitive_instance pi
|
||||
ON pi.package_id = dpc2.package_id
|
||||
AND pi.primitive_name = da.attribute_name
|
||||
INNER JOIN primitive_definition pd
|
||||
ON pd.primitive_definition_id = pi.primitive_definition_id
|
||||
AND pd.primitive_name = 'AlarmExtension'
|
||||
WHERE dpc2.gobject_id = dpc.gobject_id
|
||||
) THEN 1 ELSE 0 END AS is_alarm,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY dpc.gobject_id, da.attribute_name
|
||||
ORDER BY dpc.depth
|
||||
) AS rn
|
||||
FROM deployed_package_chain dpc
|
||||
INNER JOIN dynamic_attribute da
|
||||
ON da.package_id = dpc.package_id
|
||||
INNER JOIN gobject g
|
||||
ON g.gobject_id = dpc.gobject_id
|
||||
INNER JOIN template_definition td
|
||||
ON td.template_definition_id = g.template_definition_id
|
||||
LEFT JOIN data_type dt
|
||||
ON dt.mx_data_type = da.mx_data_type
|
||||
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
|
||||
AND da.attribute_name NOT LIKE '[_]%'
|
||||
AND da.attribute_name NOT LIKE '%.Description'
|
||||
AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24)
|
||||
)
|
||||
SELECT DISTINCT
|
||||
SELECT
|
||||
gobject_id,
|
||||
tag_name,
|
||||
primitive_name,
|
||||
@@ -48,9 +123,11 @@ SELECT DISTINCT
|
||||
array_dimension,
|
||||
mx_attribute_category,
|
||||
security_classification,
|
||||
is_historized,
|
||||
is_alarm,
|
||||
attribute_source
|
||||
FROM (
|
||||
-- Part 1: System/primitive attributes (from attribute_definition)
|
||||
-- Part 1: System/primitive attributes (from attribute_definition via deployed package)
|
||||
SELECT
|
||||
g.gobject_id,
|
||||
g.tag_name,
|
||||
@@ -71,6 +148,8 @@ FROM (
|
||||
END AS array_dimension,
|
||||
ad.mx_attribute_category,
|
||||
ad.security_classification,
|
||||
CAST(0 AS int) AS is_historized,
|
||||
CAST(0 AS int) AS is_alarm,
|
||||
'primitive' AS attribute_source
|
||||
FROM gobject g
|
||||
INNER JOIN instance i
|
||||
@@ -79,7 +158,7 @@ FROM (
|
||||
ON td.template_definition_id = g.template_definition_id
|
||||
AND td.runtime_clsid <> '{00000000-0000-0000-0000-000000000000}'
|
||||
INNER JOIN package p
|
||||
ON p.package_id = g.checked_in_package_id
|
||||
ON p.package_id = g.deployed_package_id
|
||||
INNER JOIN primitive_instance pi
|
||||
ON pi.package_id = p.package_id
|
||||
AND pi.property_bitmask & 0x10 <> 0x10
|
||||
@@ -95,40 +174,23 @@ FROM (
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Part 2: User-defined attributes (from dynamic_attribute via template chain)
|
||||
-- Part 2: User-defined attributes (shallowest override from deployed package chain)
|
||||
SELECT
|
||||
g.gobject_id,
|
||||
g.tag_name,
|
||||
gobject_id,
|
||||
tag_name,
|
||||
'' AS primitive_name,
|
||||
da.attribute_name,
|
||||
g.tag_name + '.' + da.attribute_name
|
||||
+ CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END
|
||||
AS full_tag_reference,
|
||||
da.mx_data_type,
|
||||
dt.description AS data_type_name,
|
||||
da.is_array,
|
||||
CASE WHEN da.is_array = 1
|
||||
THEN CONVERT(int, CONVERT(varbinary(2),
|
||||
SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2))
|
||||
ELSE NULL
|
||||
END AS array_dimension,
|
||||
da.mx_attribute_category,
|
||||
da.security_classification,
|
||||
attribute_name,
|
||||
full_tag_reference,
|
||||
mx_data_type,
|
||||
data_type_name,
|
||||
is_array,
|
||||
array_dimension,
|
||||
mx_attribute_category,
|
||||
security_classification,
|
||||
is_historized,
|
||||
is_alarm,
|
||||
'dynamic' AS attribute_source
|
||||
FROM template_chain tc
|
||||
INNER JOIN dynamic_attribute da
|
||||
ON da.gobject_id = tc.derived_from_gobject_id
|
||||
INNER JOIN gobject g
|
||||
ON g.gobject_id = tc.gobject_id
|
||||
INNER JOIN template_definition td
|
||||
ON td.template_definition_id = g.template_definition_id
|
||||
LEFT JOIN data_type dt
|
||||
ON dt.mx_data_type = da.mx_data_type
|
||||
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
|
||||
AND g.is_template = 0
|
||||
AND g.deployed_package_id <> 0
|
||||
AND da.attribute_name NOT LIKE '[_]%'
|
||||
AND da.attribute_name NOT LIKE '%.Description'
|
||||
AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24)
|
||||
FROM ranked_dynamic
|
||||
WHERE rn = 1
|
||||
) all_attributes
|
||||
ORDER BY tag_name, primitive_name, attribute_name;
|
||||
|
||||
Reference in New Issue
Block a user