chore(plating): de-dash fusion_plating_iot too
Same em-dash -> hyphen sweep applied to fusion_plating_iot (lives under fusion_iot/ so the main commit missed it). Comments/strings only; no functional dashes in this module. Keeps git in sync with the in-place de-dash already applied to entech. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,22 +4,22 @@
|
|||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — IoT Integration',
|
'name': 'Fusion Plating - IoT Integration',
|
||||||
'version': '19.0.2.0.0',
|
'version': '19.0.2.0.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 '
|
||||||
'on out-of-spec.',
|
'on out-of-spec.',
|
||||||
'description': """
|
'description': """
|
||||||
Fusion Plating — IoT Integration
|
Fusion Plating - IoT Integration
|
||||||
================================
|
================================
|
||||||
|
|
||||||
Bridges the generic `iot` module (IoT Box + device management) to
|
Bridges the generic `iot` module (IoT Box + device management) to
|
||||||
plating-specific models:
|
plating-specific models:
|
||||||
|
|
||||||
* ``fp.tank.sensor`` — maps an ``iot.device`` to a
|
* ``fp.tank.sensor`` - maps an ``iot.device`` to a
|
||||||
``fusion.plating.tank`` (or a ``fusion.plating.bath``).
|
``fusion.plating.tank`` (or a ``fusion.plating.bath``).
|
||||||
* ``fp.tank.reading`` — time-series log of every sensor reading.
|
* ``fp.tank.reading`` - time-series log of every sensor reading.
|
||||||
* Auto-creates a ``fusion.plating.quality.hold`` when a reading
|
* Auto-creates a ``fusion.plating.quality.hold`` when a reading
|
||||||
falls outside the tank/bath's target range (per
|
falls outside the tank/bath's target range (per
|
||||||
``fusion.plating.bath.parameter`` spec).
|
``fusion.plating.bath.parameter`` spec).
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ _logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def _parse_read_at(raw):
|
def _parse_read_at(raw):
|
||||||
"""Best-effort ISO-8601 parse — fall back to 'now' on garbage input."""
|
"""Best-effort ISO-8601 parse - fall back to 'now' on garbage input."""
|
||||||
from odoo.fields import Datetime as OdooDatetime
|
from odoo.fields import Datetime as OdooDatetime
|
||||||
if not raw:
|
if not raw:
|
||||||
return OdooDatetime.now()
|
return OdooDatetime.now()
|
||||||
@@ -62,20 +62,20 @@ class FpIotIngestController(http.Controller):
|
|||||||
methods=['POST'], csrf=False, save_session=False)
|
methods=['POST'], csrf=False, save_session=False)
|
||||||
def ingest(self, **_kwargs):
|
def ingest(self, **_kwargs):
|
||||||
"""Accept one-or-many sensor readings and land them in fp.tank.reading."""
|
"""Accept one-or-many sensor readings and land them in fp.tank.reading."""
|
||||||
# Pull the shared secret from config — configured at install via
|
# Pull the shared secret from config - configured at install via
|
||||||
# data/ir_config_parameter_data.xml, but admins can rotate it
|
# data/ir_config_parameter_data.xml, but admins can rotate it
|
||||||
# in Settings → Technical → System Parameters.
|
# in Settings → Technical → System Parameters.
|
||||||
expected = request.env['ir.config_parameter'].sudo().get_param(
|
expected = request.env['ir.config_parameter'].sudo().get_param(
|
||||||
'fusion_plating_iot.ingest_token', ''
|
'fusion_plating_iot.ingest_token', ''
|
||||||
)
|
)
|
||||||
if not expected:
|
if not expected:
|
||||||
_logger.warning('fp.iot.ingest: token not configured — all requests rejected')
|
_logger.warning('fp.iot.ingest: token not configured - all requests rejected')
|
||||||
return Response(
|
return Response(
|
||||||
json.dumps({'ok': False, 'error': 'token_not_configured'}),
|
json.dumps({'ok': False, 'error': 'token_not_configured'}),
|
||||||
status=503, content_type='application/json',
|
status=503, content_type='application/json',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Accept token via either header or payload body — some simple
|
# Accept token via either header or payload body - some simple
|
||||||
# sensors can't easily set custom headers.
|
# sensors can't easily set custom headers.
|
||||||
header_token = request.httprequest.headers.get('X-FP-IOT-Token', '')
|
header_token = request.httprequest.headers.get('X-FP-IOT-Token', '')
|
||||||
raw = request.httprequest.get_data(as_text=True) or ''
|
raw = request.httprequest.get_data(as_text=True) or ''
|
||||||
@@ -124,10 +124,10 @@ class FpIotIngestController(http.Controller):
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Sub 7 — per-sensor rate-limit. Drop readings that arrive
|
# Sub 7 - per-sensor rate-limit. Drop readings that arrive
|
||||||
# inside the sensor's effective polling interval so the Pi
|
# inside the sensor's effective polling interval so the Pi
|
||||||
# agent can happily poll every 30 s while the log only
|
# agent can happily poll every 30 s while the log only
|
||||||
# retains a row every 15–30 min. Inactive sensors also
|
# retains a row every 15-30 min. Inactive sensors also
|
||||||
# dropped so disabled tanks don't clutter the log.
|
# dropped so disabled tanks don't clutter the log.
|
||||||
if not sensor.active:
|
if not sensor.active:
|
||||||
skipped_interval += 1
|
skipped_interval += 1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Copyright 2026 Nexa Systems Inc.
|
Copyright 2026 Nexa Systems Inc.
|
||||||
License OPL-1
|
License OPL-1
|
||||||
|
|
||||||
Default sensor types — the common ones every plating shop needs.
|
Default sensor types - the common ones every plating shop needs.
|
||||||
noupdate="1" so admins can tweak without losing edits on module upgrade.
|
noupdate="1" so admins can tweak without losing edits on module upgrade.
|
||||||
-->
|
-->
|
||||||
<odoo noupdate="1">
|
<odoo noupdate="1">
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
"""Post-install hook — backfill new fields on existing live sensors.
|
"""Post-install hook - backfill new fields on existing live sensors.
|
||||||
|
|
||||||
Runs once on every install/upgrade. Idempotent: checks before writing
|
Runs once on every install/upgrade. Idempotent: checks before writing
|
||||||
so re-runs don't overwrite user-edited values.
|
so re-runs don't overwrite user-edited values.
|
||||||
|
|
||||||
What it does:
|
What it does:
|
||||||
1. Populates `uuid` on any fp.tank.sensor record that doesn't have one
|
1. Populates `uuid` on any fp.tank.sensor record that doesn't have one
|
||||||
(for sensors created BEFORE the uuid field existed — the create
|
(for sensors created BEFORE the uuid field existed - the create
|
||||||
override only covers new records).
|
override only covers new records).
|
||||||
2. Sets a default `sensor_type_id` on sensors that don't have one yet,
|
2. Sets a default `sensor_type_id` on sensors that don't have one yet,
|
||||||
inferring from `device_kind` (DS18B20 / PT100 / PT1000 → Temperature,
|
inferring from `device_kind` (DS18B20 / PT100 / PT1000 → Temperature,
|
||||||
@@ -78,7 +78,7 @@ def _backfill_sensor_types(env):
|
|||||||
|
|
||||||
|
|
||||||
def _seed_entech_tanks_and_sensors(env):
|
def _seed_entech_tanks_and_sensors(env):
|
||||||
"""Sub 7 — seed 25 tanks (5 small + 20 big; 10 big inactive) with
|
"""Sub 7 - seed 25 tanks (5 small + 20 big; 10 big inactive) with
|
||||||
one temperature and one pH sensor each.
|
one temperature and one pH sensor each.
|
||||||
|
|
||||||
Idempotent: tanks keyed by `code` so re-runs skip existing rows.
|
Idempotent: tanks keyed by `code` so re-runs skip existing rows.
|
||||||
@@ -103,7 +103,7 @@ def _seed_entech_tanks_and_sensors(env):
|
|||||||
|
|
||||||
facility = Facility.search([], limit=1)
|
facility = Facility.search([], limit=1)
|
||||||
if not facility:
|
if not facility:
|
||||||
_logger.warning('Sub 7 seed: no fusion.plating.facility found — '
|
_logger.warning('Sub 7 seed: no fusion.plating.facility found - '
|
||||||
'skipping tank seed. Create a facility first.')
|
'skipping tank seed. Create a facility first.')
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ def _seed_entech_tanks_and_sensors(env):
|
|||||||
)
|
)
|
||||||
if not temp_param or not ph_param:
|
if not temp_param or not ph_param:
|
||||||
_logger.warning('Sub 7 seed: temperature / pH bath parameters '
|
_logger.warning('Sub 7 seed: temperature / pH bath parameters '
|
||||||
'not found — skipping. Seed bath parameters first.')
|
'not found - skipping. Seed bath parameters first.')
|
||||||
return
|
return
|
||||||
|
|
||||||
plan = []
|
plan = []
|
||||||
@@ -147,7 +147,7 @@ def _seed_entech_tanks_and_sensors(env):
|
|||||||
('parameter_id.parameter_type', '=', 'temperature'),
|
('parameter_id.parameter_type', '=', 'temperature'),
|
||||||
]):
|
]):
|
||||||
Sensor.create({
|
Sensor.create({
|
||||||
'name': '%s — Temperature' % tank.name,
|
'name': '%s - Temperature' % tank.name,
|
||||||
'tank_id': tank.id,
|
'tank_id': tank.id,
|
||||||
'parameter_id': temp_param.id,
|
'parameter_id': temp_param.id,
|
||||||
'device_kind': 'ds18b20',
|
'device_kind': 'ds18b20',
|
||||||
@@ -160,7 +160,7 @@ def _seed_entech_tanks_and_sensors(env):
|
|||||||
('parameter_id.parameter_type', '=', 'ph'),
|
('parameter_id.parameter_type', '=', 'ph'),
|
||||||
]):
|
]):
|
||||||
Sensor.create({
|
Sensor.create({
|
||||||
'name': '%s — pH' % tank.name,
|
'name': '%s - pH' % tank.name,
|
||||||
'tank_id': tank.id,
|
'tank_id': tank.id,
|
||||||
'parameter_id': ph_param.id,
|
'parameter_id': ph_param.id,
|
||||||
'device_kind': 'ph',
|
'device_kind': 'ph',
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
"""Sensor dashboard — a named group of related sensors.
|
"""Sensor dashboard - a named group of related sensors.
|
||||||
|
|
||||||
Use case: a shop wants one "ENP Line — Tanks 1-4" view that aggregates
|
Use case: a shop wants one "ENP Line - Tanks 1-4" view that aggregates
|
||||||
temperature, pH, and level probes from four bath tanks into a single
|
temperature, pH, and level probes from four bath tanks into a single
|
||||||
trending chart + alert count. A dashboard is just a logical grouping;
|
trending chart + alert count. A dashboard is just a logical grouping;
|
||||||
actual rendering happens in the UI.
|
actual rendering happens in the UI.
|
||||||
@@ -14,7 +14,7 @@ from odoo import api, fields, models
|
|||||||
|
|
||||||
class FpSensorDashboard(models.Model):
|
class FpSensorDashboard(models.Model):
|
||||||
_name = 'fp.sensor.dashboard'
|
_name = 'fp.sensor.dashboard'
|
||||||
_description = 'Fusion Plating — Sensor Dashboard'
|
_description = 'Fusion Plating - Sensor Dashboard'
|
||||||
_inherit = ['mail.thread']
|
_inherit = ['mail.thread']
|
||||||
_order = 'name'
|
_order = 'name'
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class FpSensorDashboard(models.Model):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.act_window',
|
'type': 'ir.actions.act_window',
|
||||||
'name': f'Sensors — {self.name}',
|
'name': f'Sensors - {self.name}',
|
||||||
'res_model': 'fp.tank.sensor',
|
'res_model': 'fp.tank.sensor',
|
||||||
'view_mode': 'list,form',
|
'view_mode': 'list,form',
|
||||||
'domain': [('id', 'in', self.sensor_ids.ids)],
|
'domain': [('id', 'in', self.sensor_ids.ids)],
|
||||||
@@ -63,7 +63,7 @@ class FpSensorDashboard(models.Model):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.act_window',
|
'type': 'ir.actions.act_window',
|
||||||
'name': f'Readings — {self.name}',
|
'name': f'Readings - {self.name}',
|
||||||
'res_model': 'fp.tank.reading',
|
'res_model': 'fp.tank.reading',
|
||||||
'view_mode': 'graph,list,form',
|
'view_mode': 'graph,list,form',
|
||||||
'domain': [('sensor_id', 'in', self.sensor_ids.ids)],
|
'domain': [('sensor_id', 'in', self.sensor_ids.ids)],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
A richer taxonomy than the `device_kind` Selection on fp.tank.sensor.
|
A richer taxonomy than the `device_kind` Selection on fp.tank.sensor.
|
||||||
Types describe WHAT the sensor measures (temperature, pH, conductivity,
|
Types describe WHAT the sensor measures (temperature, pH, conductivity,
|
||||||
level, etc.) plus the data type of its readings. Same type can back
|
level, etc.) plus the data type of its readings. Same type can back
|
||||||
multiple hardware models — e.g. "Temperature" covers DS18B20, PT100,
|
multiple hardware models - e.g. "Temperature" covers DS18B20, PT100,
|
||||||
PT1000, thermocouple.
|
PT1000, thermocouple.
|
||||||
|
|
||||||
Seeded defaults ship with the module (see data/fp_sensor_type_data.xml)
|
Seeded defaults ship with the module (see data/fp_sensor_type_data.xml)
|
||||||
@@ -19,7 +19,7 @@ from odoo import fields, models
|
|||||||
|
|
||||||
class FpSensorType(models.Model):
|
class FpSensorType(models.Model):
|
||||||
_name = 'fp.sensor.type'
|
_name = 'fp.sensor.type'
|
||||||
_description = 'Fusion Plating — Sensor Type'
|
_description = 'Fusion Plating - Sensor Type'
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
name = fields.Char(string='Name', required=True, translate=True)
|
name = fields.Char(string='Name', required=True, translate=True)
|
||||||
@@ -74,7 +74,7 @@ class FpSensorType(models.Model):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.act_window',
|
'type': 'ir.actions.act_window',
|
||||||
'name': f'Sensors — {self.name}',
|
'name': f'Sensors - {self.name}',
|
||||||
'res_model': 'fp.tank.sensor',
|
'res_model': 'fp.tank.sensor',
|
||||||
'view_mode': 'list,form',
|
'view_mode': 'list,form',
|
||||||
'domain': [('sensor_type_id', '=', self.id)],
|
'domain': [('sensor_type_id', '=', self.id)],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"""Time-series of sensor readings.
|
"""Time-series of sensor readings.
|
||||||
|
|
||||||
Every POST to /fp/iot/ingest (or every broadcast from the iot proxy)
|
Every POST to /fp/iot/ingest (or every broadcast from the iot proxy)
|
||||||
lands as a new row here. Kept intentionally append-only — we never
|
lands as a new row here. Kept intentionally append-only - we never
|
||||||
update or delete readings, which makes this the compliance log for
|
update or delete readings, which makes this the compliance log for
|
||||||
bath-temperature history.
|
bath-temperature history.
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ Auto-creates a fusion.plating.quality.hold when a reading falls
|
|||||||
outside the sensor's alert range AND the sensor has
|
outside the sensor's alert range AND the sensor has
|
||||||
`alert_on_out_of_spec` enabled. The hold is created once per
|
`alert_on_out_of_spec` enabled. The hold is created once per
|
||||||
excursion (we don't spam a new hold for every reading during a
|
excursion (we don't spam a new hold for every reading during a
|
||||||
sustained excursion) — tracked via the sensor's most-recent
|
sustained excursion) - tracked via the sensor's most-recent
|
||||||
`last_reading_in_spec` flag.
|
`last_reading_in_spec` flag.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ _logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class FpTankReading(models.Model):
|
class FpTankReading(models.Model):
|
||||||
_name = 'fp.tank.reading'
|
_name = 'fp.tank.reading'
|
||||||
_description = 'Fusion Plating — Tank Sensor Reading'
|
_description = 'Fusion Plating - Tank Sensor Reading'
|
||||||
_order = 'reading_at desc, id desc'
|
_order = 'reading_at desc, id desc'
|
||||||
_rec_name = 'display_name'
|
_rec_name = 'display_name'
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class FpTankReading(models.Model):
|
|||||||
'fp.tank.sensor', string='Sensor', required=True,
|
'fp.tank.sensor', string='Sensor', required=True,
|
||||||
ondelete='cascade', index=True,
|
ondelete='cascade', index=True,
|
||||||
)
|
)
|
||||||
# Denormalised for fast list views + kpi queries — auto-filled at
|
# Denormalised for fast list views + kpi queries - auto-filled at
|
||||||
# create time from sensor_id. Indexed for historical trending.
|
# create time from sensor_id. Indexed for historical trending.
|
||||||
tank_id = fields.Many2one(
|
tank_id = fields.Many2one(
|
||||||
'fusion.plating.tank', string='Tank',
|
'fusion.plating.tank', string='Tank',
|
||||||
@@ -56,7 +56,7 @@ class FpTankReading(models.Model):
|
|||||||
value = fields.Float(
|
value = fields.Float(
|
||||||
string='Value (raw)', required=True, digits=(12, 4),
|
string='Value (raw)', required=True, digits=(12, 4),
|
||||||
help='Stored reading in the sensor\'s canonical unit (for '
|
help='Stored reading in the sensor\'s canonical unit (for '
|
||||||
'temperature sensors this is always °C — the DS18B20 and '
|
'temperature sensors this is always °C - the DS18B20 and '
|
||||||
'every other temperature chip reports in Celsius natively; '
|
'every other temperature chip reports in Celsius natively; '
|
||||||
'keeping storage canonical lets us switch display units '
|
'keeping storage canonical lets us switch display units '
|
||||||
'per-company without re-migrating history).',
|
'per-company without re-migrating history).',
|
||||||
@@ -66,7 +66,7 @@ class FpTankReading(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Display-aware fields — converted to the company's preferred unit
|
# Display-aware fields - converted to the company's preferred unit
|
||||||
# (res.company.x_fc_default_temp_uom = 'F' or 'C'). Only the list
|
# (res.company.x_fc_default_temp_uom = 'F' or 'C'). Only the list
|
||||||
# and form views should show these; internal spec comparisons use
|
# and form views should show these; internal spec comparisons use
|
||||||
# `value` so thresholds stay consistent across regions.
|
# `value` so thresholds stay consistent across regions.
|
||||||
@@ -84,7 +84,7 @@ class FpTankReading(models.Model):
|
|||||||
@api.depends('value', 'parameter_id', 'parameter_id.parameter_type',
|
@api.depends('value', 'parameter_id', 'parameter_id.parameter_type',
|
||||||
'parameter_id.uom')
|
'parameter_id.uom')
|
||||||
def _compute_display(self):
|
def _compute_display(self):
|
||||||
# Read once per compute call — env.company rarely changes mid-batch.
|
# Read once per compute call - env.company rarely changes mid-batch.
|
||||||
pref = self.env.company.x_fc_default_temp_uom or 'C'
|
pref = self.env.company.x_fc_default_temp_uom or 'C'
|
||||||
for r in self:
|
for r in self:
|
||||||
ptype = (r.parameter_id.parameter_type or '').lower()
|
ptype = (r.parameter_id.parameter_type or '').lower()
|
||||||
@@ -96,7 +96,7 @@ class FpTankReading(models.Model):
|
|||||||
r.display_unit = r.parameter_id.uom_display or ''
|
r.display_unit = r.parameter_id.uom_display or ''
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Deviation from setpoint — signed Δ from the sensor's effective target
|
# Deviation from setpoint - signed Δ from the sensor's effective target
|
||||||
# in the company-preferred unit. Zero if no setpoint defined.
|
# in the company-preferred unit. Zero if no setpoint defined.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
deviation_from_target = fields.Float(
|
deviation_from_target = fields.Float(
|
||||||
@@ -184,10 +184,10 @@ class FpTankReading(models.Model):
|
|||||||
for r in self:
|
for r in self:
|
||||||
sensor = r.sensor_id.name or 'sensor'
|
sensor = r.sensor_id.name or 'sensor'
|
||||||
at = fields.Datetime.to_string(r.reading_at) if r.reading_at else ''
|
at = fields.Datetime.to_string(r.reading_at) if r.reading_at else ''
|
||||||
r.display_name = f'{sensor} — {r.display_value:.2f} {r.display_unit} @ {at}'
|
r.display_name = f'{sensor} - {r.display_value:.2f} {r.display_unit} @ {at}'
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Create hook — evaluate against spec + raise a quality hold if we
|
# Create hook - evaluate against spec + raise a quality hold if we
|
||||||
# just crossed INTO an out-of-spec state.
|
# just crossed INTO an out-of-spec state.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
@@ -197,7 +197,7 @@ class FpTankReading(models.Model):
|
|||||||
try:
|
try:
|
||||||
rec._evaluate_spec()
|
rec._evaluate_spec()
|
||||||
except Exception:
|
except Exception:
|
||||||
# Never let alert-logic break the ingest path — the
|
# Never let alert-logic break the ingest path - the
|
||||||
# reading itself is what matters for compliance. Log
|
# reading itself is what matters for compliance. Log
|
||||||
# and carry on.
|
# and carry on.
|
||||||
_logger.exception(
|
_logger.exception(
|
||||||
@@ -256,7 +256,7 @@ class FpTankReading(models.Model):
|
|||||||
'hold_reason': 'out_of_spec',
|
'hold_reason': 'out_of_spec',
|
||||||
'description': description,
|
'description': description,
|
||||||
'qty_on_hold': 1,
|
'qty_on_hold': 1,
|
||||||
# state defaults to 'on_hold' — leave it
|
# state defaults to 'on_hold' - leave it
|
||||||
}
|
}
|
||||||
# Attach facility + work-centre context if the tank has them,
|
# Attach facility + work-centre context if the tank has them,
|
||||||
# so the hold is actionable from the shop floor (operator can
|
# so the hold is actionable from the shop floor (operator can
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ device registered via the iot_drivers proxy) is mapped to exactly one
|
|||||||
tank or bath and measures ONE bath parameter (temperature, pH,
|
tank or bath and measures ONE bath parameter (temperature, pH,
|
||||||
conductivity, etc.).
|
conductivity, etc.).
|
||||||
|
|
||||||
The same tank can carry multiple sensors — e.g. a temp probe and a pH
|
The same tank can carry multiple sensors - e.g. a temp probe and a pH
|
||||||
probe. Each is its own fp.tank.sensor row.
|
probe. Each is its own fp.tank.sensor row.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -18,17 +18,17 @@ from odoo import api, fields, models
|
|||||||
|
|
||||||
class FpTankSensor(models.Model):
|
class FpTankSensor(models.Model):
|
||||||
_name = 'fp.tank.sensor'
|
_name = 'fp.tank.sensor'
|
||||||
_description = 'Fusion Plating — Tank Sensor'
|
_description = 'Fusion Plating - Tank Sensor'
|
||||||
_order = 'tank_id, parameter_id'
|
_order = 'tank_id, parameter_id'
|
||||||
|
|
||||||
name = fields.Char(
|
name = fields.Char(
|
||||||
string='Sensor Name', required=True,
|
string='Sensor Name', required=True,
|
||||||
help='Human label (e.g. "Tank 3 — ENP temp").',
|
help='Human label (e.g. "Tank 3 - ENP temp").',
|
||||||
)
|
)
|
||||||
uuid = fields.Char(
|
uuid = fields.Char(
|
||||||
string='UUID',
|
string='UUID',
|
||||||
copy=False, readonly=True, index=True,
|
copy=False, readonly=True, index=True,
|
||||||
help='Stable logical identifier — survives hardware swaps. If a '
|
help='Stable logical identifier - survives hardware swaps. If a '
|
||||||
'probe dies and gets replaced, keep the UUID, change the '
|
'probe dies and gets replaced, keep the UUID, change the '
|
||||||
'device_serial. Every measurement tied to this UUID remains '
|
'device_serial. Every measurement tied to this UUID remains '
|
||||||
'part of the same logical history.',
|
'part of the same logical history.',
|
||||||
@@ -36,7 +36,7 @@ class FpTankSensor(models.Model):
|
|||||||
sensor_type_id = fields.Many2one(
|
sensor_type_id = fields.Many2one(
|
||||||
'fp.sensor.type',
|
'fp.sensor.type',
|
||||||
string='Sensor Type',
|
string='Sensor Type',
|
||||||
help='Taxonomy — temperature, pH, conductivity, etc. '
|
help='Taxonomy - temperature, pH, conductivity, etc. '
|
||||||
'Independent from hardware (a "temperature" sensor could be '
|
'Independent from hardware (a "temperature" sensor could be '
|
||||||
'a DS18B20, PT100, or thermocouple; device_kind captures '
|
'a DS18B20, PT100, or thermocouple; device_kind captures '
|
||||||
'the hardware, sensor_type_id captures the role).',
|
'the hardware, sensor_type_id captures the role).',
|
||||||
@@ -44,7 +44,7 @@ class FpTankSensor(models.Model):
|
|||||||
active = fields.Boolean(default=True)
|
active = fields.Boolean(default=True)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Physical device — either an Odoo iot.device (proxied through the Pi)
|
# Physical device - either an Odoo iot.device (proxied through the Pi)
|
||||||
# OR a direct-ingest sensor (skipping the proxy, posting straight to
|
# OR a direct-ingest sensor (skipping the proxy, posting straight to
|
||||||
# /fp/iot/ingest with the shared secret + device_serial).
|
# /fp/iot/ingest with the shared secret + device_serial).
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -73,7 +73,7 @@ class FpTankSensor(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Where this sensor lives — can be ANY of tank / work_center /
|
# Where this sensor lives - can be ANY of tank / work_center /
|
||||||
# facility / named-location. Keep tank_id as the primary (most
|
# facility / named-location. Keep tank_id as the primary (most
|
||||||
# sensors are bath-mounted) but allow the other three as
|
# sensors are bath-mounted) but allow the other three as
|
||||||
# alternatives so we can mount probes on ovens, ambient air,
|
# alternatives so we can mount probes on ovens, ambient air,
|
||||||
@@ -84,22 +84,22 @@ class FpTankSensor(models.Model):
|
|||||||
)
|
)
|
||||||
bath_id = fields.Many2one(
|
bath_id = fields.Many2one(
|
||||||
'fusion.plating.bath', string='Bath',
|
'fusion.plating.bath', string='Bath',
|
||||||
help='Optional — if the sensor is bound to a specific bath '
|
help='Optional - if the sensor is bound to a specific bath '
|
||||||
'chemistry rather than a physical tank.',
|
'chemistry rather than a physical tank.',
|
||||||
)
|
)
|
||||||
work_center_id = fields.Many2one(
|
work_center_id = fields.Many2one(
|
||||||
'fusion.plating.work.center', string='Work Centre',
|
'fusion.plating.work.center', string='Work Centre',
|
||||||
help='Alternative to tank_id — use when the sensor is attached '
|
help='Alternative to tank_id - use when the sensor is attached '
|
||||||
'to a station, oven, or line rather than a bath tank.',
|
'to a station, oven, or line rather than a bath tank.',
|
||||||
)
|
)
|
||||||
facility_id = fields.Many2one(
|
facility_id = fields.Many2one(
|
||||||
'fusion.plating.facility', string='Facility',
|
'fusion.plating.facility', string='Facility',
|
||||||
help='Alternative to tank_id / work_center_id — use for '
|
help='Alternative to tank_id / work_center_id - use for '
|
||||||
'facility-wide sensors (ambient, HVAC, perimeter).',
|
'facility-wide sensors (ambient, HVAC, perimeter).',
|
||||||
)
|
)
|
||||||
location_name = fields.Char(
|
location_name = fields.Char(
|
||||||
string='Location (free-text)',
|
string='Location (free-text)',
|
||||||
help='Free-text override when none of the above fit — e.g. '
|
help='Free-text override when none of the above fit - e.g. '
|
||||||
'"North bay wall", "Effluent pipe exit", "Rinse tank #2 '
|
'"North bay wall", "Effluent pipe exit", "Rinse tank #2 '
|
||||||
'roof". Shown alongside the structured location in views.',
|
'roof". Shown alongside the structured location in views.',
|
||||||
)
|
)
|
||||||
@@ -113,7 +113,7 @@ class FpTankSensor(models.Model):
|
|||||||
effective_location = fields.Char(
|
effective_location = fields.Char(
|
||||||
string='Location',
|
string='Location',
|
||||||
compute='_compute_effective_location', store=True,
|
compute='_compute_effective_location', store=True,
|
||||||
help='Display string for the sensor location — prefers tank, '
|
help='Display string for the sensor location - prefers tank, '
|
||||||
'then work_center, then facility, then free-text.',
|
'then work_center, then facility, then free-text.',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ class FpTankSensor(models.Model):
|
|||||||
rec.effective_location = ''
|
rec.effective_location = ''
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Target / alerting behaviour — three concepts:
|
# Target / alerting behaviour - three concepts:
|
||||||
# setpoint → what we CONTROL TOWARD (dashboards, PID, trend baseline)
|
# setpoint → what we CONTROL TOWARD (dashboards, PID, trend baseline)
|
||||||
# alert min → lower alarm boundary (quality hold fires below)
|
# alert min → lower alarm boundary (quality hold fires below)
|
||||||
# alert max → upper alarm boundary (quality hold fires above)
|
# alert max → upper alarm boundary (quality hold fires above)
|
||||||
@@ -164,7 +164,7 @@ class FpTankSensor(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Sub 7 — per-sensor polling interval
|
# Sub 7 - per-sensor polling interval
|
||||||
#
|
#
|
||||||
# Blank on the sensor = inherit res.company default. The effective
|
# Blank on the sensor = inherit res.company default. The effective
|
||||||
# value gates the ingest endpoint so too-frequent readings are
|
# value gates the ingest endpoint so too-frequent readings are
|
||||||
@@ -175,7 +175,7 @@ class FpTankSensor(models.Model):
|
|||||||
help='How often a reading from this sensor should be stored. '
|
help='How often a reading from this sensor should be stored. '
|
||||||
'Leave blank to inherit the company-wide default. Readings '
|
'Leave blank to inherit the company-wide default. Readings '
|
||||||
'that arrive inside this interval are dropped by the '
|
'that arrive inside this interval are dropped by the '
|
||||||
'ingest endpoint — the database stays clean even if the '
|
'ingest endpoint - the database stays clean even if the '
|
||||||
'Pi agent polls more often.',
|
'Pi agent polls more often.',
|
||||||
)
|
)
|
||||||
poll_interval_display = fields.Char(
|
poll_interval_display = fields.Char(
|
||||||
@@ -211,7 +211,7 @@ class FpTankSensor(models.Model):
|
|||||||
return self.env.company.x_fc_default_poll_interval_minutes or 30
|
return self.env.company.x_fc_default_poll_interval_minutes or 30
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Effective target — resolves override → parameter default → 0
|
# Effective target - resolves override → parameter default → 0
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
effective_target = fields.Float(
|
effective_target = fields.Float(
|
||||||
string='Effective Setpoint',
|
string='Effective Setpoint',
|
||||||
@@ -253,7 +253,7 @@ class FpTankSensor(models.Model):
|
|||||||
string='In Spec?', readonly=True,
|
string='In Spec?', readonly=True,
|
||||||
help='Computed from the last reading vs alert_min/alert_max.',
|
help='Computed from the last reading vs alert_min/alert_max.',
|
||||||
)
|
)
|
||||||
# Display-aware version of last_reading_value — converted to company
|
# Display-aware version of last_reading_value - converted to company
|
||||||
# preferred unit (res.company.x_fc_default_temp_uom).
|
# preferred unit (res.company.x_fc_default_temp_uom).
|
||||||
last_reading_display = fields.Float(
|
last_reading_display = fields.Float(
|
||||||
string='Latest Value',
|
string='Latest Value',
|
||||||
@@ -308,7 +308,7 @@ class FpTankSensor(models.Model):
|
|||||||
rec.reading_count = len(rec.reading_ids)
|
rec.reading_count = len(rec.reading_ids)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Resolve effective alert range — override wins, else bath.parameter
|
# Resolve effective alert range - override wins, else bath.parameter
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def _get_alert_range(self):
|
def _get_alert_range(self):
|
||||||
"""Return (min, max) floats. Zero means 'no bound'."""
|
"""Return (min, max) floats. Zero means 'no bound'."""
|
||||||
@@ -324,7 +324,7 @@ class FpTankSensor(models.Model):
|
|||||||
def _get_setpoint(self):
|
def _get_setpoint(self):
|
||||||
"""Canonical (raw) setpoint used for deviation calcs.
|
"""Canonical (raw) setpoint used for deviation calcs.
|
||||||
|
|
||||||
Returns 0.0 if no setpoint is set at either level — callers should
|
Returns 0.0 if no setpoint is set at either level - callers should
|
||||||
treat 0 as "no target defined, skip deviation display".
|
treat 0 as "no target defined, skip deviation display".
|
||||||
"""
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
@@ -336,7 +336,7 @@ class FpTankSensor(models.Model):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.act_window',
|
'type': 'ir.actions.act_window',
|
||||||
'name': f'Readings — {self.name}',
|
'name': f'Readings - {self.name}',
|
||||||
'res_model': 'fp.tank.reading',
|
'res_model': 'fp.tank.reading',
|
||||||
'view_mode': 'list,form,graph',
|
'view_mode': 'list,form,graph',
|
||||||
'domain': [('sensor_id', '=', self.id)],
|
'domain': [('sensor_id', '=', self.id)],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
#
|
#
|
||||||
# Sub 7 — company-wide default for the IoT sensor polling interval.
|
# Sub 7 - company-wide default for the IoT sensor polling interval.
|
||||||
|
|
||||||
from odoo import fields, models
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
#
|
#
|
||||||
# Sub 7 — Expose the IoT polling default on the Fusion Plating
|
# Sub 7 - Expose the IoT polling default on the Fusion Plating
|
||||||
# Settings page so admins manage it alongside other plating settings.
|
# Settings page so admins manage it alongside other plating settings.
|
||||||
|
|
||||||
from odoo import fields, models
|
from odoo import fields, models
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
invisible="last_reading_in_spec or not last_reading_at"
|
invisible="last_reading_in_spec or not last_reading_at"
|
||||||
bg_color="text-bg-danger"/>
|
bg_color="text-bg-danger"/>
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<h1><field name="name" placeholder="e.g. Tank 3 — ENP temp"/></h1>
|
<h1><field name="name" placeholder="e.g. Tank 3 - ENP temp"/></h1>
|
||||||
</div>
|
</div>
|
||||||
<group>
|
<group>
|
||||||
<group string="Identity">
|
<group string="Identity">
|
||||||
@@ -73,10 +73,10 @@
|
|||||||
<field name="device_serial" placeholder="28-abc123def456"/>
|
<field name="device_serial" placeholder="28-abc123def456"/>
|
||||||
<field name="iot_device_id"
|
<field name="iot_device_id"
|
||||||
options="{'no_create': True}"
|
options="{'no_create': True}"
|
||||||
help="Optional — the iot.device auto-registered by the Pi proxy."/>
|
help="Optional - the iot.device auto-registered by the Pi proxy."/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group string="Location — fill in ONE (first one set wins for display)">
|
<group string="Location - fill in ONE (first one set wins for display)">
|
||||||
<group>
|
<group>
|
||||||
<field name="tank_id" options="{'no_create': True}"/>
|
<field name="tank_id" options="{'no_create': True}"/>
|
||||||
<field name="bath_id" options="{'no_create': True}"/>
|
<field name="bath_id" options="{'no_create': True}"/>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
<group string="Setpoint & Alerting">
|
<group string="Setpoint & Alerting">
|
||||||
<group string="Target (setpoint)">
|
<group string="Target (setpoint)">
|
||||||
<field name="target_value_override"
|
<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."/>
|
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" readonly="1"/>
|
||||||
<field name="effective_target_unit" readonly="1"/>
|
<field name="effective_target_unit" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Copyright 2026 Nexa Systems Inc.
|
Copyright 2026 Nexa Systems Inc.
|
||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Part of the Fusion Plating product family.
|
Part of the Fusion Plating product family.
|
||||||
Sub 7 — IoT default polling interval setting.
|
Sub 7 - IoT default polling interval setting.
|
||||||
-->
|
-->
|
||||||
<odoo>
|
<odoo>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user