Files
lmxopcua/gr/queries/attributes.sql
Joseph Doherty 415e62c585 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>
2026-03-26 11:32:33 -04:00

124 lines
4.8 KiB
SQL

-- Galaxy Object User-Defined Attributes/Tags for OPC UA Server
-- Returns user-defined (dynamic) attributes for deployed automation objects.
-- These are the attributes defined on templates and inherited by instances
-- 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).
--
-- Data types (mx_data_type):
-- 1 = Boolean, 2 = Integer (Int32), 3 = Float (Single), 4 = Double,
-- 5 = String, 6 = Time (DateTime), 7 = ElapsedTime (TimeSpan),
-- 8 = (reference), 13 = (enumeration), 14 = (custom), 15 = InternationalizedString, 16 = (custom)
;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 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
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;