Per-sensor override on fp.tank.sensor.poll_interval_minutes with a
company-wide default (res.company.x_fc_default_poll_interval_minutes,
default 30) exposed in Settings → Fusion Plating → IoT. Single
lookup helper _fp_effective_poll_interval_minutes keeps downstream
call sites simple. Read-only poll_interval_display Char ("30 min
(default)" / "15 min (override)") keeps units unambiguous per user
request.
Ingest endpoint /fp/iot/ingest drops readings that arrive inside a
sensor's effective interval, returning {accepted, skipped} so the Pi
agent can log it. Pi-side interval stays its own concern.
Post-init hook seeds 5 small tanks + 20 big tanks (10 active, 10
inactive) with 1 temperature + 1 pH sensor each → 25 tanks, 50
sensors. Idempotent (keyed by tank.code, with_context(active_test=
False)). Opt-in via ir.config_parameter
fusion_plating_iot.seed_entech_tanks = '1' so a fresh install
elsewhere doesn't auto-seed. Flag set on entech today; 27 tanks / 52
sensors now live (2 pilot + 25 seeded).
Smoke on entech: 14/14 assertions pass including idempotency and
rate-limit conditions.
fusion_plating_iot → 19.0.2.0.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
173 lines
6.1 KiB
Python
173 lines
6.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
"""Post-install hook — backfill new fields on existing live sensors.
|
|
|
|
Runs once on every install/upgrade. Idempotent: checks before writing
|
|
so re-runs don't overwrite user-edited values.
|
|
|
|
What it does:
|
|
1. Populates `uuid` on any fp.tank.sensor record that doesn't have one
|
|
(for sensors created BEFORE the uuid field existed — the create
|
|
override only covers new records).
|
|
2. Sets a default `sensor_type_id` on sensors that don't have one yet,
|
|
inferring from `device_kind` (DS18B20 / PT100 / PT1000 → Temperature,
|
|
pH → pH probe, etc.).
|
|
"""
|
|
import logging
|
|
import uuid as _uuid
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def post_init_hook(env):
|
|
_backfill_uuids(env)
|
|
_backfill_sensor_types(env)
|
|
_seed_entech_tanks_and_sensors(env)
|
|
|
|
|
|
def _backfill_uuids(env):
|
|
Sensor = env['fp.tank.sensor']
|
|
missing = Sensor.search([('uuid', '=', False)])
|
|
if not missing:
|
|
return
|
|
for s in missing:
|
|
s.sudo().write({'uuid': _uuid.uuid4().hex})
|
|
_logger.info('fp.tank.sensor: populated UUID on %d existing records',
|
|
len(missing))
|
|
|
|
|
|
def _backfill_sensor_types(env):
|
|
Sensor = env['fp.tank.sensor']
|
|
Type = env['fp.sensor.type']
|
|
|
|
# Map device_kind → sensor-type code. Falls back to 'temperature' for
|
|
# the rare case someone set device_kind='other' on a probe that IS
|
|
# temperature (common on the pilot).
|
|
kind_to_code = {
|
|
'ds18b20': 'temperature',
|
|
'pt100': 'temperature',
|
|
'pt1000': 'temperature',
|
|
'ph': 'ph',
|
|
'conductivity': 'conductivity',
|
|
'level': 'level',
|
|
}
|
|
|
|
missing = Sensor.search([('sensor_type_id', '=', False)])
|
|
if not missing:
|
|
return
|
|
|
|
# Resolve the types once up front
|
|
type_cache = {}
|
|
for code in set(kind_to_code.values()):
|
|
t = Type.search([('code', '=', code)], limit=1)
|
|
if t:
|
|
type_cache[code] = t.id
|
|
|
|
updated = 0
|
|
for s in missing:
|
|
code = kind_to_code.get(s.device_kind)
|
|
# Unmapped (device_kind='other') → try temperature as the most
|
|
# common fallback. Admin can correct in the UI.
|
|
type_id = type_cache.get(code) or type_cache.get('temperature')
|
|
if type_id:
|
|
s.sudo().write({'sensor_type_id': type_id})
|
|
updated += 1
|
|
_logger.info('fp.tank.sensor: set default sensor_type_id on %d records',
|
|
updated)
|
|
|
|
|
|
def _seed_entech_tanks_and_sensors(env):
|
|
"""Sub 7 — seed 25 tanks (5 small + 20 big; 10 big inactive) with
|
|
one temperature and one pH sensor each.
|
|
|
|
Idempotent: tanks keyed by `code` so re-runs skip existing rows.
|
|
Sensors keyed by (tank_id, parameter_type) so duplicates aren't
|
|
created if the admin added a temp/pH sensor manually after seed.
|
|
|
|
Opt-in via system parameter `fusion_plating_iot.seed_entech_tanks`.
|
|
Default False so a fresh install doesn't auto-seed on anyone else.
|
|
Admin sets it to '1' via Settings → Technical → System Parameters
|
|
and re-runs the module upgrade (-u fusion_plating_iot) to trigger.
|
|
"""
|
|
Param = env['ir.config_parameter'].sudo()
|
|
if Param.get_param('fusion_plating_iot.seed_entech_tanks', '0') != '1':
|
|
return
|
|
|
|
# with_context(active_test=False) so the idempotency search sees
|
|
# the inactive big tanks and doesn't try to recreate them on re-run.
|
|
Tank = env['fusion.plating.tank'].with_context(active_test=False)
|
|
Sensor = env['fp.tank.sensor'].with_context(active_test=False)
|
|
Parameter = env['fusion.plating.bath.parameter']
|
|
Facility = env['fusion.plating.facility']
|
|
|
|
facility = Facility.search([], limit=1)
|
|
if not facility:
|
|
_logger.warning('Sub 7 seed: no fusion.plating.facility found — '
|
|
'skipping tank seed. Create a facility first.')
|
|
return
|
|
|
|
temp_param = (
|
|
Parameter.search([('code', '=', 'TEMP')], limit=1)
|
|
or Parameter.search([('parameter_type', '=', 'temperature')], limit=1)
|
|
)
|
|
ph_param = (
|
|
Parameter.search([('code', '=', 'PH')], limit=1)
|
|
or Parameter.search([('parameter_type', '=', 'ph')], limit=1)
|
|
)
|
|
if not temp_param or not ph_param:
|
|
_logger.warning('Sub 7 seed: temperature / pH bath parameters '
|
|
'not found — skipping. Seed bath parameters first.')
|
|
return
|
|
|
|
plan = []
|
|
for i in range(1, 6):
|
|
plan.append({'name': 'Small Tank #%d' % i, 'code': 'SMALL-%02d' % i,
|
|
'active': True, 'sequence': 10 + i})
|
|
for i in range(1, 21):
|
|
plan.append({'name': 'Big Tank #%d' % i, 'code': 'BIG-%02d' % i,
|
|
'active': (i <= 10), 'sequence': 20 + i})
|
|
|
|
tanks_created = 0
|
|
sensors_created = 0
|
|
for row in plan:
|
|
tank = Tank.search([('code', '=', row['code'])], limit=1)
|
|
if not tank:
|
|
tank = Tank.create({
|
|
'name': row['name'],
|
|
'code': row['code'],
|
|
'facility_id': facility.id,
|
|
'sequence': row['sequence'],
|
|
'active': row['active'],
|
|
})
|
|
tanks_created += 1
|
|
# Temperature sensor
|
|
if not Sensor.search_count([
|
|
('tank_id', '=', tank.id),
|
|
('parameter_id.parameter_type', '=', 'temperature'),
|
|
]):
|
|
Sensor.create({
|
|
'name': '%s — Temperature' % tank.name,
|
|
'tank_id': tank.id,
|
|
'parameter_id': temp_param.id,
|
|
'device_kind': 'ds18b20',
|
|
'active': row['active'],
|
|
})
|
|
sensors_created += 1
|
|
# pH sensor
|
|
if not Sensor.search_count([
|
|
('tank_id', '=', tank.id),
|
|
('parameter_id.parameter_type', '=', 'ph'),
|
|
]):
|
|
Sensor.create({
|
|
'name': '%s — pH' % tank.name,
|
|
'tank_id': tank.id,
|
|
'parameter_id': ph_param.id,
|
|
'device_kind': 'ph',
|
|
'active': row['active'],
|
|
})
|
|
sensors_created += 1
|
|
|
|
_logger.info('Sub 7 seed: created %d tanks + %d sensors',
|
|
tanks_created, sensors_created)
|