Save local fertigungski changes
This commit is contained in:
@@ -12,6 +12,23 @@ CREATE TABLE IF NOT EXISTS material_consumption_snapshots (
|
||||
CREATE INDEX IF NOT EXISTS material_consumption_snapshots_machine_time_idx
|
||||
ON material_consumption_snapshots (calculation_id, machine_id, timestamp);
|
||||
|
||||
-- Channel identities are intentionally independent of material assignment.
|
||||
CREATE TABLE IF NOT EXISTS channel_material_consumption_snapshots (
|
||||
timestamp timestamptz NOT NULL,
|
||||
calculation_id text NOT NULL,
|
||||
machine_id text NOT NULL,
|
||||
extruder text NOT NULL,
|
||||
doser_channel text NOT NULL,
|
||||
production_order text NOT NULL,
|
||||
run_id text NOT NULL,
|
||||
cumulative_consumption_kg double precision NOT NULL,
|
||||
material_number text NULL,
|
||||
material_name text NULL,
|
||||
material_mapping_status text NOT NULL CHECK (material_mapping_status IN ('ASSIGNED', 'UNAVAILABLE', 'AMBIGUOUS', 'UNMAPPED')),
|
||||
percentage_sum_valid boolean NOT NULL,
|
||||
PRIMARY KEY (calculation_id, machine_id, extruder, doser_channel, production_order, timestamp)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS material_efficiency_snapshots (
|
||||
erp_feedback_timestamp timestamptz NOT NULL,
|
||||
material_snapshot_timestamp timestamptz NOT NULL,
|
||||
@@ -70,3 +87,42 @@ CREATE TABLE IF NOT EXISTS power_meter_monthly_reports (
|
||||
generated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (meter, month)
|
||||
);
|
||||
|
||||
-- Mutable ENLYZE downtime source records. external_id is the stable ENLYZE UUID.
|
||||
CREATE TABLE IF NOT EXISTS production_downtime_events (
|
||||
external_id text PRIMARY KEY,
|
||||
machine_id text NOT NULL,
|
||||
production_order text NULL,
|
||||
source_type text NOT NULL,
|
||||
source_start timestamptz NOT NULL,
|
||||
source_end timestamptz NULL,
|
||||
attributed_start timestamptz NULL,
|
||||
attributed_end timestamptz NULL,
|
||||
attributed_duration_seconds double precision NULL,
|
||||
reason_id text NULL,
|
||||
reason_name text NULL,
|
||||
reason_description text NULL,
|
||||
reason_group text NULL,
|
||||
category text NOT NULL CHECK (category IN ('PLANNED', 'UNPLANNED', 'UNKNOWN')),
|
||||
comment text NULL,
|
||||
source_updated_at timestamptz NULL,
|
||||
last_reconciled_at timestamptz NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS production_downtime_events_machine_attributed_time_idx
|
||||
ON production_downtime_events (machine_id, attributed_start);
|
||||
CREATE INDEX IF NOT EXISTS production_downtime_events_production_order_idx
|
||||
ON production_downtime_events (production_order);
|
||||
CREATE INDEX IF NOT EXISTS production_downtime_events_category_idx
|
||||
ON production_downtime_events (category);
|
||||
CREATE INDEX IF NOT EXISTS production_downtime_events_unresolved_idx
|
||||
ON production_downtime_events (machine_id, source_start)
|
||||
WHERE source_end IS NULL OR category = 'UNKNOWN';
|
||||
|
||||
-- One observed ERP order boundary per machine, used to clip open ENLYZE downtimes.
|
||||
CREATE TABLE IF NOT EXISTS production_order_attribution_state (
|
||||
machine_id text PRIMARY KEY,
|
||||
production_order text NOT NULL,
|
||||
started_at timestamptz NOT NULL,
|
||||
ended_at timestamptz NULL
|
||||
);
|
||||
|
||||
@@ -128,10 +128,25 @@ def _parser() -> argparse.ArgumentParser:
|
||||
monthly.add_argument("--meter")
|
||||
monthly.add_argument("--month", required=True, help="YYYY-MM")
|
||||
monthly.add_argument("--db-secrets-file", type=Path, default=Path("secrets/postgres.env"))
|
||||
|
||||
channel_material = runners.add_parser(
|
||||
"channel-material-poll", help="Continuously poll channel-level material consumption"
|
||||
)
|
||||
channel_material.add_argument("--config", type=Path, required=True)
|
||||
channel_material.add_argument("--poll-interval-seconds", type=float, default=10.0)
|
||||
channel_material.add_argument(
|
||||
"--state-directory", type=Path, default=Path("data/state/channel-material")
|
||||
)
|
||||
channel_material.add_argument("--secrets-file", type=Path, default=Path("secrets/enlyze.env"))
|
||||
channel_material.add_argument("--once", action="store_true", help="Run one poll cycle and exit")
|
||||
efficiency = runners.add_parser("material-efficiency", help="Persist ERP feedback KPI points")
|
||||
efficiency.add_argument("--config", type=Path, required=True)
|
||||
efficiency.add_argument("--secrets-file", type=Path, default=Path("secrets/enlyze.env"))
|
||||
efficiency.add_argument("--erp-secrets-file", type=Path, default=Path("secrets/erp.env"))
|
||||
downtime = runners.add_parser("downtime-reconcile", help="Reconcile ENLYZE downtime events")
|
||||
downtime.add_argument("--config", type=Path, required=True)
|
||||
downtime.add_argument("--secrets-file", type=Path, default=Path("secrets/enlyze.env"))
|
||||
downtime.add_argument("--erp-secrets-file", type=Path, default=Path("secrets/erp.env"))
|
||||
return parser
|
||||
|
||||
|
||||
@@ -174,6 +189,35 @@ def _run_material(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _run_channel_material(args: argparse.Namespace) -> int:
|
||||
from production_analytics.calculations.channel_config import load_channel_material_calculation
|
||||
from production_analytics.calculations.config import CalculationConfigError
|
||||
from production_analytics.enlyze.exploration import ConfigurationError
|
||||
from production_analytics.service.channel_material_runtime import build_channel_material_runner
|
||||
|
||||
try:
|
||||
calculation = load_channel_material_calculation(args.config)
|
||||
runner = build_channel_material_runner(
|
||||
calculation,
|
||||
poll_interval_seconds=args.poll_interval_seconds,
|
||||
state_directory=args.state_directory,
|
||||
secrets_file=args.secrets_file,
|
||||
)
|
||||
if args.once:
|
||||
runner.run_once()
|
||||
else:
|
||||
runner.run()
|
||||
except (CalculationConfigError, ConfigurationError) as exc:
|
||||
print(f"Config/startup error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Startup/runtime error: {type(exc).__name__}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _run_material_efficiency(args: argparse.Namespace) -> int:
|
||||
from production_analytics.calculations.config import CalculationConfigError
|
||||
from production_analytics.enlyze.exploration import ConfigurationError
|
||||
@@ -263,13 +307,32 @@ def _run_monthly_report(args: argparse.Namespace) -> int:
|
||||
return 2
|
||||
|
||||
|
||||
def _run_downtime(args: argparse.Namespace) -> int:
|
||||
from production_analytics.service.downtime_runtime import build_downtime_runner
|
||||
|
||||
try:
|
||||
build_downtime_runner(
|
||||
args.config,
|
||||
secrets_file=args.secrets_file,
|
||||
erp_secrets_file=args.erp_secrets_file,
|
||||
).run()
|
||||
except Exception as exc:
|
||||
print(f"Config/startup/runtime error: {type(exc).__name__}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
args = _parser().parse_args(argv)
|
||||
if args.namespace == "run":
|
||||
if args.command == "channel-material-poll":
|
||||
return _run_channel_material(args)
|
||||
if args.command == "material-efficiency":
|
||||
return _run_material_efficiency(args)
|
||||
if args.command == "power-meter":
|
||||
return _run_power_meter(args)
|
||||
if args.command == "downtime-reconcile":
|
||||
return _run_downtime(args)
|
||||
return _run_material(args)
|
||||
if args.namespace == "report":
|
||||
return _run_monthly_report(args)
|
||||
|
||||
Reference in New Issue
Block a user