feat(fusion_iot): add setpoint/optimum + deviation to sensor schema
Sensors previously only tracked alarm thresholds (alert_min/alert_max). Missing the third piece of standard process control: the SETPOINT — what the heater/chiller controls toward and what dashboards compare against. Without it an operator can't tell whether 89°C is "on target" or "barely still in spec". Schema changes: **fusion.plating.bath.parameter** (shop-wide default) - New `target_value` field — the default setpoint for this parameter across the shop (e.g. 87°C for ENP bath). Parallel to existing target_min / target_max. **fp.tank.sensor** (per-sensor override) - New `target_value_override` — per-sensor override, zero = inherit from parameter. Matches the existing override pattern for alert thresholds so users can fine-tune per-tank without touching the shop-wide spec. - New `effective_target` / `effective_target_unit` computed — resolves override → parameter default, converts to company-preferred unit. - New `_get_setpoint()` helper for internal use. **fp.tank.reading** - New `deviation_from_target` — signed Δ from the sensor's effective setpoint, in the company's preferred unit. Positive = above, negative = below, zero if no setpoint defined. - New `deviation_band` (selection: on/near/far/out/none) — coarse band for fast visual scanning. `on` = within ±1° of target, `near` = ±3°, `far` = beyond, `out` = actually out of the alarm band. **Views** - Sensor form: split the alerting panel into two groups — "Target (setpoint)" on the left, "Alarm band" on the right. Makes the distinction between "where we want to be" and "where we'd panic" visually obvious. - Reading list: new Δ + band columns, with decoration classes (success/info/warning/danger) so the list reads at a glance. - Tank form Sensors tab: inline setpoint + unit column. Seeded: parameter "Bath Temperature (Hot Process)" now carries target_value=87°C as a realistic ENP shop default. Sensors inherit unless they set their own override. Design decisions kept simple: - Did NOT add a warning band (warn_min/warn_max). Two-tier model (setpoint + alarm band) is enough for the pilot. Can add soft warnings later as a separate commit if ops wants them. - Did NOT auto-control heaters. Setpoint is stored as metadata only; actual heater actuation via IoT is a future phase C project. Verified: setpoint 87°C stored → displays 188.60°F on the live pilot sensor (company pref = F). Each incoming reading correctly computes signed deviation; bands colour the reading list appropriately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — IoT Integration',
|
'name': 'Fusion Plating — IoT Integration',
|
||||||
'version': '19.0.0.2.0',
|
'version': '19.0.0.3.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Wire physical tank sensors to Fusion Plating — live '
|
'summary': 'Wire physical tank sensors to Fusion Plating — live '
|
||||||
'temperature / chemistry readings with auto quality holds '
|
'temperature / chemistry readings with auto quality holds '
|
||||||
|
|||||||
@@ -95,6 +95,66 @@ class FpTankReading(models.Model):
|
|||||||
r.display_value = r.value
|
r.display_value = r.value
|
||||||
r.display_unit = r.parameter_id.uom or ''
|
r.display_unit = r.parameter_id.uom or ''
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Deviation from setpoint — signed Δ from the sensor's effective target
|
||||||
|
# in the company-preferred unit. Zero if no setpoint defined.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
deviation_from_target = fields.Float(
|
||||||
|
string='Δ from Setpoint',
|
||||||
|
compute='_compute_deviation',
|
||||||
|
digits=(12, 2),
|
||||||
|
help='Signed difference between this reading and the sensor\'s '
|
||||||
|
'setpoint, in the company-preferred unit. Positive = above '
|
||||||
|
'setpoint, negative = below. Zero if no setpoint is defined.',
|
||||||
|
)
|
||||||
|
deviation_band = fields.Selection(
|
||||||
|
[
|
||||||
|
('none', 'No setpoint'),
|
||||||
|
('on', 'On target (±1°)'),
|
||||||
|
('near', 'Near target (±3°)'),
|
||||||
|
('far', 'Far from target (>3°)'),
|
||||||
|
('out', 'Out of spec'),
|
||||||
|
],
|
||||||
|
string='Band',
|
||||||
|
compute='_compute_deviation',
|
||||||
|
help='Coarse band for quick visual scanning in lists.',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('value', 'display_value', 'in_spec',
|
||||||
|
'sensor_id', 'sensor_id.target_value_override',
|
||||||
|
'sensor_id.parameter_id.target_value',
|
||||||
|
'sensor_id.parameter_id.parameter_type')
|
||||||
|
def _compute_deviation(self):
|
||||||
|
pref = self.env.company.x_fc_default_temp_uom or 'C'
|
||||||
|
for r in self:
|
||||||
|
sensor = r.sensor_id
|
||||||
|
if not sensor:
|
||||||
|
r.deviation_from_target = 0.0
|
||||||
|
r.deviation_band = 'none'
|
||||||
|
continue
|
||||||
|
raw_sp = sensor._get_setpoint()
|
||||||
|
if not raw_sp:
|
||||||
|
r.deviation_from_target = 0.0
|
||||||
|
r.deviation_band = 'none'
|
||||||
|
continue
|
||||||
|
# Convert setpoint + value to display unit for the delta so the
|
||||||
|
# number shown matches what operators expect (°F if pref=F).
|
||||||
|
ptype = (sensor.parameter_id.parameter_type or '').lower()
|
||||||
|
if ptype == 'temperature' and pref == 'F':
|
||||||
|
sp_disp = raw_sp * 9.0 / 5.0 + 32.0
|
||||||
|
else:
|
||||||
|
sp_disp = raw_sp
|
||||||
|
delta = r.display_value - sp_disp
|
||||||
|
r.deviation_from_target = round(delta, 2)
|
||||||
|
if not r.in_spec:
|
||||||
|
r.deviation_band = 'out'
|
||||||
|
elif abs(delta) <= 1.0:
|
||||||
|
r.deviation_band = 'on'
|
||||||
|
elif abs(delta) <= 3.0:
|
||||||
|
r.deviation_band = 'near'
|
||||||
|
else:
|
||||||
|
r.deviation_band = 'far'
|
||||||
|
|
||||||
source = fields.Selection(
|
source = fields.Selection(
|
||||||
[
|
[
|
||||||
('iot_proxy', 'IoT Proxy (Pi)'),
|
('iot_proxy', 'IoT Proxy (Pi)'),
|
||||||
|
|||||||
@@ -76,12 +76,24 @@ class FpTankSensor(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Alerting behaviour
|
# Target / alerting behaviour — three concepts:
|
||||||
|
# setpoint → what we CONTROL TOWARD (dashboards, PID, trend baseline)
|
||||||
|
# alert min → lower alarm boundary (quality hold fires below)
|
||||||
|
# alert max → upper alarm boundary (quality hold fires above)
|
||||||
|
#
|
||||||
|
# All three have a parameter-level default + per-sensor override
|
||||||
|
# pattern so users can fine-tune per-tank without touching the
|
||||||
|
# shop-wide spec. Zero on an override field = "inherit from parameter".
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
target_value_override = fields.Float(
|
||||||
|
string='Setpoint / Optimum (override)', digits=(10, 4),
|
||||||
|
help='Optional per-sensor override of the parameter\'s default '
|
||||||
|
'setpoint. Leave 0 to inherit from bath.parameter.target_value.',
|
||||||
|
)
|
||||||
alert_on_out_of_spec = fields.Boolean(
|
alert_on_out_of_spec = fields.Boolean(
|
||||||
string='Alert on Out-of-Spec', default=True,
|
string='Alert on Out-of-Spec', default=True,
|
||||||
help='If checked, a fusion.plating.quality.hold is auto-created '
|
help='If checked, a fusion.plating.quality.hold is auto-created '
|
||||||
'when a reading falls outside the parameter target range.',
|
'when a reading falls outside the alert_min/alert_max band.',
|
||||||
)
|
)
|
||||||
alert_min_override = fields.Float(
|
alert_min_override = fields.Float(
|
||||||
string='Alert Min (override)', digits=(10, 4),
|
string='Alert Min (override)', digits=(10, 4),
|
||||||
@@ -94,6 +106,37 @@ class FpTankSensor(models.Model):
|
|||||||
'specific sensor.',
|
'specific sensor.',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Effective target — resolves override → parameter default → 0
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
effective_target = fields.Float(
|
||||||
|
string='Effective Setpoint',
|
||||||
|
compute='_compute_effective_target',
|
||||||
|
digits=(12, 2),
|
||||||
|
help='The setpoint currently in use, after applying any per-sensor '
|
||||||
|
'override. Displayed in the company-preferred unit.',
|
||||||
|
)
|
||||||
|
effective_target_unit = fields.Char(
|
||||||
|
string='Setpoint Unit',
|
||||||
|
compute='_compute_effective_target',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('target_value_override', 'parameter_id.target_value',
|
||||||
|
'parameter_id.parameter_type', 'parameter_id.uom')
|
||||||
|
def _compute_effective_target(self):
|
||||||
|
pref = self.env.company.x_fc_default_temp_uom or 'C'
|
||||||
|
for rec in self:
|
||||||
|
raw = rec.target_value_override or (
|
||||||
|
rec.parameter_id.target_value if rec.parameter_id else 0.0
|
||||||
|
)
|
||||||
|
ptype = (rec.parameter_id.parameter_type or '').lower()
|
||||||
|
if ptype == 'temperature' and pref == 'F':
|
||||||
|
rec.effective_target = raw * 9.0 / 5.0 + 32.0 if raw else 0.0
|
||||||
|
rec.effective_target_unit = '°F'
|
||||||
|
else:
|
||||||
|
rec.effective_target = raw
|
||||||
|
rec.effective_target_unit = rec.parameter_id.uom or ''
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Cached latest-reading fields (for quick display in list views)
|
# Cached latest-reading fields (for quick display in list views)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -163,6 +206,17 @@ class FpTankSensor(models.Model):
|
|||||||
)
|
)
|
||||||
return (lo or 0.0, hi or 0.0)
|
return (lo or 0.0, hi or 0.0)
|
||||||
|
|
||||||
|
def _get_setpoint(self):
|
||||||
|
"""Canonical (raw) setpoint used for deviation calcs.
|
||||||
|
|
||||||
|
Returns 0.0 if no setpoint is set at either level — callers should
|
||||||
|
treat 0 as "no target defined, skip deviation display".
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
return self.target_value_override or (
|
||||||
|
self.parameter_id.target_value if self.parameter_id else 0.0
|
||||||
|
) or 0.0
|
||||||
|
|
||||||
def action_view_readings(self):
|
def action_view_readings(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -10,15 +10,22 @@
|
|||||||
<field name="model">fp.tank.reading</field>
|
<field name="model">fp.tank.reading</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list string="Sensor Readings"
|
<list string="Sensor Readings"
|
||||||
decoration-danger="not in_spec" default_order="reading_at desc">
|
default_order="reading_at desc"
|
||||||
|
decoration-danger="deviation_band == 'out'"
|
||||||
|
decoration-warning="deviation_band == 'far'"
|
||||||
|
decoration-info="deviation_band == 'near'"
|
||||||
|
decoration-success="deviation_band == 'on'"
|
||||||
|
decoration-muted="deviation_band == 'none'">
|
||||||
<field name="reading_at"/>
|
<field name="reading_at"/>
|
||||||
<field name="sensor_id"/>
|
<field name="sensor_id"/>
|
||||||
<field name="tank_id" optional="show"/>
|
<field name="tank_id" optional="show"/>
|
||||||
<field name="parameter_id" optional="hide"/>
|
<field name="parameter_id" optional="hide"/>
|
||||||
<field name="display_value"/>
|
<field name="display_value"/>
|
||||||
<field name="display_unit"/>
|
<field name="display_unit"/>
|
||||||
|
<field name="deviation_from_target"/>
|
||||||
|
<field name="deviation_band" widget="badge"/>
|
||||||
<field name="value" optional="hide" string="Value (°C raw)"/>
|
<field name="value" optional="hide" string="Value (°C raw)"/>
|
||||||
<field name="in_spec" widget="boolean_toggle"/>
|
<field name="in_spec" widget="boolean_toggle" optional="show"/>
|
||||||
<field name="source" optional="hide"/>
|
<field name="source" optional="hide"/>
|
||||||
<field name="hold_id" optional="show"/>
|
<field name="hold_id" optional="show"/>
|
||||||
</list>
|
</list>
|
||||||
|
|||||||
@@ -69,13 +69,19 @@
|
|||||||
<field name="parameter_id" options="{'no_create': True}"/>
|
<field name="parameter_id" options="{'no_create': True}"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group string="Alerting">
|
<group string="Setpoint & Alerting">
|
||||||
<group>
|
<group string="Target (setpoint)">
|
||||||
|
<field name="target_value_override"
|
||||||
|
help="Leave 0 to inherit from bath parameter's Default Setpoint. This is the IDEAL operating value — not an alarm threshold."/>
|
||||||
|
<field name="effective_target" readonly="1"/>
|
||||||
|
<field name="effective_target_unit" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Alarm band">
|
||||||
<field name="alert_on_out_of_spec"/>
|
<field name="alert_on_out_of_spec"/>
|
||||||
<field name="alert_min_override"
|
<field name="alert_min_override"
|
||||||
help="Leave 0 to inherit from the bath parameter's target_min."/>
|
help="Leave 0 to inherit from the bath parameter's target_min. Readings below this fire a quality hold."/>
|
||||||
<field name="alert_max_override"
|
<field name="alert_max_override"
|
||||||
help="Leave 0 to inherit from the bath parameter's target_max."/>
|
help="Leave 0 to inherit from the bath parameter's target_max. Readings above this fire a quality hold."/>
|
||||||
</group>
|
</group>
|
||||||
<group string="Most Recent Reading">
|
<group string="Most Recent Reading">
|
||||||
<field name="last_reading_display" readonly="1"/>
|
<field name="last_reading_display" readonly="1"/>
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
<field name="device_kind"/>
|
<field name="device_kind"/>
|
||||||
<field name="device_serial"/>
|
<field name="device_serial"/>
|
||||||
<field name="parameter_id"/>
|
<field name="parameter_id"/>
|
||||||
|
<field name="effective_target" readonly="1"/>
|
||||||
|
<field name="effective_target_unit" readonly="1"/>
|
||||||
<field name="last_reading_display"/>
|
<field name="last_reading_display"/>
|
||||||
<field name="last_reading_display_unit"/>
|
<field name="last_reading_display_unit"/>
|
||||||
<field name="last_reading_at" readonly="1"/>
|
<field name="last_reading_at" readonly="1"/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating',
|
'name': 'Fusion Plating',
|
||||||
'version': '19.0.5.4.0',
|
'version': '19.0.5.5.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ class FpBathParameter(models.Model):
|
|||||||
string='Default Target Max',
|
string='Default Target Max',
|
||||||
help='Default target maximum. Per-bath overrides are allowed.',
|
help='Default target maximum. Per-bath overrides are allowed.',
|
||||||
)
|
)
|
||||||
|
target_value = fields.Float(
|
||||||
|
string='Default Setpoint / Optimum',
|
||||||
|
help='The IDEAL operating value — what the heater/chiller controls '
|
||||||
|
'toward, what dashboards compare against. Sits between '
|
||||||
|
'target_min and target_max. Per-sensor override via '
|
||||||
|
'fp.tank.sensor.target_value_override.',
|
||||||
|
)
|
||||||
warning_tolerance = fields.Float(
|
warning_tolerance = fields.Float(
|
||||||
string='Warning Tolerance %',
|
string='Warning Tolerance %',
|
||||||
default=10.0,
|
default=10.0,
|
||||||
|
|||||||
Reference in New Issue
Block a user