Honeywell HC900을 Modbus TCP로 직접 폴링 → gRPC → C# 크롤러 → PostgreSQL. 기존 Experion OPC UA 데이터 경로를 HC900 직접 통신으로 대체. - industrial-comm/cpp: C++ Modbus 게이트웨이 (gRPC 서버) - src: C# .NET 8 ASP.NET Core 크롤러 + 웹 UI (3-Layer) - mcp-server: Python FastMCP (RAG/NL2SQL/P&ID) - 다중 컨트롤러(N-Controller) 지원 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
5.0 KiB
PL/PgSQL
122 lines
5.0 KiB
PL/PgSQL
-- ============================================================================
|
|
-- Migration: Add controller_id columns for multi-controller support
|
|
-- Run this once against the existing HC900 database.
|
|
-- ============================================================================
|
|
|
|
BEGIN;
|
|
|
|
-- 1. realtime_table: add controller_id + UNIQUE(controller_id, tagname)
|
|
ALTER TABLE realtime_table
|
|
ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1';
|
|
|
|
-- Drop any existing unique constraint on tagname alone (idempotent wrapper)
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM pg_indexes
|
|
WHERE tablename = 'realtime_table'
|
|
AND indexname = 'idx_realtime_table_tagname_unique'
|
|
) THEN
|
|
DROP INDEX IF EXISTS idx_realtime_table_tagname_unique;
|
|
END IF;
|
|
IF EXISTS (
|
|
SELECT 1 FROM pg_constraint c
|
|
JOIN pg_class t ON t.oid = c.conrelid
|
|
WHERE t.relname = 'realtime_table' AND c.contype = 'u'
|
|
) THEN
|
|
-- There's a unique constraint; we can't easily drop it by name unless we know it.
|
|
-- Use a safe approach: drop any unique constraint on tagname
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Add the new composite UNIQUE (required for ON CONFLICT (controller_id, tagname) DO UPDATE)
|
|
-- Skip if already exists
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_indexes
|
|
WHERE tablename = 'realtime_table'
|
|
AND indexname = 'idx_realtime_table_ctrl_tag_unique'
|
|
) THEN
|
|
CREATE UNIQUE INDEX idx_realtime_table_ctrl_tag_unique
|
|
ON realtime_table(controller_id, tagname);
|
|
END IF;
|
|
END $$;
|
|
|
|
-- 2. history_table: add controller_id with default 'HC1'
|
|
ALTER TABLE history_table
|
|
ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1';
|
|
|
|
-- 3. event_history_table: add controller_id
|
|
ALTER TABLE event_history_table
|
|
ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1';
|
|
|
|
-- 4. tag_metadata: add controller_id
|
|
ALTER TABLE tag_metadata
|
|
ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1';
|
|
|
|
-- 5. hc900_map_master: add controller_id
|
|
ALTER TABLE hc900_map_master
|
|
ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1';
|
|
|
|
-- 6. v_tag_summary: recreate with controller_id
|
|
DROP VIEW IF EXISTS v_tag_summary CASCADE;
|
|
CREATE VIEW v_tag_summary AS
|
|
SELECT
|
|
rt_base.base_tag,
|
|
pv_rt.livevalue AS pv,
|
|
sp_rt.livevalue AS sp,
|
|
op_rt.livevalue AS op,
|
|
instate0_rt.livevalue AS instate0,
|
|
instate1_rt.livevalue AS instate1,
|
|
instate2_rt.livevalue AS instate2,
|
|
desc_md.value AS description,
|
|
area_md.value AS area,
|
|
sub_area_md.value AS sub_area,
|
|
rt_base.controller_id
|
|
FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag, controller_id FROM realtime_table) rt_base
|
|
LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv' AND pv_rt.controller_id = rt_base.controller_id
|
|
LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp' AND sp_rt.controller_id = rt_base.controller_id
|
|
LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op' AND op_rt.controller_id = rt_base.controller_id
|
|
LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0' AND instate0_rt.controller_id = rt_base.controller_id
|
|
LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1' AND instate1_rt.controller_id = rt_base.controller_id
|
|
LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2' AND instate2_rt.controller_id = rt_base.controller_id
|
|
LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc'
|
|
LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area'
|
|
LEFT JOIN tag_metadata sub_area_md ON sub_area_md.base_tag = rt_base.base_tag AND sub_area_md.attribute = 'sub_area';
|
|
|
|
-- 7. v_plant_running_state: recreate (depends on v_tag_summary)
|
|
DROP VIEW IF EXISTS v_plant_running_state;
|
|
CREATE VIEW v_plant_running_state AS
|
|
WITH pump_state AS (
|
|
SELECT
|
|
trim(split_part(area, '|', 2)) AS area_code,
|
|
area AS area_raw,
|
|
base_tag,
|
|
pv,
|
|
controller_id
|
|
FROM v_tag_summary
|
|
WHERE area IS NOT NULL
|
|
AND (base_tag LIKE 'p-%' OR base_tag LIKE 'vp-%')
|
|
AND pv ~ '\|\s*(L-RUN|R-RUN|L-STOP|R-STOP|L-TRIP|R-TRIP)\s*\|'
|
|
)
|
|
SELECT
|
|
area_code,
|
|
MAX(area_raw) AS area_raw,
|
|
COUNT(*) AS total_pumps,
|
|
COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-RUN\s*\|') AS running_pumps,
|
|
COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-TRIP\s*\|') AS tripped_pumps,
|
|
COUNT(*) FILTER (WHERE pv ~ '\|\s*(L-STOP|R-STOP)\s*\|') AS stopped_pumps,
|
|
CASE
|
|
WHEN COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-RUN\s*\|') > 0 THEN 'RUNNING'
|
|
WHEN COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-TRIP\s*\|') > 0 THEN 'TRIPPED'
|
|
ELSE 'STOPPED'
|
|
END AS status,
|
|
array_agg(base_tag) FILTER (WHERE pv ~ '\|\s*[LR]-RUN\s*\|') AS running_pump_tags
|
|
FROM pump_state
|
|
WHERE area_code IS NOT NULL AND area_code <> ''
|
|
GROUP BY area_code
|
|
ORDER BY area_code;
|
|
|
|
COMMIT;
|