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:
gsinghpal
2026-04-20 16:36:58 -04:00
parent 089cda71fe
commit 118f96dad4
8 changed files with 146 additions and 10 deletions

View File

@@ -95,6 +95,66 @@ class FpTankReading(models.Model):
r.display_value = r.value
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(
[
('iot_proxy', 'IoT Proxy (Pi)'),