Add Bento 1 fresh bentonite consumption
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
"""ERP nominal-width resolution for an explicitly matched production order."""
|
||||
|
||||
from production_analytics.context import build_enlyze_production_order, extract_nominal_width_m
|
||||
from production_analytics.erp import ErpWorkplaceStatusGateway
|
||||
|
||||
|
||||
class ErpNominalWidthProvider:
|
||||
def __init__(
|
||||
self, gateway: ErpWorkplaceStatusGateway, *, workplace: str, format_template: str,
|
||||
) -> None:
|
||||
self.gateway = gateway
|
||||
self.workplace = workplace
|
||||
self.format_template = format_template
|
||||
|
||||
def __call__(self, production_order: str) -> float:
|
||||
status = self.gateway.get_current_workplace_status(self.workplace)
|
||||
if (status is None or status.workplace != self.workplace
|
||||
or build_enlyze_production_order(status.production_order, self.format_template)
|
||||
!= production_order):
|
||||
raise ValueError("ERP context does not match the production order")
|
||||
width = extract_nominal_width_m(status.article_description)
|
||||
if width is None:
|
||||
raise ValueError("ERP article description has no unambiguous nominal width")
|
||||
return width
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Single-cycle live material polling orchestration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Protocol
|
||||
@@ -53,7 +54,13 @@ class MaterialPollingService:
|
||||
gate_variable_id: str,
|
||||
gate_threshold: float,
|
||||
max_sample_gap_seconds: float,
|
||||
application_variable_ids: tuple[str, ...] = (),
|
||||
nominal_width_provider: Callable[[str], float] | None = None,
|
||||
) -> None:
|
||||
self._application_variable_ids = application_variable_ids
|
||||
self._nominal_width_provider = nominal_width_provider
|
||||
if application_variable_ids and nominal_width_provider is None:
|
||||
raise ValueError("area application requires a nominal width provider")
|
||||
self._gateway = gateway
|
||||
self._state_store = state_store
|
||||
self._machine_id = machine_id
|
||||
@@ -71,6 +78,8 @@ class MaterialPollingService:
|
||||
if run is None or now < run.start:
|
||||
return None
|
||||
|
||||
width = (self._nominal_width_provider(run.production_order)
|
||||
if self._application_variable_ids else None)
|
||||
saved_polling_state = self._state_store.load(
|
||||
self._machine_id,
|
||||
run.production_order,
|
||||
@@ -91,7 +100,7 @@ class MaterialPollingService:
|
||||
if previous.end is None or previous.end > run.start:
|
||||
raise ValueError("Historical production run overlaps the current open run")
|
||||
integrated = self._integrate(
|
||||
initial_state, start=previous.start, end=previous.end,
|
||||
initial_state, start=previous.start, end=previous.end, width=width,
|
||||
)
|
||||
initial_state = MaterialIntegrationState(
|
||||
cumulative_consumption_kg=integrated.cumulative_consumption_kg,
|
||||
@@ -116,7 +125,7 @@ class MaterialPollingService:
|
||||
if now < start:
|
||||
return None
|
||||
|
||||
state = self._integrate(initial_state, start=start, end=now)
|
||||
state = self._integrate(initial_state, start=start, end=now, width=width)
|
||||
|
||||
self._state_store.save(
|
||||
self._machine_id,
|
||||
@@ -134,13 +143,20 @@ class MaterialPollingService:
|
||||
|
||||
def _integrate(
|
||||
self, initial_state: MaterialIntegrationState, *, start: datetime, end: datetime,
|
||||
width: float | None = None,
|
||||
) -> MaterialIntegrationState:
|
||||
integrator = MaterialConsumptionIntegrator(self._config, initial_state=initial_state)
|
||||
source_options = {}
|
||||
if self._application_variable_ids:
|
||||
source_options = dict(
|
||||
application_variable_ids=self._application_variable_ids, nominal_width_m=width,
|
||||
)
|
||||
samples = self._gateway.get_material_samples(
|
||||
machine_id=self._machine_id,
|
||||
rate_variable_id=self._rate_variable_id,
|
||||
gate_variable_id=self._gate_variable_id,
|
||||
start=start,
|
||||
end=end,
|
||||
**source_options,
|
||||
)
|
||||
return integrator.process_many(samples)
|
||||
|
||||
@@ -14,6 +14,8 @@ from production_analytics.enlyze.exploration import (
|
||||
load_secret_file,
|
||||
)
|
||||
from production_analytics.enlyze.gateway import EnlyzeApiGateway
|
||||
from production_analytics.erp import ErpSettings, ErpWorkplaceStatusGateway
|
||||
from production_analytics.service.material_context import ErpNominalWidthProvider
|
||||
from production_analytics.service.material_polling import (
|
||||
MaterialPollingService,
|
||||
MaterialPollingState,
|
||||
@@ -48,6 +50,7 @@ class _ReportingStateStore:
|
||||
def build_material_runner(
|
||||
calculation: MaterialCalculationConfig, *, poll_interval_seconds: float,
|
||||
state_directory: Path, secrets_file: Path,
|
||||
erp_secrets_file: Path = Path("secrets/erp.env"),
|
||||
) -> MaterialPollingRunner:
|
||||
finite_number(poll_interval_seconds, "poll interval", positive=True)
|
||||
environment = dict(os.environ)
|
||||
@@ -69,6 +72,16 @@ def build_material_runner(
|
||||
f"State directory startup check failed: {type(exc).__name__}"
|
||||
) from exc
|
||||
postgres_settings = PostgresSettings.from_environment(environment)
|
||||
source_options = {}
|
||||
if calculation.source_mode == "area_application":
|
||||
source_options = dict(
|
||||
application_variable_ids=calculation.application_signal_refs,
|
||||
nominal_width_provider=ErpNominalWidthProvider(
|
||||
ErpWorkplaceStatusGateway(ErpSettings.from_secret_file(erp_secrets_file)),
|
||||
workplace=calculation.erp_workplace,
|
||||
format_template=calculation.production_order_format,
|
||||
),
|
||||
)
|
||||
service = MaterialPollingService(
|
||||
gateway=EnlyzeApiGateway(ExplorationClient(settings)),
|
||||
state_store=_ReportingStateStore(JsonMaterialStateStore(state_directory)),
|
||||
@@ -77,6 +90,7 @@ def build_material_runner(
|
||||
gate_variable_id=calculation.gate_signal_ref,
|
||||
gate_threshold=calculation.gate_threshold,
|
||||
max_sample_gap_seconds=calculation.max_sample_gap_seconds,
|
||||
**source_options,
|
||||
)
|
||||
return MaterialPollingRunner(
|
||||
service, calculation_id=calculation.id,
|
||||
|
||||
Reference in New Issue
Block a user