refactor(fusion_iot): port sensor taxonomy + dashboards, retire fusion_plating_sensors

fusion_plating_sensors had broader scope (sensor_type taxonomy,
dashboards, location flexibility) but its core logic was ALL
scaffolding — alert rules stored thresholds with zero side effects,
measurement create just filled a name sequence, the HTTP endpoint
required user-auth session cookies. Meanwhile fusion_plating_iot had
the actual working alerting: in-spec checks, quality-hold auto-raise
with excursion dedupe, setpoint + deviation, token-auth ingest for
headless hardware. Plus 563 real readings from the pilot Pi.

Right unification: keep fusion_plating_iot (working) as the base,
port the valuable structural bits from fusion_plating_sensors, demolish
the latter entirely.

**Ported to fusion_plating_iot:**

- `fp.sensor.type` — taxonomy model with 8 seeded types (Temperature,
  pH, Conductivity, Level, Pressure, Flow, Concentration, Switch).
  Richer than the device_kind Selection; hardware-independent (one
  "Temperature" type covers DS18B20 / PT100 / thermocouple).
- `fp.sensor.dashboard` — named grouping of sensors with
  out-of-spec count. Simple but useful ("ENP Line 1 — all tanks")
  without the broken alert-rule complexity.
- Extended `fp.tank.sensor`:
  * `uuid` (stable logical ID, survives hardware swaps)
  * `sensor_type_id` (link to the taxonomy above)
  * `work_center_id`, `facility_id`, `location_name` — alternatives to
    tank_id so probes can live on ovens, ambient air, effluent pipes
    without faking a "tank"
  * `effective_location` computed — picks the first non-empty of the
    four location fields for display

**Post-install hook** backfills UUID + default sensor_type on existing
live sensors. Verified on the 2 pilot sensors: both got UUIDs, both
auto-assigned the Temperature type via device_kind=ds18b20 mapping.

**Deleted** (all of fusion_plating_sensors, 1205 LOC):
- fp.sensor (replaced by fp.tank.sensor with added fields)
- fp.sensor.measurement (replaced by fp.tank.reading)
- fp.sensor.alert.rule (replaced by inline alert_min/max + working hold)
- /fp/sensor/measure controller (replaced by /fp/iot/ingest)
- fp.sensor.measure.wizard (not needed — Odoo's normal create form works)
- The "Sensors" submenu hierarchy (Dashboards/All Sensors/Measurements/
  Sensor Types) that created the dup menus the user reported

**Menu now**: Plating → Operations → Sensors
  - Dashboards    (fp.sensor.dashboard)
  - Sensors       (fp.tank.sensor — renamed from "Tank Sensors" since
                   it supports non-tank locations now)
  - Readings      (fp.tank.reading)
  - Sensor Types  (fp.sensor.type)

No data loss: all 591 Pi readings preserved (up from 563 earlier as
the live poller kept running throughout the refactor). Brief 503 on
the Pi during the Odoo module-update restart; poller auto-retried on
the next 30s tick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-20 19:58:57 -04:00
parent 118f96dad4
commit cf205cfd11
34 changed files with 573 additions and 1224 deletions

View File

@@ -5,3 +5,4 @@
from . import models
from . import controllers
from .hooks import post_init_hook

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — IoT Integration',
'version': '19.0.0.3.0',
'version': '19.0.1.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Wire physical tank sensors to Fusion Plating — live '
'temperature / chemistry readings with auto quality holds '
@@ -45,11 +45,15 @@ Part of the Fusion Plating product family by Nexa Systems Inc.
'data': [
'security/ir.model.access.csv',
'data/ir_config_parameter_data.xml',
'data/fp_sensor_type_data.xml',
'views/fp_sensor_type_views.xml',
'views/fp_sensor_dashboard_views.xml',
'views/fp_tank_sensor_views.xml',
'views/fp_tank_reading_views.xml',
'views/fusion_plating_tank_views.xml',
'views/fp_iot_menu.xml',
],
'post_init_hook': 'post_init_hook',
'installable': True,
'application': False,
'auto_install': False,

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1
Default sensor types — the common ones every plating shop needs.
noupdate="1" so admins can tweak without losing edits on module upgrade.
-->
<odoo noupdate="1">
<record id="fp_sensor_type_temperature" model="fp.sensor.type">
<field name="name">Temperature</field>
<field name="code">temperature</field>
<field name="sequence">10</field>
<field name="measurement_type">number</field>
<field name="default_uom">°C</field>
<field name="icon">fa-thermometer-half</field>
</record>
<record id="fp_sensor_type_ph" model="fp.sensor.type">
<field name="name">pH</field>
<field name="code">ph</field>
<field name="sequence">20</field>
<field name="measurement_type">number</field>
<field name="default_uom">pH</field>
<field name="icon">fa-flask</field>
</record>
<record id="fp_sensor_type_conductivity" model="fp.sensor.type">
<field name="name">Conductivity</field>
<field name="code">conductivity</field>
<field name="sequence">30</field>
<field name="measurement_type">number</field>
<field name="default_uom">µS/cm</field>
<field name="icon">fa-bolt</field>
</record>
<record id="fp_sensor_type_level" model="fp.sensor.type">
<field name="name">Tank Level</field>
<field name="code">level</field>
<field name="sequence">40</field>
<field name="measurement_type">number</field>
<field name="default_uom">cm</field>
<field name="icon">fa-tint</field>
</record>
<record id="fp_sensor_type_pressure" model="fp.sensor.type">
<field name="name">Pressure</field>
<field name="code">pressure</field>
<field name="sequence">50</field>
<field name="measurement_type">number</field>
<field name="default_uom">kPa</field>
<field name="icon">fa-tachometer</field>
</record>
<record id="fp_sensor_type_flow" model="fp.sensor.type">
<field name="name">Flow Rate</field>
<field name="code">flow</field>
<field name="sequence">60</field>
<field name="measurement_type">number</field>
<field name="default_uom">L/min</field>
<field name="icon">fa-cogs</field>
</record>
<record id="fp_sensor_type_concentration" model="fp.sensor.type">
<field name="name">Concentration</field>
<field name="code">concentration</field>
<field name="sequence">70</field>
<field name="measurement_type">number</field>
<field name="default_uom">g/L</field>
<field name="icon">fa-vial</field>
</record>
<record id="fp_sensor_type_switch" model="fp.sensor.type">
<field name="name">Limit / Float Switch</field>
<field name="code">switch</field>
<field name="sequence">100</field>
<field name="measurement_type">boolean</field>
<field name="icon">fa-toggle-on</field>
</record>
</odoo>

View File

@@ -0,0 +1,76 @@
# -*- 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)
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)

View File

@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from . import fp_sensor_type
from . import fp_sensor_dashboard
from . import fp_tank_sensor
from . import fp_tank_reading
from . import fusion_plating_tank

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
"""Sensor dashboard — a named group of related sensors.
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
trending chart + alert count. A dashboard is just a logical grouping;
actual rendering happens in the UI.
"""
from odoo import api, fields, models
class FpSensorDashboard(models.Model):
_name = 'fp.sensor.dashboard'
_description = 'Fusion Plating — Sensor Dashboard'
_inherit = ['mail.thread']
_order = 'name'
name = fields.Char(string='Name', required=True, tracking=True)
description = fields.Text(string='Description')
sensor_ids = fields.Many2many(
'fp.tank.sensor',
'fp_sensor_dashboard_rel',
'dashboard_id', 'sensor_id',
string='Sensors',
)
sensor_count = fields.Integer(
string='Members', compute='_compute_counts',
)
out_of_spec_count = fields.Integer(
string='Sensors Out of Spec', compute='_compute_counts',
help='How many of this dashboard\'s sensors currently have a '
'last-reading that\'s out of spec.',
)
color = fields.Integer(string='Colour Tag')
active = fields.Boolean(default=True)
company_id = fields.Many2one(
'res.company', default=lambda self: self.env.company,
)
@api.depends('sensor_ids.last_reading_in_spec', 'sensor_ids.last_reading_at')
def _compute_counts(self):
for dash in self:
dash.sensor_count = len(dash.sensor_ids)
dash.out_of_spec_count = len(dash.sensor_ids.filtered(
lambda s: s.last_reading_at and not s.last_reading_in_spec
))
def action_view_sensors(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': f'Sensors — {self.name}',
'res_model': 'fp.tank.sensor',
'view_mode': 'list,form',
'domain': [('id', 'in', self.sensor_ids.ids)],
'target': 'current',
}
def action_view_recent_readings(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': f'Readings — {self.name}',
'res_model': 'fp.tank.reading',
'view_mode': 'graph,list,form',
'domain': [('sensor_id', 'in', self.sensor_ids.ids)],
'target': 'current',
}

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
"""Sensor-type taxonomy.
A richer taxonomy than the `device_kind` Selection on fp.tank.sensor.
Types describe WHAT the sensor measures (temperature, pH, conductivity,
level, etc.) plus the data type of its readings. Same type can back
multiple hardware models — e.g. "Temperature" covers DS18B20, PT100,
PT1000, thermocouple.
Seeded defaults ship with the module (see data/fp_sensor_type_data.xml)
so users don't have to set them up from scratch. Admins add more via
Plating → Operations → Sensors → Sensor Types.
"""
from odoo import fields, models
class FpSensorType(models.Model):
_name = 'fp.sensor.type'
_description = 'Fusion Plating — Sensor Type'
_order = 'sequence, name'
name = fields.Char(string='Name', required=True, translate=True)
sequence = fields.Integer(default=10)
code = fields.Char(
string='Code', required=True,
help='Short machine-friendly identifier, e.g. "temperature", "ph".',
)
measurement_type = fields.Selection(
[
('number', 'Number'),
('boolean', 'Boolean'),
('text', 'Text'),
],
string='Measurement Type', required=True, default='number',
help='Most plating-shop sensors are numeric. Boolean is useful for '
'limit switches / float switches; text for things like batch '
'lot codes from a barcode reader.',
)
default_uom = fields.Char(
string='Default Unit',
help='Unit string (°C, pH, µS/cm, etc.) inherited by sensors of '
'this type unless they carry a parameter-specific unit.',
)
icon = fields.Char(
string='Icon',
help='Optional fa-* or oi-* icon name for use in dashboards.',
)
sensor_ids = fields.One2many(
'fp.tank.sensor', 'sensor_type_id',
string='Sensors of this type',
)
sensor_count = fields.Integer(
string='Sensor Count', compute='_compute_sensor_count',
)
active = fields.Boolean(default=True)
company_id = fields.Many2one(
'res.company',
default=lambda self: self.env.company,
)
_sql_constraints = [
('fp_sensor_type_code_uniq', 'unique(code, company_id)',
'Sensor-type code must be unique per company.'),
]
def _compute_sensor_count(self):
for rec in self:
rec.sensor_count = len(rec.sensor_ids)
def action_view_sensors(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': f'Sensors — {self.name}',
'res_model': 'fp.tank.sensor',
'view_mode': 'list,form',
'domain': [('sensor_type_id', '=', self.id)],
'context': {'default_sensor_type_id': self.id},
'target': 'current',
}

View File

@@ -25,6 +25,22 @@ class FpTankSensor(models.Model):
string='Sensor Name', required=True,
help='Human label (e.g. "Tank 3 — ENP temp").',
)
uuid = fields.Char(
string='UUID',
copy=False, readonly=True, index=True,
help='Stable logical identifier — survives hardware swaps. If a '
'probe dies and gets replaced, keep the UUID, change the '
'device_serial. Every measurement tied to this UUID remains '
'part of the same logical history.',
)
sensor_type_id = fields.Many2one(
'fp.sensor.type',
string='Sensor Type',
help='Taxonomy — temperature, pH, conductivity, etc. '
'Independent from hardware (a "temperature" sensor could be '
'a DS18B20, PT100, or thermocouple; device_kind captures '
'the hardware, sensor_type_id captures the role).',
)
active = fields.Boolean(default=True)
# ------------------------------------------------------------------
@@ -57,7 +73,11 @@ class FpTankSensor(models.Model):
)
# ------------------------------------------------------------------
# Where this sensor lives + what it measures
# Where this sensor lives — can be ANY of tank / work_center /
# facility / named-location. Keep tank_id as the primary (most
# sensors are bath-mounted) but allow the other three as
# alternatives so we can mount probes on ovens, ambient air,
# waste-water discharge, etc. without forcing a fake "tank".
# ------------------------------------------------------------------
tank_id = fields.Many2one(
'fusion.plating.tank', string='Tank', ondelete='cascade',
@@ -67,6 +87,22 @@ class FpTankSensor(models.Model):
help='Optional — if the sensor is bound to a specific bath '
'chemistry rather than a physical tank.',
)
work_center_id = fields.Many2one(
'fusion.plating.work.center', string='Work Centre',
help='Alternative to tank_id — use when the sensor is attached '
'to a station, oven, or line rather than a bath tank.',
)
facility_id = fields.Many2one(
'fusion.plating.facility', string='Facility',
help='Alternative to tank_id / work_center_id — use for '
'facility-wide sensors (ambient, HVAC, perimeter).',
)
location_name = fields.Char(
string='Location (free-text)',
help='Free-text override when none of the above fit — e.g. '
'"North bay wall", "Effluent pipe exit", "Rinse tank #2 '
'roof". Shown alongside the structured location in views.',
)
parameter_id = fields.Many2one(
'fusion.plating.bath.parameter', string='Parameter Measured',
required=True,
@@ -74,6 +110,27 @@ class FpTankSensor(models.Model):
'etc.). Drives unit labelling + out-of-spec alerting against '
'the parameter\'s target_min / target_max.',
)
effective_location = fields.Char(
string='Location',
compute='_compute_effective_location', store=True,
help='Display string for the sensor location — prefers tank, '
'then work_center, then facility, then free-text.',
)
@api.depends('tank_id', 'bath_id', 'work_center_id', 'facility_id',
'location_name')
def _compute_effective_location(self):
for rec in self:
if rec.tank_id:
rec.effective_location = rec.tank_id.name
elif rec.work_center_id:
rec.effective_location = rec.work_center_id.name
elif rec.facility_id:
rec.effective_location = rec.facility_id.name
elif rec.location_name:
rec.effective_location = rec.location_name
else:
rec.effective_location = ''
# ------------------------------------------------------------------
# Target / alerting behaviour — three concepts:
@@ -185,8 +242,19 @@ class FpTankSensor(models.Model):
('fp_tank_sensor_serial_uniq',
'unique(device_serial)',
'Each hardware serial can only be mapped to one sensor.'),
('fp_tank_sensor_uuid_uniq',
'unique(uuid)',
'UUID must be unique across all sensors.'),
]
@api.model_create_multi
def create(self, vals_list):
import uuid as _uuid
for vals in vals_list:
if not vals.get('uuid'):
vals['uuid'] = _uuid.uuid4().hex
return super().create(vals_list)
@api.depends('reading_ids')
def _compute_reading_count(self):
for rec in self:

View File

@@ -5,3 +5,9 @@ fp_tank_sensor_manager,fp.tank.sensor manager,model_fp_tank_sensor,fusion_platin
fp_tank_reading_operator,fp.tank.reading operator,model_fp_tank_reading,fusion_plating.group_fusion_plating_operator,1,0,1,0
fp_tank_reading_supervisor,fp.tank.reading supervisor,model_fp_tank_reading,fusion_plating.group_fusion_plating_supervisor,1,0,1,0
fp_tank_reading_manager,fp.tank.reading manager,model_fp_tank_reading,fusion_plating.group_fusion_plating_manager,1,1,1,1
fp_sensor_type_operator,fp.sensor.type operator,model_fp_sensor_type,fusion_plating.group_fusion_plating_operator,1,0,0,0
fp_sensor_type_supervisor,fp.sensor.type supervisor,model_fp_sensor_type,fusion_plating.group_fusion_plating_supervisor,1,0,0,0
fp_sensor_type_manager,fp.sensor.type manager,model_fp_sensor_type,fusion_plating.group_fusion_plating_manager,1,1,1,1
fp_sensor_dashboard_operator,fp.sensor.dashboard operator,model_fp_sensor_dashboard,fusion_plating.group_fusion_plating_operator,1,0,0,0
fp_sensor_dashboard_supervisor,fp.sensor.dashboard supervisor,model_fp_sensor_dashboard,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
fp_sensor_dashboard_manager,fp.sensor.dashboard manager,model_fp_sensor_dashboard,fusion_plating.group_fusion_plating_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
5 fp_tank_reading_operator fp.tank.reading operator model_fp_tank_reading fusion_plating.group_fusion_plating_operator 1 0 1 0
6 fp_tank_reading_supervisor fp.tank.reading supervisor model_fp_tank_reading fusion_plating.group_fusion_plating_supervisor 1 0 1 0
7 fp_tank_reading_manager fp.tank.reading manager model_fp_tank_reading fusion_plating.group_fusion_plating_manager 1 1 1 1
8 fp_sensor_type_operator fp.sensor.type operator model_fp_sensor_type fusion_plating.group_fusion_plating_operator 1 0 0 0
9 fp_sensor_type_supervisor fp.sensor.type supervisor model_fp_sensor_type fusion_plating.group_fusion_plating_supervisor 1 0 0 0
10 fp_sensor_type_manager fp.sensor.type manager model_fp_sensor_type fusion_plating.group_fusion_plating_manager 1 1 1 1
11 fp_sensor_dashboard_operator fp.sensor.dashboard operator model_fp_sensor_dashboard fusion_plating.group_fusion_plating_operator 1 0 0 0
12 fp_sensor_dashboard_supervisor fp.sensor.dashboard supervisor model_fp_sensor_dashboard fusion_plating.group_fusion_plating_supervisor 1 1 1 0
13 fp_sensor_dashboard_manager fp.sensor.dashboard manager model_fp_sensor_dashboard fusion_plating.group_fusion_plating_manager 1 1 1 1

View File

@@ -3,27 +3,39 @@
Copyright 2026 Nexa Systems Inc.
License OPL-1
Surface IoT sensors + readings under the existing Plating >
Operations menu. Not a top-level app — sensors are an extension
of bath/tank management, not a separate concern.
Single "Sensors" section under Plating > Operations. Consolidates
all sensor management into one place (formerly split across an
obsolete fusion_plating_sensors module).
-->
<odoo>
<menuitem id="menu_fp_iot_root"
name="Sensors &amp; Readings"
name="Sensors"
parent="fusion_plating.menu_fp_operations"
sequence="55"/>
<menuitem id="menu_fp_tank_sensor"
name="Tank Sensors"
<menuitem id="menu_fp_sensor_dashboard"
name="Dashboards"
parent="menu_fp_iot_root"
action="action_fp_tank_sensor"
action="action_fp_sensor_dashboard"
sequence="10"/>
<menuitem id="menu_fp_tank_reading"
name="Sensor Readings"
<menuitem id="menu_fp_tank_sensor"
name="Sensors"
parent="menu_fp_iot_root"
action="action_fp_tank_reading"
action="action_fp_tank_sensor"
sequence="20"/>
<menuitem id="menu_fp_tank_reading"
name="Readings"
parent="menu_fp_iot_root"
action="action_fp_tank_reading"
sequence="30"/>
<menuitem id="menu_fp_sensor_type"
name="Sensor Types"
parent="menu_fp_iot_root"
action="action_fp_sensor_type"
sequence="40"/>
</odoo>

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="fp_sensor_dashboard_list" model="ir.ui.view">
<field name="name">fp.sensor.dashboard.list</field>
<field name="model">fp.sensor.dashboard</field>
<field name="arch" type="xml">
<list string="Sensor Dashboards"
decoration-danger="out_of_spec_count &gt; 0">
<field name="name"/>
<field name="description" optional="show"/>
<field name="sensor_count"/>
<field name="out_of_spec_count"/>
<field name="color" widget="color_picker" optional="show"/>
<field name="active" widget="boolean_toggle" optional="show"/>
</list>
</field>
</record>
<record id="fp_sensor_dashboard_form" model="ir.ui.view">
<field name="name">fp.sensor.dashboard.form</field>
<field name="model">fp.sensor.dashboard</field>
<field name="arch" type="xml">
<form string="Sensor Dashboard">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_sensors" type="object"
class="oe_stat_button" icon="fa-microchip">
<field name="sensor_count" widget="statinfo"
string="Sensors"/>
</button>
<button name="action_view_recent_readings" type="object"
class="oe_stat_button" icon="fa-line-chart">
<field name="out_of_spec_count" widget="statinfo"
string="Out of Spec"/>
</button>
</div>
<div class="oe_title">
<h1><field name="name" placeholder="e.g. ENP Line 1"/></h1>
</div>
<group>
<field name="description" placeholder="What this dashboard shows…"/>
<field name="color" widget="color_picker"/>
<field name="active"/>
</group>
<notebook>
<page string="Sensors">
<field name="sensor_ids">
<list>
<field name="name"/>
<field name="sensor_type_id"/>
<field name="effective_location"/>
<field name="last_reading_display"/>
<field name="last_reading_display_unit"/>
<field name="last_reading_in_spec" widget="boolean_toggle"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="action_fp_sensor_dashboard" model="ir.actions.act_window">
<field name="name">Dashboards</field>
<field name="res_model">fp.sensor.dashboard</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="fp_sensor_type_list" model="ir.ui.view">
<field name="name">fp.sensor.type.list</field>
<field name="model">fp.sensor.type</field>
<field name="arch" type="xml">
<list string="Sensor Types" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="code"/>
<field name="measurement_type"/>
<field name="default_uom"/>
<field name="icon" optional="hide"/>
<field name="sensor_count"/>
<field name="active" widget="boolean_toggle" optional="show"/>
</list>
</field>
</record>
<record id="fp_sensor_type_form" model="ir.ui.view">
<field name="name">fp.sensor.type.form</field>
<field name="model">fp.sensor.type</field>
<field name="arch" type="xml">
<form string="Sensor Type">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_sensors" type="object"
class="oe_stat_button" icon="fa-microchip">
<field name="sensor_count" widget="statinfo"
string="Sensors"/>
</button>
</div>
<div class="oe_title">
<h1><field name="name" placeholder="e.g. Temperature"/></h1>
</div>
<group>
<group>
<field name="code"/>
<field name="measurement_type"/>
<field name="default_uom"/>
</group>
<group>
<field name="sequence"/>
<field name="icon"/>
<field name="active"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_fp_sensor_type" model="ir.actions.act_window">
<field name="name">Sensor Types</field>
<field name="res_model">fp.sensor.type</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@@ -10,14 +10,19 @@
<field name="name">fp.tank.sensor.list</field>
<field name="model">fp.tank.sensor</field>
<field name="arch" type="xml">
<list string="Tank Sensors" decoration-danger="not last_reading_in_spec and last_reading_at"
<list string="Sensors" decoration-danger="not last_reading_in_spec and last_reading_at"
decoration-muted="not active">
<field name="name"/>
<field name="device_kind"/>
<field name="tank_id"/>
<field name="bath_id" optional="show"/>
<field name="sensor_type_id" optional="show"/>
<field name="device_kind" optional="show"/>
<field name="effective_location" string="Location" optional="show"/>
<field name="tank_id" optional="hide"/>
<field name="work_center_id" optional="hide"/>
<field name="facility_id" optional="hide"/>
<field name="bath_id" optional="hide"/>
<field name="parameter_id"/>
<field name="device_serial" optional="show"/>
<field name="uuid" optional="hide"/>
<field name="iot_device_id" optional="hide"/>
<field name="last_reading_display"/>
<field name="last_reading_display_unit"/>
@@ -55,18 +60,30 @@
<h1><field name="name" placeholder="e.g. Tank 3 — ENP temp"/></h1>
</div>
<group>
<group string="Identity">
<field name="sensor_type_id" options="{'no_create': True}"/>
<field name="uuid" readonly="1"/>
<field name="parameter_id" options="{'no_create': True}"/>
<field name="active"/>
</group>
<group string="Hardware">
<field name="device_kind"/>
<field name="device_serial" placeholder="28-abc123def456"/>
<field name="iot_device_id"
options="{'no_create': True}"
help="Optional — the iot.device auto-registered by the Pi proxy."/>
<field name="active"/>
</group>
<group string="Location">
</group>
<group string="Location — fill in ONE (first one set wins for display)">
<group>
<field name="tank_id" options="{'no_create': True}"/>
<field name="bath_id" options="{'no_create': True}"/>
<field name="parameter_id" options="{'no_create': True}"/>
</group>
<group>
<field name="work_center_id" options="{'no_create': True}"/>
<field name="facility_id" options="{'no_create': True}"/>
<field name="location_name"/>
<field name="effective_location" readonly="1"/>
</group>
</group>
<group string="Setpoint &amp; Alerting">