57 min readDeterministic automation

Implementation Guide: Trigger preventive maintenance schedules based on equipment run hours

Step-by-step implementation guide for deploying AI to trigger preventive maintenance schedules based on equipment run hours for Manufacturing clients.

Hardware Procurement

Industrial IoT Wireless AC Current Monitor

NCD (National Control Devices)PR52-33 (900 MHz, split-core CT, wireless)Qty: 20

$179–$249/unit MSP cost / $299–$399 suggested resale

Non-invasive clamp-on current transformer sensor that detects electrical current draw on each machine's power feed. When current exceeds a configurable threshold (e.g., >2A), the machine is considered 'running' and hours accumulate. 900 MHz wireless mesh network with up to 2-mile range. Powered by 4 AA batteries with ~10-year battery life at typical reporting intervals. One sensor per monitored machine.

Industrial IoT Wireless Vibration & Temperature Sensor V3

NCD (National Control Devices)PR52-6V (900 MHz, vibration + temp, wireless)Qty: 5

$349–$499/unit MSP cost / $549–$749 suggested resale

Optional premium sensor for the 5 most critical assets. Adds vibration and temperature monitoring on top of run-hour tracking, enabling early anomaly detection on high-value equipment such as CNC spindles, large compressors, or injection mold clamps. Provides additional data points that can trigger condition-based work orders alongside run-hour-based PMs.

Industrial IoT Gateway

Industrial IoT Gateway

MoxaUC-2101-LX (Cortex-A8, Moxa Industrial Linux)Qty: 3

$250–$400/unit MSP cost / $400–$600 suggested resale

DIN-rail-mountable industrial gateway that receives wireless sensor data via the NCD wireless mesh, runs Mosquitto MQTT broker and Node-RED locally, and forwards processed data to the cloud CMMS. One gateway per production zone or building — each supports 10–15 sensors. Moxa Industrial Linux pre-installed with long-term support.

Industrial Ethernet Switch (Managed)

MoxaEDS-205A (5-port unmanaged) or EDS-G508E (8-port managed)Qty: 3

$80–$250/unit MSP cost / $150–$400 suggested resale

Connects Moxa gateways to the plant's IT network backbone. Managed switches recommended for VLAN segmentation between OT sensor network and IT corporate network. Industrial-rated for plant-floor temperature and vibration environments.

DIN Rail Enclosure with Power Supply

Moxa / Weidmüller / Phoenix ContactVarious — IP65 rated DIN enclosure + 24VDC DIN rail power supplyQty: 3

$50–$100/unit MSP cost / $100–$200 suggested resale

Houses each gateway and Ethernet switch securely on the plant floor. IP65 rating protects against dust and coolant splash common in manufacturing environments. 24VDC DIN rail power supply provides clean power to gateway.

Edge Server / Mini PC

Edge Server / Mini PC

Intel / DellIntel NUC 12 Pro (NUC12WSHi5) or Dell OptiPlex Micro 7010Qty: 1

$500–$800/unit MSP cost / $800–$1,200 suggested resale

Optional on-premises server located in the plant's IT closet or server room. Runs a centralized MQTT broker (Mosquitto), Node-RED instance, InfluxDB time-series database for historical run-hour data, and Grafana dashboard for local visibility. Recommended for plants with unreliable internet or desire for local data sovereignty. Can be omitted if gateways handle logic and cloud CMMS handles all data storage.

UPS Battery Backup

APC / CyberPowerAPC Smart-UPS 750VA (SMT750) or CyberPower OR700LCDRM1UQty: 1

$250–$400/unit MSP cost / $400–$600 suggested resale

Provides battery backup for the edge server and core networking equipment. Ensures run-hour data is not lost during plant power fluctuations, which are common in manufacturing environments with large inductive loads starting and stopping.

Cat6 Ethernet Patch Cables (Industrial Rated)

Tripp Lite / L-comVarious lengths, shielded, CMR/CMP ratedQty: 10

$10–$25 each MSP cost / $20–$50 suggested resale

Connect gateways to industrial Ethernet switches. Shielded cables reduce electromagnetic interference from nearby VFDs, welders, and motors on the plant floor.

Sensor Mounting Hardware Kit

NCD / GenericCT clamp zip-ties, DIN clips, adhesive mounts, cable glandsQty: 1

$50–$100 per kit MSP cost / $100–$200 suggested resale

Mounting supplies for securing CT sensors to machine power feeds and routing sensor cables. Includes zip ties, adhesive-backed cable clips, and cable glands for routing through enclosures.

Software Procurement

Fiix CMMS (Professional Plan)

Rockwell Automation (Fiix)Professional PlanQty: 1 admin + 3 technicians (typical)

$75/user/month. Typical deployment: 1 admin + 3 technicians = $300/month. MSP resells at 15-20% markup or bills separately for managed CMMS administration.

Cloud-based Computerized Maintenance Management System that serves as the central hub for all maintenance operations. Professional plan includes meter-based work order triggers (the core feature for this project), asset hierarchy management, PM scheduling, parts inventory, mobile app for technicians, API access for integration, and full audit trail for compliance. Fiix natively supports 'meter readings' as PM triggers — when a meter (run hours) hits a threshold, a work order is automatically created and assigned.

Mosquitto MQTT Broker

Eclipse Foundation (Open Source)

Free. No licensing cost. Runs on gateway or edge server.

Lightweight MQTT message broker that receives sensor data from IoT gateways and makes it available to Node-RED for processing. Handles pub/sub messaging with topics organized per asset (e.g., plant/zone1/machine-cnc01/current). Supports TLS encryption for secure data transport.

Node-RED

OpenJS Foundation (Open Source)

Free. No licensing cost. Runs on gateway or edge server.

Flow-based visual programming tool that serves as the logic engine for the system. Accumulates run hours per asset based on sensor current readings, compares accumulated hours against PM thresholds, and makes REST API calls to Fiix CMMS to create work orders when thresholds are reached. Also handles data transformation, alerting, and error handling.

InfluxDB OSS

InfluxData (Open Source)v2 OSS

Free for self-hosted OSS edition. Runs on edge server.

Time-series database optimized for IoT sensor data. Stores raw current readings, calculated run-hour totals, and historical trends per asset. Enables dashboarding and historical analysis. Retention policies auto-purge raw data after 90 days while keeping hourly/daily aggregates indefinitely.

Grafana OSS

Grafana Labs (Open Source)

Free for self-hosted OSS edition. Runs on edge server.

Visualization dashboard that displays real-time and historical run-hour data per asset, sensor health status, and maintenance KPIs. Provides the plant manager and MSP with a visual overview without needing CMMS login. Optional but highly recommended for client satisfaction and MSP monitoring.

Zapier (Starter Plan)

ZapierStarter Plan

$29.99/month for 750 tasks/month. Typically sufficient for 20-machine deployments.

Connects Fiix CMMS to the client's existing business systems for notifications and workflow automation. Example Zaps: work order created → Slack notification to maintenance team, work order overdue → email to plant manager, parts below reorder point → PO request in QuickBooks/ERP. Reduces the need for custom API integration for common business workflow connections.

Prerequisites

  • Client must have an inventory of all equipment to be monitored, including: asset name, make/model, electrical specifications (voltage, amperage), physical location, and existing PM intervals (if any). If none exist, MSP should conduct an asset audit as a billable Phase 0 activity.
  • Each monitored machine must be electrically powered and have an accessible power feed (either at the machine's disconnect switch, electrical panel breaker, or power cord) where a CT clamp sensor can be installed. Machines powered by compressed air only, manual/hydraulic-only, or with inaccessible wiring will need alternative approaches.
  • The plant must have Ethernet connectivity (wired or Wi-Fi) that can reach the location of each IoT gateway. If plant-floor networking does not exist, the MSP should scope a separate network infrastructure project as a prerequisite. Cellular gateways (e.g., Teltonika RUT955) can be substituted if wired/Wi-Fi is not feasible.
  • Internet connectivity with at least 5 Mbps upload speed must be available for cloud CMMS access. Sensor data volume is minimal (~30 MB/day for 20 sensors at 1-minute intervals), but CMMS web interface and mobile app require reliable internet.
  • The client must designate a maintenance manager or supervisor who will serve as the CMMS administrator — responsible for reviewing work orders, adjusting PM thresholds, and managing the technician roster. This person should have basic computer literacy and a smartphone or tablet.
  • All maintenance technicians who will receive and complete work orders must have smartphones (iOS or Android) for the Fiix mobile app. The client should plan for 1–2 hours of technician training time.
  • A licensed electrician must be available (either on the client's staff or subcontracted by the MSP) for Phase 3 sensor installation if local codes require a licensed electrician to work on or near machine power feeds. Check local jurisdiction requirements — many states require a licensed electrician for any work involving opening electrical panels.
  • The client should have existing PM procedures or manufacturer-recommended maintenance intervals for their equipment. If not, the MSP and client must jointly develop PM task lists and run-hour thresholds during Phase 1. Common starting points: oil changes every 500 hours, filter replacements every 250 hours, bearing lubrication every 1,000 hours.
  • VLAN-capable network infrastructure is recommended (but not strictly required) to segment IoT/OT sensor traffic from the client's corporate IT network per IEC 62443 best practices.
  • If the client has an existing ERP system (Epicor, SAP Business One, NetSuite, QuickBooks, etc.) and wants maintenance-to-purchasing integration, the ERP system's API credentials and documentation must be available before Phase 4.

Installation Steps

...

Step 1: Conduct Asset Inventory and PM Threshold Planning

Before any hardware is installed, perform a thorough on-site survey of all equipment to be monitored. For each asset, document: (1) Asset name and unique identifier, (2) Make, model, and serial number, (3) Electrical specifications — voltage, rated amperage, number of phases, (4) Physical location on the plant floor (take photos and annotate a floor plan), (5) Location of electrical disconnect/panel where CT sensor will be installed, (6) Existing maintenance schedule and procedures (if any), (7) Manufacturer-recommended PM intervals. Work with the maintenance manager to define run-hour thresholds for each PM task per equipment type. Create a spreadsheet mapping: Asset ID → PM Task → Run-Hour Trigger → Parts Required → Estimated Duration → Assigned Technician.

Note

This is the most important step. Getting accurate thresholds saves rework later. If the client has no existing PM data, use manufacturer recommendations as starting points and plan to adjust after 3 months of runtime data. Common defaults: lubrication every 250 hours, filter changes every 500 hours, major service every 2,000 hours. Take detailed photos of every electrical panel and machine power feed — the electrician will need these for Phase 3 planning.

Step 2: Provision Network Infrastructure for IoT Sensors

Ensure plant-floor network coverage for the IoT gateway deployment. The NCD wireless sensors communicate at 900 MHz directly to the NCD gateway/receiver — they do NOT use Wi-Fi. However, each Moxa UC-2101 gateway needs an Ethernet connection to reach the edge server and internet. Steps: (1) Identify 2–3 gateway placement locations on the plant floor — central to each sensor cluster, within the NCD 900 MHz wireless range (~200 feet indoors with obstructions, up to 2 miles line-of-sight), and near an Ethernet drop and 120VAC power outlet. (2) If Ethernet drops don't exist at these locations, run Cat6 shielded cable from the nearest switch closet. (3) Configure a dedicated OT VLAN (e.g., VLAN 100, subnet 10.100.0.0/24) on the client's managed switch infrastructure. (4) Configure firewall rules: allow outbound HTTPS (443) and MQTTS (8883) from the OT VLAN to the internet. Block all inbound connections to the OT VLAN from the internet. Allow the edge server to communicate with the OT VLAN.

Example VLAN configuration on a managed switch (Cisco/HP syntax varies)
bash
# Example VLAN configuration on a managed switch (Cisco/HP syntax varies)
# On the client's core switch:
configure terminal
vlan 100
name OT-Sensors
exit
interface range GigabitEthernet 0/20-22
switchport mode access
switchport access vlan 100
exit
# On the firewall (example pfSense/OPNsense rules):
# Allow VLAN 100 -> Internet on ports 443 (HTTPS) and 8883 (MQTTS)
# Allow VLAN 100 -> Edge Server IP on ports 1883 (MQTT), 8086 (InfluxDB), 1880 (Node-RED)
# Deny all other traffic from VLAN 100
Note

NCD 900 MHz sensors have excellent range and penetration through walls and equipment. However, large metal enclosures, thick concrete walls, and electrical interference from VFDs can reduce range. Plan to test wireless signal strength with one sensor before committing to gateway placement. If coverage is insufficient, add an additional gateway (they are relatively inexpensive). The DIN rail enclosure should be mounted in a location protected from direct exposure to coolant, chips, and high vibration.

Step 3: Install CT Current Sensors on Equipment

For each machine to be monitored, install one NCD PR52-33 wireless CT current sensor on the machine's primary power feed. This step typically requires a licensed electrician. Process per machine:

1
Coordinate with production to schedule downtime or install during shift changes.
2
Lock out/tag out (LOTO) the machine per OSHA 29 CFR 1910.147 before opening any electrical panels.
3
Open the machine's main disconnect or electrical panel to access the power feed conductors.
4
Clamp the split-core CT around ONE of the phase conductors (typically the L1/hot wire for single-phase, or any one phase for three-phase). Do NOT clamp around the ground wire or around multiple conductors bundled together.
5
Route the CT sensor leads out of the panel through a cable gland or existing knockout.
6
Mount the NCD wireless transmitter outside the electrical panel using the DIN clip or adhesive mount. The wireless signal is severely degraded inside a metal panel.
7
Insert 4 AA lithium batteries into the NCD transmitter.
8
Close and re-secure the electrical panel. Remove LOTO.
9
Power on the machine and verify the sensor is transmitting (LED indicator on NCD module blinks on transmission).
10
Label the sensor with the asset ID using a durable industrial label.
Critical

LOTO procedures must be followed for every sensor installation. The CT clamp is non-invasive (it does not make electrical contact with the conductor), but the electrician must open the panel to access the conductors, which exposes them to energized circuits. For three-phase motors, clamping around a single phase conductor is sufficient to detect running status. The current reading will be approximately 1/3 of the total motor current. Document which phase conductor the CT is clamped on. For machines with Variable Frequency Drives (VFDs), install the CT on the INPUT side of the VFD (line side), not the output side, as VFD output waveforms can give inaccurate CT readings. Battery life: NCD sensors with 10-second reporting intervals get ~3-year battery life; with 1-minute intervals, expect ~10 years. Recommend 1-minute intervals for run-hour tracking — sub-minute precision is unnecessary.

Step 4: Deploy and Configure IoT Gateways

Install the Moxa UC-2101 gateways in their DIN rail enclosures at the pre-planned locations. Each gateway will run the NCD wireless receiver interface, Mosquitto MQTT broker (local), and Node-RED.

1
Mount the DIN rail enclosure at each planned location. Install the 24VDC power supply and Moxa UC-2101 inside.
2
Connect the Moxa gateway to the Ethernet switch via Cat6 cable.
3
Connect the NCD wireless receiver USB dongle to the Moxa gateway USB port.
4
Power on the gateway and connect via SSH for initial configuration.
5
Set a static IP address on the OT VLAN.
6
Install Mosquitto MQTT broker and Node-RED.
7
Configure the NCD wireless receiver software to output sensor data to the local MQTT broker.
Gateway SSH setup, Mosquitto MQTT broker installation, and Node-RED configuration
bash
# SSH into the Moxa gateway (default IP varies, check Moxa documentation)
ssh moxa@192.168.100.10
# Set static IP (edit Moxa network config or use Moxa's web-based tool)
sudo nano /etc/network/interfaces
# Configure: address 10.100.0.11, netmask 255.255.255.0, gateway 10.100.0.1

# Install Mosquitto MQTT Broker
sudo apt-get update
sudo apt-get install -y mosquitto mosquitto-clients

# Configure Mosquitto with authentication
sudo nano /etc/mosquitto/conf.d/default.conf
# Add the following:
# listener 1883
# allow_anonymous false
# password_file /etc/mosquitto/passwd

# Create MQTT user
sudo mosquitto_passwd -c /etc/mosquitto/passwd iot_gateway
# Enter password when prompted

sudo systemctl enable mosquitto
sudo systemctl start mosquitto

# Install Node-RED (if not pre-installed on Moxa Industrial Linux)
sudo apt-get install -y nodejs npm
sudo npm install -g --unsafe-perm node-red

# Install Node-RED as a systemd service
sudo nano /etc/systemd/system/nodered.service
# [Unit]
# Description=Node-RED
# After=network.target
# [Service]
# ExecStart=/usr/bin/node-red --max-old-space-size=256
# Restart=on-failure
# User=moxa
# [Install]
# WantedBy=multi-user.target

sudo systemctl daemon-reload
sudo systemctl enable nodered
sudo systemctl start nodered

# Verify services are running
sudo systemctl status mosquitto
sudo systemctl status nodered

# Test MQTT locally
mosquitto_pub -h localhost -u iot_gateway -P <password> -t test/hello -m 'Gateway online'
mosquitto_sub -h localhost -u iot_gateway -P <password> -t test/hello
Note

If using the Moxa UC-2101 with Moxa Industrial Linux, some packages may need to be installed from Moxa's repository rather than standard Debian repos. Refer to Moxa's documentation for the specific firmware version. For the NCD wireless receiver, NCD provides a Node-RED node package (node-red-contrib-ncd-wireless) that simplifies sensor data ingestion. Install it in Node-RED: Menu → Manage Palette → Install → search 'ncd-wireless'. Alternative: If deploying a centralized edge server instead of per-gateway logic, install Mosquitto and Node-RED only on the edge server, and configure gateways to forward MQTT data to the central broker using MQTT bridge configuration.

Set up the Intel NUC or Dell OptiPlex Micro as the centralized edge server. This server runs the master MQTT broker, central Node-RED instance, InfluxDB for time-series storage, and Grafana for dashboarding. Steps: (1) Install Ubuntu Server 22.04 LTS on the mini PC. (2) Assign a static IP on the OT VLAN (e.g., 10.100.0.5). (3) Install Docker and Docker Compose for simplified service management. (4) Deploy Mosquitto, Node-RED, InfluxDB, and Grafana as Docker containers.

Edge server setup: static IP configuration, Docker installation, and container deployment for Mosquitto, Node-RED, InfluxDB, and Grafana
bash
# Install Ubuntu Server 22.04 LTS (standard installation)
# After installation, set static IP:
sudo nano /etc/netplan/00-installer-config.yaml
# network:
#   ethernets:
#     eno1:
#       dhcp4: no
#       addresses: [10.100.0.5/24]
#       gateway4: 10.100.0.1
#       nameservers:
#         addresses: [8.8.8.8, 8.8.4.4]
#   version: 2
sudo netplan apply

# Install Docker
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER

# Create project directory
mkdir -p ~/pm-automation && cd ~/pm-automation

# Create docker-compose.yml (see custom_ai_components for full file)
nano docker-compose.yml

# Create Mosquitto config
mkdir -p mosquitto/config mosquitto/data mosquitto/log
nano mosquitto/config/mosquitto.conf
# persistence true
# persistence_location /mosquitto/data/
# log_dest file /mosquitto/log/mosquitto.log
# listener 1883
# allow_anonymous false
# password_file /mosquitto/config/passwd

# Create MQTT password file
docker run --rm -v $(pwd)/mosquitto/config:/mosquitto/config eclipse-mosquitto mosquitto_passwd -c /mosquitto/config/passwd pm_system

# Create InfluxDB data directory
mkdir -p influxdb/data

# Create Grafana data directory
mkdir -p grafana/data && sudo chown -R 472:472 grafana/data

# Start all services
docker compose up -d

# Verify all containers are running
docker compose ps
Note

The edge server is optional if you choose to run Node-RED directly on each Moxa gateway and rely solely on the cloud CMMS for data storage. However, the edge server provides: (1) a centralized MQTT broker that aggregates data from all gateways, (2) local time-series storage in InfluxDB for historical analysis even if internet is down, (3) Grafana dashboards for local plant-floor visibility, and (4) a single point for the MSP to remote-manage via VPN or RMM agent. The UPS battery backup should protect this server. Ensure the RMM agent (e.g., ConnectWise Automate, Datto RMM, NinjaRMM) is installed on this server for remote MSP monitoring.

Step 6: Configure Fiix CMMS — Asset Hierarchy and Meter Setup

Set up the Fiix CMMS cloud instance with the client's asset hierarchy, define meters for run hours, and configure meter-based PM triggers.

1
Create the Fiix account and add user licenses (1 admin, 3 technicians typical).
2
Build the asset hierarchy: Site → Building/Area → Equipment.
3
For each monitored asset, add a 'Meter' of type 'Run Hours' with the unit set to 'Hours'.
4
Create PM templates for each equipment type with task checklists, parts lists, estimated duration, and assigned technician.
5
Link each PM template to the corresponding asset's run-hour meter with the appropriate trigger threshold (e.g., every 500 hours).
6
Configure notification settings: email and mobile push for work order assignment, overdue reminders, and critical alerts.
  • Navigate to: Settings → Import/Export → Import Assets → Upload CSV (for bulk asset import)
  • Navigate to Fiix web interface at https://app.fiixsoftware.com (no CLI — GUI configuration only)
Bulk asset import CSV format for Fiix
csv
asset_name, asset_code, site, area, category, make, model, serial_number
CNC-Mill-01, CNC01, Main Plant, Machine Shop, CNC Machine, Haas, VF-2SS, SN12345
CNC-Mill-02, CNC02, Main Plant, Machine Shop, CNC Machine, Haas, VF-2SS, SN12346
Compressor-01, COMP01, Main Plant, Utilities, Air Compressor, Ingersoll Rand, R-Series R11i, SN98765
Fiix REST API — create a meter programmatically
http
POST https://api.fiixsoftware.com/api/v2/meters
Authorization: Bearer <API_TOKEN>
Content-Type: application/json

{
  "name": "Run Hours - CNC01",
  "unit": "Hours",
  "assetId": 12345,
  "meterType": "CUMULATIVE"
}
Note

Fiix Professional plan ($75/user/month) is required for meter-based PM triggers and API access. The Basic plan ($45/user/month) does not support meter-based scheduling. When defining PM thresholds, start conservative (shorter intervals) and adjust upward after gathering 3-6 months of data. Fiix supports multiple meters per asset and multiple PMs per meter — so a single machine can have a 250-hour lubrication PM, a 500-hour filter PM, and a 2,000-hour major overhaul PM all running independently. Configure the Fiix mobile app on all technician smartphones during this step as well.

Step 7: Configure Node-RED Run-Hour Accumulation Logic

Build the Node-RED flows that: (1) Subscribe to MQTT sensor data topics, (2) Determine if each machine is running based on current threshold, (3) Accumulate run hours per asset, (4) Store run-hour data in InfluxDB, (5) Push updated meter readings to Fiix CMMS via REST API, (6) Handle edge cases like sensor disconnection, power outages, and battery warnings. Access Node-RED at http://<edge-server-ip>:1880 and import the flows defined in the custom_ai_components section of this guide.

Install required Node-RED palette nodes and restart service
bash
# Install required Node-RED palette nodes
cd ~/.node-red
npm install node-red-contrib-influxdb
npm install node-red-contrib-ncd-wireless
npm install node-red-node-smooth

# Restart Node-RED to load new nodes
sudo systemctl restart nodered
  • Import the flow JSON via Node-RED UI: Menu (hamburger) → Import → Clipboard → paste the flow JSON from custom_ai_components, then Deploy the flow
  • Alternatively, copy the flow JSON directly to /home/<user>/.node-red/flows.json and restart Node-RED to load
Note

The complete Node-RED flow implementation is provided in the custom_ai_components section. The flow uses a context store (file-backed) to persist run-hour totals across Node-RED restarts. This is critical — without persistent context, a Node-RED restart would reset all accumulated hours. Configure Node-RED's settings.js to use a file-based context store: contextStorage: { default: { module: 'localfilesystem' } }. The Fiix API token should be stored as an environment variable, not hardcoded in the flow. Set it in the Node-RED environment: FIIX_API_TOKEN=<token>.

Step 8: Configure Fiix API Integration for Automated Meter Updates

1
In Fiix, navigate to Settings → API Keys → Generate new API key.
2
Note the API key and secret.
3
Configure the Fiix API endpoint in Node-RED HTTP request nodes.
4
Test meter reading submission by manually posting a test reading.
5
Verify that the meter reading appears in Fiix and that PM work orders are triggered when thresholds are crossed.
Fiix API authentication, meter reading submission, and credential setup
bash
# Generate Fiix API credentials via the Fiix web UI:
# Settings → Integrations → API Keys → + New API Key
# Save the Client ID and Client Secret securely

# Test API authentication (get OAuth2 bearer token):
curl -X POST https://api.fiixsoftware.com/api/v2/auth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>'

# Response will include: { "access_token": "eyJ...", "expires_in": 3600 }

# Test submitting a meter reading:
curl -X POST https://api.fiixsoftware.com/api/v2/meter-readings \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{
    "meterId": 12345,
    "reading": 100.5,
    "readingDate": "2025-01-15T10:30:00Z"
  }'

# Verify in Fiix UI: Navigate to the asset → Meters tab → confirm reading appears

# Test PM trigger: Submit a reading that exceeds the PM threshold
# Then check: Work Orders → Open → verify a new PM work order was auto-created

# Store credentials as environment variables on the edge server:
echo 'export FIIX_CLIENT_ID=<your_client_id>' >> ~/.bashrc
echo 'export FIIX_CLIENT_SECRET=<your_client_secret>' >> ~/.bashrc
source ~/.bashrc
Note

Fiix API rate limits: check current documentation, but typically 60 requests per minute. For 20 machines with hourly meter updates, you'll make ~20 requests per hour — well within limits. The Node-RED flow should batch meter updates if possible. Use OAuth2 client_credentials flow for server-to-server authentication. Token expires in 3600 seconds — the Node-RED flow should cache the token and refresh it before expiry. IMPORTANT: After submitting a meter reading that crosses a PM threshold, Fiix automatically creates the work order. Verify this behavior in testing — it's the core deliverable of the entire project.

Step 9: Configure Notifications and Escalation Workflows

Set up notification channels so that maintenance technicians are immediately aware of new PM work orders and management is alerted to overdue items. Steps: (1) In Fiix, configure email notifications for work order creation and assignment. (2) Enable push notifications in the Fiix mobile app. (3) Set up Zapier integration for additional notification channels (Slack, SMS, or Teams). (4) Configure escalation rules: if a PM work order is not started within 24 hours, notify the maintenance manager; if not completed within 72 hours, notify the plant manager.

1
Fiix notification configuration is done in the web UI: Settings → Notifications → Configure per event type
2
Create a Zapier account at https://zapier.com
3
Create a new Zap: Trigger: Fiix → New Work Order Created | Filter: Work Order Type = Preventive Maintenance | Action: Slack → Send Channel Message → #maintenance-alerts | Message: 'New PM Work Order: {{work_order_code}} - {{description}} for {{asset_name}}. Assigned to: {{assigned_to}}. Due: {{due_date}}'
4
Create escalation Zap: Trigger: Schedule by Zapier → Every Hour | Action: Fiix → Find Work Orders (status=Open, overdue=true) | Filter: Only continue if results found | Action: Email → Send Email to plant.manager@client.com | Subject: 'OVERDUE: {{count}} PM work orders require attention'
Note

Zapier's free tier (100 tasks/month) may be sufficient for small deployments. Each notification counts as one task. For 20 machines with monthly PMs, expect ~20-40 Zap executions per month. If the client uses Microsoft Teams instead of Slack, substitute the Teams action in the Zap. If the client prefers not to use Zapier, Fiix's built-in email notifications cover the basic use case — Zapier is an enhancement for multi-channel notifications and escalation logic.

Step 10: Set Up Grafana Monitoring Dashboard

Create a Grafana dashboard that provides real-time visibility into equipment run hours, sensor health, and maintenance KPIs. This dashboard is valuable for both the client (plant manager view) and the MSP (remote monitoring). Steps: (1) Access Grafana at http://<edge-server-ip>:3000 (default login admin/admin — change immediately). (2) Add InfluxDB as a data source. (3) Import or create dashboards for: equipment run hours (current vs. PM threshold), sensor health (last reading timestamp, battery voltage), daily/weekly equipment utilization trends, and open/overdue work order count.

Grafana InfluxDB data source configuration and example InfluxQL queries for run hours and utilization panels
influxql
# Access Grafana UI at http://10.100.0.5:3000
# Default credentials: admin / admin (change on first login)

# Add InfluxDB data source:
# Configuration → Data Sources → Add data source → InfluxDB
# URL: http://influxdb:8086 (Docker internal hostname) or http://10.100.0.5:8086
# Database: pm_automation
# User: <influxdb_user>
# Password: <influxdb_password>
# HTTP Method: GET
# Save & Test

# Example InfluxDB query for Grafana panel (run hours gauge per machine):
# SELECT last("run_hours_total") FROM "equipment_runtime" WHERE "asset_id" = 'CNC01' AND $timeFilter GROUP BY time(1h) fill(previous)

# Example query for equipment utilization % (hours running / hours in period):
# SELECT (last("run_hours_total") - first("run_hours_total")) / (($__interval_ms / 3600000) * 1.0) * 100 FROM "equipment_runtime" WHERE "asset_id" = 'CNC01' AND $timeFilter GROUP BY time(1d)
Note

Grafana is optional but provides significant value for client demonstrations and MSP remote monitoring. Create two dashboard views: (1) a 'Plant Overview' with a table showing all assets, current run hours, next PM due at, and sensor status — this is the plant manager's daily view; (2) a 'MSP Health' dashboard showing sensor last-seen timestamps, gateway uptime, MQTT message rates, and InfluxDB storage usage — this is for the MSP's NOC. Share the Plant Overview dashboard via a public snapshot link (read-only) so the client can view it without Grafana credentials.

Step 11: Validate End-to-End System Operation

Perform comprehensive testing of the complete data pipeline: sensor → gateway → MQTT → Node-RED → InfluxDB → Fiix CMMS → work order → notification. For each monitored machine: (1) Turn the machine on and verify current reading appears in Node-RED debug output and InfluxDB within 2 minutes. (2) Turn the machine off and verify the sensor reads near-zero current and run-hour accumulation stops. (3) Manually inject a simulated meter reading into Node-RED that exceeds the PM threshold for a test asset. (4) Verify that Fiix automatically creates a PM work order. (5) Verify that the assigned technician receives a notification (email, push, Slack). (6) Have the technician open the work order on the Fiix mobile app, follow the PM checklist, and mark it complete. (7) Verify that the run-hour meter resets (or the next PM threshold is set correctly) after work order completion. (8) Repeat for at least 3 different machines to confirm consistency.

Inject test MQTT messages, verify InfluxDB data, and trigger Fiix PM work order via API
bash
# Inject test MQTT message simulating high current (machine running):
mosquitto_pub -h 10.100.0.5 -u pm_system -P <password> \
  -t 'plant/zone1/CNC01/current' \
  -m '{"sensor_id":"NCD-001","current_rms":15.2,"battery_v":3.4,"timestamp":"2025-01-15T10:00:00Z"}'

# Verify in Node-RED: check debug tab for processed message
# Verify in InfluxDB:
curl -G 'http://10.100.0.5:8086/query' \
  --data-urlencode 'db=pm_automation' \
  --data-urlencode 'q=SELECT last(*) FROM equipment_runtime WHERE asset_id=\'CNC01\''

# Inject test meter reading to Fiix to force PM trigger:
# (Adjust the reading value to exceed the configured PM threshold)
curl -X POST https://api.fiixsoftware.com/api/v2/meter-readings \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"meterId": <CNC01_METER_ID>, "reading": 501.0, "readingDate": "2025-01-15T12:00:00Z"}'

# Check Fiix for auto-generated work order
# Check technician's phone for push notification
Note

Testing is critical. Do not hand off to the client until every machine has been through at least one complete cycle: sensor detects running → hours accumulate → threshold reached → work order created → technician notified → work order completed → counter resets for next interval. Keep a testing checklist and have both the MSP tech and the client's maintenance manager sign off. Document any machines where sensor readings are unreliable (e.g., machines with very low current draw that barely exceed the running threshold) and adjust the current threshold in Node-RED accordingly.

Step 12: Deploy to Production and Expand

After successful validation on the initial pilot group (5-10 machines), expand to the remaining equipment.

1
Review pilot results with the client — confirm PM thresholds are appropriate, notifications are working, and technicians are comfortable with the mobile workflow.
2
Install sensors on remaining machines following the same Phase 3 process.
3
Add new assets, meters, and PM templates to Fiix.
4
Update Node-RED flows with new asset configurations.
5
Adjust Grafana dashboards to include new assets.
6
Schedule a 30-day review after full deployment to fine-tune thresholds based on actual utilization data.
Add a new asset to the Node-RED asset_config context, then redeploy the flow
json
# To add a new asset to the Node-RED configuration, update the asset_config context:
# In Node-RED, edit the 'Asset Configuration' change node and add the new asset:
{
  "asset_id": "PRESS-03",
  "sensor_topic": "plant/zone2/PRESS03/current",
  "current_threshold": 5.0,
  "fiix_meter_id": 67890,
  "pm_thresholds": [
    { "hours": 250, "pm_type": "Lubrication" },
    { "hours": 1000, "pm_type": "Major Service" }
  ]
}

# Deploy the updated flow in Node-RED
Note

Expansion should be faster than the initial pilot — the infrastructure is already in place. Budget 15-30 minutes per machine for sensor installation (with electrician) and 10-15 minutes per machine for CMMS/Node-RED configuration. If the client wants to add equipment gradually over time, provide them a simple request form: asset name, electrical specs, desired PM intervals. The MSP can handle sensor installation and configuration as a recurring billable service.

Custom AI Components

Docker Compose Stack for Edge Server

Type: integration Complete Docker Compose configuration that deploys all four core services (Mosquitto MQTT, Node-RED, InfluxDB, Grafana) on the edge server as a single managed stack. This ensures consistent deployment, easy updates, and simplified backup.

Implementation:

docker-compose.yml
yaml
# Save to ~/pm-automation/docker-compose.yml

# docker-compose.yml
# Save to ~/pm-automation/docker-compose.yml

version: '3.8'

services:
  mosquitto:
    image: eclipse-mosquitto:2
    container_name: pm-mosquitto
    restart: unless-stopped
    ports:
      - '1883:1883'
      - '9001:9001'
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - ./mosquitto/data:/mosquitto/data
      - ./mosquitto/log:/mosquitto/log
    networks:
      - pm-network

  nodered:
    image: nodered/node-red:3-18
    container_name: pm-nodered
    restart: unless-stopped
    ports:
      - '1880:1880'
    volumes:
      - ./nodered/data:/data
    environment:
      - TZ=America/Chicago
      - FIIX_CLIENT_ID=${FIIX_CLIENT_ID}
      - FIIX_CLIENT_SECRET=${FIIX_CLIENT_SECRET}
      - FIIX_API_URL=https://api.fiixsoftware.com/api/v2
    depends_on:
      - mosquitto
      - influxdb
    networks:
      - pm-network

  influxdb:
    image: influxdb:1.8
    container_name: pm-influxdb
    restart: unless-stopped
    ports:
      - '8086:8086'
    volumes:
      - ./influxdb/data:/var/lib/influxdb
    environment:
      - INFLUXDB_DB=pm_automation
      - INFLUXDB_ADMIN_USER=admin
      - INFLUXDB_ADMIN_PASSWORD=${INFLUXDB_ADMIN_PASSWORD}
      - INFLUXDB_USER=pm_writer
      - INFLUXDB_USER_PASSWORD=${INFLUXDB_USER_PASSWORD}
      - INFLUXDB_READ_USER=pm_reader
      - INFLUXDB_READ_USER_PASSWORD=${INFLUXDB_READ_PASSWORD}
    networks:
      - pm-network

  grafana:
    image: grafana/grafana-oss:10.2.0
    container_name: pm-grafana
    restart: unless-stopped
    ports:
      - '3000:3000'
    volumes:
      - ./grafana/data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
      - GF_USERS_ALLOW_SIGN_UP=false
    depends_on:
      - influxdb
    networks:
      - pm-network

networks:
  pm-network:
    driver: bridge

# Create a .env file alongside docker-compose.yml:
# FIIX_CLIENT_ID=your_client_id
# FIIX_CLIENT_SECRET=your_client_secret
# INFLUXDB_ADMIN_PASSWORD=securepassword1
# INFLUXDB_USER_PASSWORD=securepassword2
# INFLUXDB_READ_PASSWORD=securepassword3
# GRAFANA_ADMIN_PASSWORD=securepassword4

Run-Hour Accumulation Engine (Node-RED Flow)

The core Node-RED flow that subscribes to MQTT sensor data, determines machine running state based on current draw thresholds, accumulates run hours per asset with persistent storage, writes time-series data to InfluxDB, and pushes updated meter readings to Fiix CMMS API at configurable intervals. Handles edge cases including sensor dropout detection, battery low warnings, and graceful recovery from restarts.

Node-RED Flow JSON
json
# Import via Menu → Import → Clipboard. Implements the complete run-hour
# accumulation and PM trigger pipeline.

// Node-RED Flow JSON — Import via Menu → Import → Clipboard
// This flow implements the complete run-hour accumulation and PM trigger pipeline.
// 
// FLOW OVERVIEW:
// 1. MQTT In nodes subscribe to sensor topics per zone
// 2. JSON parse node extracts current_rms value
// 3. Asset Lookup function maps sensor_id to asset configuration
// 4. Running State Detector compares current to threshold
// 5. Hour Accumulator increments run hours when machine is running
// 6. InfluxDB Write stores data points
// 7. Fiix API Updater pushes meter readings at scheduled intervals
// 8. Sensor Health Monitor detects stale/missing sensors
//
// CONFIGURATION:
// Edit the 'Asset Configuration' function node to define your assets.
// Each asset needs: asset_id, sensor_id, current_threshold_amps, fiix_meter_id

[
  {
    "id": "mqtt-broker-config",
    "type": "mqtt-broker",
    "name": "Local MQTT Broker",
    "broker": "mosquitto",
    "port": "1883",
    "clientid": "nodered-pm-engine",
    "autoConnect": true,
    "usetls": false,
    "credentials": {
      "user": "pm_system",
      "password": ""
    }
  },
  {
    "id": "mqtt-in-sensors",
    "type": "mqtt in",
    "name": "All Sensor Data",
    "topic": "plant/+/+/current",
    "qos": "1",
    "datatype": "json",
    "broker": "mqtt-broker-config",
    "wires": [["parse-sensor-data"]]
  },
  {
    "id": "parse-sensor-data",
    "type": "function",
    "name": "Parse & Enrich Sensor Data",
    "func": "// Extract zone and machine from MQTT topic\n// Topic format: plant/{zone}/{machine_id}/current\nconst topicParts = msg.topic.split('/');\/\nmsg.zone = topicParts[1];\nmsg.machine_id = topicParts[2];\nmsg.current_rms = msg.payload.current_rms || 0;\nmsg.battery_v = msg.payload.battery_v || 0;\nmsg.sensor_id = msg.payload.sensor_id || msg.machine_id;\nmsg.timestamp = new Date().toISOString();\nreturn msg;",
    "wires": [["asset-lookup"]]
  },
  {
    "id": "asset-lookup",
    "type": "function",
    "name": "Asset Configuration Lookup",
    "func": "// ============================================\n// ASSET CONFIGURATION - EDIT THIS FOR EACH CLIENT\n// ============================================\nconst ASSETS = {\n  'CNC01': {\n    asset_id: 'CNC01',\n    asset_name: 'CNC Mill #1 (Haas VF-2SS)',\n    sensor_id: 'NCD-001',\n    current_threshold_amps: 3.0,\n    fiix_meter_id: 12345,\n    fiix_asset_id: 67890,\n    pm_schedules: [\n      { type: 'Lubrication', interval_hours: 250 },\n      { type: 'Filter Change', interval_hours: 500 },\n      { type: 'Major Service', interval_hours: 2000 }\n    ]\n  },\n  'CNC02': {\n    asset_id: 'CNC02',\n    asset_name: 'CNC Mill #2 (Haas VF-2SS)',\n    sensor_id: 'NCD-002',\n    current_threshold_amps: 3.0,\n    fiix_meter_id: 12346,\n    fiix_asset_id: 67891,\n    pm_schedules: [\n      { type: 'Lubrication', interval_hours: 250 },\n      { type: 'Filter Change', interval_hours: 500 },\n      { type: 'Major Service', interval_hours: 2000 }\n    ]\n  },\n  'COMP01': {\n    asset_id: 'COMP01',\n    asset_name: 'Air Compressor #1 (IR R-Series)',\n    sensor_id: 'NCD-010',\n    current_threshold_amps: 5.0,\n    fiix_meter_id: 12350,\n    fiix_asset_id: 67895,\n    pm_schedules: [\n      { type: 'Oil Change', interval_hours: 500 },\n      { type: 'Air Filter', interval_hours: 250 },\n      { type: 'Separator Element', interval_hours: 4000 }\n    ]\n  }\n  // ADD MORE ASSETS HERE following the same pattern\n};\n\nconst asset = ASSETS[msg.machine_id];\nif (!asset) {\n  node.warn('Unknown machine_id: ' + msg.machine_id);\n  return null; // Drop unknown sensors\n}\nmsg.asset = asset;\nreturn msg;",
    "wires": [["running-state-detector", "sensor-health-check", "battery-check"]]
  },
  {
    "id": "running-state-detector",
    "type": "function",
    "name": "Running State Detector",
    "func": "// Determine if machine is running based on current threshold\n// Uses hysteresis to prevent chattering at threshold boundary\nconst HYSTERESIS = 0.5; // amps\nconst asset = msg.asset;\nconst current = msg.current_rms;\n\n// Get previous state from persistent context\nconst stateKey = 'state_' + asset.asset_id;\nconst prevState = flow.get(stateKey) || { running: false, last_change: Date.now() };\n\nlet isRunning;\nif (prevState.running) {\n  // Machine was running — only stop if current drops below (threshold - hysteresis)\n  isRunning = current >= (asset.current_threshold_amps - HYSTERESIS);\n} else {\n  // Machine was stopped — only start if current exceeds (threshold + hysteresis)\n  isRunning = current >= (asset.current_threshold_amps + HYSTERESIS);\n}\n\n// Detect state change\nconst stateChanged = (isRunning !== prevState.running);\nif (stateChanged) {\n  prevState.last_change = Date.now();\n  node.log(asset.asset_id + ' state changed to: ' + (isRunning ? 'RUNNING' : 'STOPPED'));\n}\nprevState.running = isRunning;\nflow.set(stateKey, prevState);\n\nmsg.is_running = isRunning;\nmsg.state_changed = stateChanged;\nreturn msg;",
    "wires": [["hour-accumulator"]]
  },
  {
    "id": "hour-accumulator",
    "type": "function",
    "name": "Run Hour Accumulator",
    "func": "// Accumulate run hours using persistent file-backed context\n// Uses time delta between readings to calculate fractional hours\nconst asset = msg.asset;\nconst hoursKey = 'hours_' + asset.asset_id;\nconst lastSeenKey = 'lastseen_' + asset.asset_id;\n\n// Get persisted values\nlet totalHours = global.get(hoursKey, 'file') || 0;\nconst lastSeen = global.get(lastSeenKey, 'file') || Date.now();\nconst now = Date.now();\n\n// Calculate time delta in hours\nconst deltaMs = now - lastSeen;\nconst deltaHours = deltaMs / (1000 * 60 * 60);\n\n// Safety: ignore deltas > 1 hour (indicates restart or gap)\nif (deltaHours > 1.0) {\n  node.warn(asset.asset_id + ': Large time gap detected (' + deltaHours.toFixed(2) + ' hrs). Skipping accumulation for this interval.');\n} else if (msg.is_running && deltaHours > 0) {\n  totalHours += deltaHours;\n}\n\n// Persist updated values\nglobal.set(hoursKey, totalHours, 'file');\nglobal.set(lastSeenKey, now, 'file');\n\nmsg.run_hours_total = Math.round(totalHours * 100) / 100;\nmsg.delta_hours = Math.round(deltaHours * 1000) / 1000;\n\nreturn msg;",
    "wires": [["influxdb-write", "fiix-update-throttle"]]
  },
  {
    "id": "influxdb-write",
    "type": "function",
    "name": "Format for InfluxDB",
    "func": "msg.payload = [\n  {\n    measurement: 'equipment_runtime',\n    tags: {\n      asset_id: msg.asset.asset_id,\n      asset_name: msg.asset.asset_name,\n      zone: msg.zone\n    },\n    fields: {\n      current_rms: msg.current_rms,\n      is_running: msg.is_running ? 1 : 0,\n      run_hours_total: msg.run_hours_total,\n      battery_v: msg.battery_v\n    },\n    timestamp: new Date()\n  }\n];\nreturn msg;",
    "wires": [["influxdb-out"]]
  },
  {
    "id": "influxdb-out",
    "type": "influxdb out",
    "name": "Write to InfluxDB",
    "influxdb": "influxdb-config",
    "measurement": "",
    "wires": []
  },
  {
    "id": "influxdb-config",
    "type": "influxdb",
    "name": "PM Automation DB",
    "hostname": "influxdb",
    "port": "8086",
    "database": "pm_automation",
    "credentials": {
      "username": "pm_writer",
      "password": ""
    }
  },
  {
    "id": "fiix-update-throttle",
    "type": "delay",
    "name": "Throttle: 1 msg per asset per hour",
    "pauseType": "rate",
    "timeout": "1",
    "timeoutUnits": "hours",
    "rate": "1",
    "nbRateUnits": "1",
    "rateUnits": "hour",
    "randomFirst": "1",
    "randomLast": "5",
    "randomUnits": "seconds",
    "drop": true,
    "wires": [["fiix-auth"]]
  },
  {
    "id": "fiix-auth",
    "type": "function",
    "name": "Fiix OAuth2 Token Manager",
    "func": "// Manage Fiix API OAuth2 token with caching\nconst tokenData = flow.get('fiix_token') || {};\nconst now = Date.now();\n\n// Check if token is still valid (with 5-minute buffer)\nif (tokenData.access_token && tokenData.expires_at && tokenData.expires_at > (now + 300000)) {\n  msg.fiix_token = tokenData.access_token;\n  return [msg, null]; // First output: token valid, proceed\n}\n\n// Token expired or missing — request new token\nconst authMsg = {\n  method: 'POST',\n  url: (env.get('FIIX_API_URL') || 'https://api.fiixsoftware.com/api/v2') + '/auth/token',\n  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n  payload: 'grant_type=client_credentials' +\n           '&client_id=' + encodeURIComponent(env.get('FIIX_CLIENT_ID')) +\n           '&client_secret=' + encodeURIComponent(env.get('FIIX_CLIENT_SECRET')),\n  _originalMsg: msg\n};\nreturn [null, authMsg]; // Second output: need new token",
    "outputs": 2,
    "wires": [["fiix-meter-update"], ["fiix-auth-request"]]
  },
  {
    "id": "fiix-auth-request",
    "type": "http request",
    "name": "Fiix Auth Request",
    "method": "POST",
    "ret": "obj",
    "wires": [["fiix-auth-response"]]
  },
  {
    "id": "fiix-auth-response",
    "type": "function",
    "name": "Cache Auth Token",
    "func": "if (msg.statusCode === 200 && msg.payload.access_token) {\n  const tokenData = {\n    access_token: msg.payload.access_token,\n    expires_at: Date.now() + (msg.payload.expires_in * 1000)\n  };\n  flow.set('fiix_token', tokenData);\n  \n  // Restore original message and add token\n  const origMsg = msg._originalMsg;\n  origMsg.fiix_token = tokenData.access_token;\n  return origMsg;\n} else {\n  node.error('Fiix auth failed: ' + JSON.stringify(msg.payload));\n  return null;\n}",
    "wires": [["fiix-meter-update"]]
  },
  {
    "id": "fiix-meter-update",
    "type": "function",
    "name": "Format Fiix Meter Reading",
    "func": "msg.method = 'POST';\nmsg.url = (env.get('FIIX_API_URL') || 'https://api.fiixsoftware.com/api/v2') + '/meter-readings';\nmsg.headers = {\n  'Authorization': 'Bearer ' + msg.fiix_token,\n  'Content-Type': 'application/json'\n};\nmsg.payload = {\n  meterId: msg.asset.fiix_meter_id,\n  reading: msg.run_hours_total,\n  readingDate: new Date().toISOString()\n};\n\nnode.log('Updating Fiix meter ' + msg.asset.fiix_meter_id + ' for ' + msg.asset.asset_id + ': ' + msg.run_hours_total + ' hours');\nreturn msg;",
    "wires": [["fiix-api-call"]]
  },
  {
    "id": "fiix-api-call",
    "type": "http request",
    "name": "Fiix API Call",
    "method": "use",
    "ret": "obj",
    "wires": [["fiix-response-handler"]]
  },
  {
    "id": "fiix-response-handler",
    "type": "function",
    "name": "Handle Fiix API Response",
    "func": "if (msg.statusCode >= 200 && msg.statusCode < 300) {\n  node.log('Fiix meter update successful: ' + JSON.stringify(msg.payload));\n} else if (msg.statusCode === 401) {\n  // Token expired mid-flight, clear cached token\n  flow.set('fiix_token', null);\n  node.warn('Fiix auth token expired, will refresh on next cycle');\n} else {\n  node.error('Fiix API error (' + msg.statusCode + '): ' + JSON.stringify(msg.payload));\n}\nreturn null; // Terminal node",
    "wires": []
  },
  {
    "id": "sensor-health-check",
    "type": "function",
    "name": "Sensor Health Monitor",
    "func": "// Track last-seen timestamp for each sensor\nconst healthKey = 'health_' + msg.asset.asset_id;\nflow.set(healthKey, {\n  last_seen: Date.now(),\n  battery_v: msg.battery_v,\n  current_rms: msg.current_rms,\n  sensor_id: msg.sensor_id\n});\nreturn null; // Just stores, no output",
    "wires": []
  },
  {
    "id": "battery-check",
    "type": "function",
    "name": "Battery Low Alert",
    "func": "// Alert if battery voltage drops below 2.5V (NCD sensors nominal 3.0-3.6V)\nconst LOW_BATTERY_THRESHOLD = 2.5;\nif (msg.battery_v > 0 && msg.battery_v < LOW_BATTERY_THRESHOLD) {\n  msg.payload = {\n    alert: 'LOW_BATTERY',\n    asset_id: msg.asset.asset_id,\n    asset_name: msg.asset.asset_name,\n    sensor_id: msg.sensor_id,\n    battery_v: msg.battery_v,\n    message: 'Sensor battery low on ' + msg.asset.asset_name + ' (' + msg.battery_v + 'V). Replace batteries soon.'\n  };\n  return msg;\n}\nreturn null;",
    "wires": [["alert-email"]]
  },
  {
    "id": "alert-email",
    "type": "e-mail",
    "name": "Send Battery Alert Email",
    "server": "smtp.client.com",
    "port": "587",
    "to": "msp-alerts@mspcompany.com",
    "subject": "[PM System] Low Battery Alert - {{payload.asset_name}}",
    "wires": []
  }
]
Note

Node-RED settings.js must enable file-backed context storage: contextStorage: { default: { module: 'memory' }, file: { module: 'localfilesystem' } } This ensures run-hour totals survive Node-RED restarts.

1
The 'Asset Configuration Lookup' function node is where you define all client assets. Edit this node for each deployment.
2
The Fiix update throttle is set to 1 message per asset per hour. This keeps API usage well within Fiix rate limits (60/min) while providing hourly granularity for meter readings. Fiix PM triggers are evaluated on each meter reading submission.
3
Sensor data arrives approximately every 60 seconds per sensor. The hour accumulator calculates fractional hours from time deltas between readings, so it's accurate even with irregular intervals.
4
The 1-hour max delta safety check prevents a Node-RED restart or sensor gap from incorrectly adding a large block of hours.

Sensor Health Monitoring Cron Flow

Type: workflow A scheduled Node-RED flow that runs every 15 minutes to check for sensors that have stopped reporting. If any sensor has not been seen for more than 10 minutes, it sends an alert to the MSP. This prevents silent failures where a sensor dies and run hours stop accumulating without anyone noticing — which would cause missed PMs.

Implementation:

Node-RED sensor health monitoring flow
json
# import alongside the main flow

// Additional Node-RED flow — import alongside the main flow
// This flow checks sensor health every 15 minutes

[
  {
    "id": "health-cron",
    "type": "inject",
    "name": "Every 15 Minutes",
    "repeat": "900",
    "crontab": "",
    "once": true,
    "onceDelay": "60",
    "payload": "",
    "payloadType": "date",
    "wires": [["check-all-sensors"]]
  },
  {
    "id": "check-all-sensors",
    "type": "function",
    "name": "Check All Sensor Health",
    "func": "// List of all expected asset IDs (must match Asset Configuration)\nconst EXPECTED_ASSETS = ['CNC01', 'CNC02', 'COMP01']; // EDIT FOR EACH CLIENT\nconst STALE_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes\nconst now = Date.now();\nconst alerts = [];\n\nfor (const assetId of EXPECTED_ASSETS) {\n  const healthKey = 'health_' + assetId;\n  const health = flow.get(healthKey);\n  \n  if (!health) {\n    alerts.push({\n      asset_id: assetId,\n      issue: 'NEVER_SEEN',\n      message: assetId + ': Sensor has never reported data. Check installation.'\n    });\n  } else {\n    const age = now - health.last_seen;\n    if (age > STALE_THRESHOLD_MS) {\n      const minutesAgo = Math.round(age / 60000);\n      alerts.push({\n        asset_id: assetId,\n        issue: 'STALE',\n        message: assetId + ': Last sensor reading was ' + minutesAgo + ' minutes ago. Possible sensor failure or wireless connectivity issue.',\n        last_battery_v: health.battery_v\n      });\n    }\n  }\n}\n\nif (alerts.length > 0) {\n  msg.payload = {\n    alert_type: 'SENSOR_HEALTH',\n    alert_count: alerts.length,\n    alerts: alerts,\n    summary: alerts.length + ' sensor(s) reporting issues: ' + alerts.map(a => a.asset_id).join(', ')\n  };\n  msg.topic = 'Sensor Health Alert';\n  return msg;\n}\nreturn null; // No alerts, no output",
    "wires": [["health-alert-email"]]
  },
  {
    "id": "health-alert-email",
    "type": "e-mail",
    "name": "Send Health Alert to MSP",
    "server": "smtp.mspcompany.com",
    "port": "587",
    "to": "noc@mspcompany.com, maintenance.manager@client.com",
    "subject": "[PM System] Sensor Health Alert - {{payload.summary}}",
    "wires": []
  }
]

// NOTE: Update the EXPECTED_ASSETS array to match all monitored assets.
// This flow is essential for MSP monitoring — it catches the failure mode
// where a sensor silently dies, which would otherwise result in missed PMs
// because run hours stop accumulating.

Run-Hour Counter Reset Handler

Type: workflow A Node-RED flow that listens for work order completion events from Fiix (via webhook or polling) and resets the run-hour counter for the associated asset. This ensures the next PM cycle starts counting from zero after maintenance is performed. Without this, the system would only trigger the PM once and never again.

Implementation:

Node-RED flow for handling PM work order completions via Fiix webhook or polling
javascript
// Node-RED flow for handling PM work order completions
// Two approaches: Webhook (preferred) or Polling

// APPROACH 1: Fiix Webhook (if available on your Fiix plan)
[
  {
    "id": "fiix-webhook-in",
    "type": "http in",
    "name": "Fiix Work Order Webhook",
    "url": "/api/fiix/wo-complete",
    "method": "post",
    "wires": [["validate-webhook"]]
  },
  {
    "id": "validate-webhook",
    "type": "function",
    "name": "Validate & Process WO Completion",
    "func": "// Validate the webhook payload\nconst wo = msg.payload;\n\n// Check if this is a PM work order completion\nif (wo.status !== 'Completed' || wo.workOrderType !== 'PreventiveMaintenance') {\n  // Not relevant, acknowledge but don't process\n  msg.statusCode = 200;\n  msg.payload = { status: 'ignored', reason: 'Not a completed PM' };\n  return [null, msg]; // Second output: HTTP response\n}\n\n// Map Fiix asset ID to our asset_id\nconst FIIX_ASSET_MAP = {\n  67890: 'CNC01',\n  67891: 'CNC02',\n  67895: 'COMP01'\n  // ADD ALL ASSET MAPPINGS HERE\n};\n\nconst assetId = FIIX_ASSET_MAP[wo.assetId];\nif (!assetId) {\n  node.warn('Unknown Fiix asset ID in WO completion: ' + wo.assetId);\n  msg.statusCode = 200;\n  msg.payload = { status: 'ignored', reason: 'Unknown asset' };\n  return [null, msg];\n}\n\nmsg.asset_id = assetId;\nmsg.fiix_wo = wo;\n\n// Determine which PM type was completed and calculate reset\n// IMPORTANT: Fiix meter-based PMs typically auto-set the next trigger\n// (e.g., current reading + interval). The meter itself is CUMULATIVE\n// and should NOT be reset to zero. Instead, Fiix tracks the 'next due at'\n// reading internally.\n//\n// However, we may want to log the completion for our own tracking:\nmsg.payload = {\n  event: 'PM_COMPLETED',\n  asset_id: assetId,\n  work_order: wo.code,\n  completed_at: wo.completedDate,\n  current_hours: global.get('hours_' + assetId, 'file') || 0\n};\n\nnode.log('PM completed for ' + assetId + ' at ' + msg.payload.current_hours + ' hours');\n\n// HTTP response\nconst responseMsg = { statusCode: 200, payload: { status: 'processed' } };\nreturn [msg, responseMsg];",
    "outputs": 2,
    "wires": [["log-pm-completion"], ["webhook-response"]]
  },
  {
    "id": "webhook-response",
    "type": "http response",
    "name": "Webhook ACK",
    "statusCode": "200",
    "wires": []
  },
  {
    "id": "log-pm-completion",
    "type": "function",
    "name": "Log PM Completion to InfluxDB",
    "func": "msg.payload = [{\n  measurement: 'pm_completions',\n  tags: {\n    asset_id: msg.asset_id\n  },\n  fields: {\n    work_order: msg.fiix_wo.code,\n    hours_at_completion: msg.payload.current_hours\n  },\n  timestamp: new Date()\n}];\nreturn msg;",
    "wires": [["influxdb-out"]]
  }
]

// APPROACH 2: Polling (if webhooks not available)
// Add an inject node on a 30-minute schedule that calls:
// GET /api/v2/work-orders?status=Completed&completedDateAfter=<last_check>
// Process results through similar logic as above.
//
// CRITICAL NOTE ON METER BEHAVIOR:
// Fiix uses CUMULATIVE meters for run hours. The meter value always goes UP.
// When you configure a meter-based PM in Fiix with interval=500 hours,
// Fiix will trigger the first PM at 500 hours, then automatically set the
// next trigger at 1000 hours, then 1500, etc.
// You do NOT need to reset the meter to zero after each PM.
// The Node-RED run-hour accumulator should therefore also be cumulative
// (which it is, as implemented in the main flow).
// This simplifies the system significantly — no reset logic needed.

NCD Sensor MQTT Bridge Configuration

Type: integration Configuration for the NCD wireless receiver to publish sensor data to the local Mosquitto MQTT broker in a standardized topic structure. NCD sensors communicate via their proprietary 900 MHz protocol to a USB receiver, which is then bridged to MQTT using NCD's Node-RED nodes.

Implementation:

Node-RED flow for NCD wireless sensor to MQTT bridge, including serial gateway config, sensor ID mapping, payload decode function, and Mosquitto bridge configuration
json
// NCD Wireless Sensor → MQTT Bridge
// This runs as a separate Node-RED flow on each gateway that has an NCD USB receiver

// Step 1: Install the NCD Node-RED package
// In Node-RED: Menu → Manage Palette → Install → search 'node-red-contrib-ncd-wireless'
// Or via command line: cd ~/.node-red && npm install node-red-contrib-ncd-wireless

// Step 2: Import this flow on each gateway:
[
  {
    "id": "ncd-serial-config",
    "type": "ncd-gateway-config",
    "name": "NCD USB Receiver",
    "comm_type": "serial",
    "serial_port": "/dev/ttyUSB0",
    "serial_baud": "115200"
  },
  {
    "id": "ncd-gateway-node",
    "type": "ncd-gateway-node",
    "name": "NCD Gateway",
    "connection": "ncd-serial-config",
    "wires": [["ncd-sensor-decode"]]
  },
  {
    "id": "ncd-sensor-decode",
    "type": "function",
    "name": "Decode NCD Sensor & Publish to MQTT",
    "func": "// NCD sensor data arrives as a JSON object with sensor_id and readings\n// Map NCD sensor IDs to machine IDs and zone\nconst SENSOR_MAP = {\n  'NCD-001': { zone: 'zone1', machine_id: 'CNC01' },\n  'NCD-002': { zone: 'zone1', machine_id: 'CNC02' },\n  'NCD-003': { zone: 'zone1', machine_id: 'CNC03' },\n  'NCD-004': { zone: 'zone1', machine_id: 'CNC04' },\n  'NCD-005': { zone: 'zone1', machine_id: 'CNC05' },\n  'NCD-010': { zone: 'zone2', machine_id: 'COMP01' },\n  'NCD-011': { zone: 'zone2', machine_id: 'COMP02' },\n  'NCD-012': { zone: 'zone2', machine_id: 'PRESS01' },\n  'NCD-013': { zone: 'zone2', machine_id: 'PRESS02' },\n  'NCD-014': { zone: 'zone2', machine_id: 'PRESS03' }\n  // ADD ALL SENSORS FOR THIS GATEWAY'S ZONE\n};\n\nconst sensorId = msg.payload.addr || msg.payload.sensor_id || 'unknown';\nconst mapping = SENSOR_MAP[sensorId];\n\nif (!mapping) {\n  node.warn('Unknown NCD sensor: ' + sensorId);\n  return null;\n}\n\n// Extract current reading (NCD AC current sensor type)\n// NCD current sensors report RMS current in amps\nconst currentRms = msg.payload.current || msg.payload.sensor_data?.current_rms || 0;\nconst batteryV = msg.payload.battery || msg.payload.sensor_data?.battery || 0;\n\nmsg.topic = 'plant/' + mapping.zone + '/' + mapping.machine_id + '/current';\nmsg.payload = {\n  sensor_id: sensorId,\n  current_rms: currentRms,\n  battery_v: batteryV,\n  timestamp: new Date().toISOString()\n};\n\nreturn msg;",
    "wires": [["mqtt-out-bridge"]]
  },
  {
    "id": "mqtt-out-bridge",
    "type": "mqtt out",
    "name": "Publish to MQTT",
    "topic": "",
    "qos": "1",
    "retain": false,
    "broker": "mqtt-broker-config",
    "wires": []
  }
]

// NOTE: The NCD node-red-contrib-ncd-wireless package handles the low-level
// serial communication with the NCD USB receiver. The sensor_id format and
// payload structure may vary slightly depending on the NCD firmware version.
// Test with one sensor first and inspect the raw payload in Node-RED debug
// to confirm the field names match. Adjust the decode function accordingly.
//
// If using the centralized edge server architecture (recommended), configure
// each gateway's Mosquitto as an MQTT bridge to forward all messages to the
// central broker:
//
// On gateway's mosquitto.conf, add:
// connection central-broker
// address 10.100.0.5:1883
// topic plant/# out 1
// remote_username pm_system
// remote_password <password>
// bridge_protocol_version mqttv311

Testing & Validation

  • SENSOR VALIDATION: For each of the 20 monitored machines, turn the machine on and verify within 2 minutes that a current reading >0 appears in the Node-RED debug console. Record the actual idle and running current values for each machine to calibrate the running threshold. Expected: idle current 0-1A, running current >3A for typical 3-phase motors.
  • RUNNING STATE DETECTION: For 5 representative machines, turn each machine on and off 3 times. Verify that Node-RED correctly detects state transitions within 2 sensor reporting cycles (~2 minutes). Check the Node-RED log for 'state changed to: RUNNING' and 'state changed to: STOPPED' messages. Verify no chattering (rapid state oscillation) when machine is at idle speed.
  • RUN-HOUR ACCUMULATION ACCURACY: Run a known machine for exactly 1 hour (use a stopwatch). Compare the Node-RED accumulated hours to the expected value. Acceptable accuracy: within ±3 minutes per hour (95% accuracy). If inaccurate, check the sensor reporting interval and time delta calculations.
  • RUN-HOUR PERSISTENCE: Verify that accumulated run hours survive a Node-RED restart. Record the current hours for 3 assets, restart the Node-RED Docker container (docker restart pm-nodered), wait 2 minutes, then verify the hours values are unchanged in the global context.
  • INFLUXDB DATA STORAGE: Query InfluxDB to confirm sensor data is being written using the curl command against http://10.100.0.5:8086/query with db=pm_automation. Expected: count should be approximately (number of sensors × 60) for the last hour at 1-minute intervals.
  • FIIX METER UPDATE: Check the Fiix CMMS web interface for each monitored asset. Navigate to the asset detail → Meters tab and verify that run-hour readings are being updated approximately every hour. The readings should be cumulative and always increasing (never decreasing or resetting).
  • PM WORK ORDER AUTO-CREATION: For a test asset, temporarily lower the PM threshold in Fiix to a value just above the current meter reading (e.g., if current reading is 50 hours, set PM interval to trigger at 51 hours). Wait for the next hourly meter update from Node-RED. Verify that Fiix automatically creates a PM work order with the correct template, task list, assigned technician, and parts list. Then reset the threshold to the production value.
  • MOBILE APP WORK ORDER FLOW: Have a maintenance technician open the Fiix mobile app, locate the test work order, review the PM checklist, mark tasks as complete, add any notes, and close the work order. Verify the work order status changes to 'Completed' in the web interface and that the next PM trigger is correctly set.
  • NOTIFICATION DELIVERY: Verify that when a PM work order is created, the assigned technician receives: (1) an email notification within 5 minutes, (2) a push notification on the Fiix mobile app, and (3) a Slack/Teams message (if configured via Zapier). Test overdue escalation by leaving a test work order open past the 24-hour threshold.
  • SENSOR HEALTH MONITORING: Disconnect one sensor (remove batteries) and wait 15 minutes. Verify that the sensor health cron flow sends an alert email to both the MSP NOC and the client's maintenance manager identifying the specific asset with the offline sensor.
  • BATTERY LOW ALERT: If possible, use a sensor with partially depleted batteries to verify the battery low alert triggers at the configured threshold (2.5V). If not practical, temporarily raise the threshold in the Node-RED battery check node to a value above the current battery voltage to trigger the alert, then reset.
  • GATEWAY FAILOVER: Power off one Moxa gateway to simulate a failure. Verify that: (1) sensors connected to that gateway stop reporting (expected), (2) sensors on other gateways continue functioning (expected), (3) the MSP receives a sensor health alert within 15 minutes for the affected assets. Power the gateway back on and verify data flow resumes within 5 minutes.
  • GRAFANA DASHBOARD: Verify all Grafana dashboard panels are populated with data: equipment run-hour gauges, running state indicators, utilization percentage charts, and sensor battery levels. Confirm the dashboard refreshes automatically (configure 30-second auto-refresh).
  • END-TO-END LATENCY: Measure the time from machine start to work order notification for a threshold-crossing event. Target: sensor reading (1 min) → Node-RED processing (<1 sec) → Fiix API update (hourly batch) → work order creation (<1 min) → notification (<5 min). Total end-to-end: work order should appear within the next hourly update cycle.
  • FULL SYSTEM REBOOT TEST: Simultaneously power off the edge server and all gateways (simulating a plant-wide power outage). Power everything back on. Verify: (1) all Docker containers auto-start (docker compose ps shows all 'running'), (2) Node-RED reconnects to MQTT broker, (3) run-hour totals are preserved (check global context), (4) sensor data flow resumes within 5 minutes of gateway power-up. The UPS should prevent this scenario, but test recovery regardless.
InfluxDB data storage validation query
bash
# confirms sensor data is being written over the last hour

curl -G 'http://10.100.0.5:8086/query' --data-urlencode 'db=pm_automation' --data-urlencode 'q=SELECT count(*) FROM equipment_runtime WHERE time > now() - 1h'

Client Handoff

The client handoff should be conducted as a structured 2-hour on-site session with the maintenance manager, lead technician(s), and plant manager. Cover the following topics:

1
System Overview (15 min): Walk through the architecture diagram showing sensors → gateways → edge server → CMMS. Explain in plain language how the system detects machine running state, accumulates hours, and triggers work orders. Emphasize that this is deterministic — not AI guessing, but precise measurement.
2
Fiix CMMS Training (45 min): Demonstrate the web interface: viewing assets, checking meter readings, opening and completing work orders, adjusting PM thresholds, adding new PM templates. Demonstrate the mobile app: receiving notifications, opening work orders, following checklists, marking tasks complete, adding photos/notes. Show how to add a new user account. Show how to run reports: work order completion rate, average response time, equipment uptime percentage.
3
Grafana Dashboard Tour (10 min): Show the plant overview dashboard. Explain each panel. Provide the read-only dashboard URL and ensure bookmarks are set on the plant manager's browser.
4
Threshold Adjustment Guide (15 min): Demonstrate how to change PM intervals in Fiix when the client learns their equipment needs different maintenance frequencies. Provide a printed quick-reference card with steps: (1) Log into Fiix, (2) Navigate to Asset → PM Schedule, (3) Edit the meter trigger value, (4) Save.
5
Troubleshooting Guide (15 min): Cover common issues: sensor battery replacement (how to identify which sensor, how to replace batteries, expected battery life); what to do if a work order doesn't appear (check Fiix meter readings, check Node-RED status); who to call (MSP support number and SLA response times).
6
Documentation Packet: Leave behind: (a) System architecture diagram (printed and digital), (b) Asset inventory spreadsheet with sensor-to-machine mappings, (c) PM threshold reference table (asset, PM type, interval hours), (d) Quick-reference cards for Fiix web and mobile app, (e) Troubleshooting decision tree, (f) MSP contact information and SLA terms, (g) Login credentials document (sealed envelope to maintenance manager), (h) Network diagram showing VLAN, IP addresses, gateway locations.
7
Success Criteria Review (20 min): Review together: all 20 machines reporting sensor data (show Grafana), all PM schedules configured and active in Fiix, test work order successfully created and completed, all technicians logged into mobile app and received test notification, Grafana dashboard accessible. Get sign-off from maintenance manager and plant manager on a completion checklist.

Maintenance

Monthly MSP Responsibilities

  • Review sensor health dashboard: confirm all sensors reporting, check battery voltage trends, identify sensors approaching end-of-life
  • Review MQTT broker logs for connection errors or dropped messages
  • Verify Node-RED flows are running without errors (check Node-RED admin UI and system logs)
  • Review Fiix meter readings for anomalies: gaps in data, unexpected values, or assets with zero accumulation
  • Check InfluxDB storage usage and verify retention policies are working (raw data >90 days should be auto-purged)
  • Confirm Docker containers are running and auto-restarting properly on the edge server
  • Review UPS battery health (replace batteries every 3-5 years per manufacturer recommendation)
  • Generate and send a monthly maintenance KPI report to the client: number of PMs triggered, PMs completed on time, average response time, equipment utilization rates, and sensor system uptime percentage

Quarterly MSP Responsibilities

  • PM threshold optimization review: analyze actual run-hour data against PM triggers. If equipment consistently triggers a PM every 3 weeks but the client reports the maintenance task isn't really needed that frequently, recommend increasing the interval. Conversely, if equipment shows signs of degradation between PMs, recommend shorter intervals
  • Review and apply firmware updates for Moxa gateways and NCD sensor firmware (if available)
  • Update Node-RED, Mosquitto, InfluxDB, and Grafana Docker images to latest stable versions (test in a lab environment first)
  • Review Fiix CMMS release notes for new features that could benefit the client
  • Conduct a brief on-site visit to physically inspect sensor installations, gateway enclosures, and cable integrity

Annual MSP Responsibilities

  • Comprehensive system health audit: verify all sensors, gateways, edge server, and CMMS are functioning correctly
  • Review network security: rotate MQTT passwords, API keys, and service account credentials
  • Replace sensor batteries proactively if voltage trends indicate they will reach end-of-life within the next year
  • Review the asset inventory: add new equipment, remove decommissioned equipment, update PM templates for any equipment changes
  • Reassess CMMS licensing: are there enough user seats? Do any new features on higher tiers justify an upgrade?
  • Present an annual summary report to the client: total PMs completed, estimated downtime avoided, ROI analysis

SLA Considerations

  • Critical (sensor system completely down, no PMs being triggered): 4-hour remote response, 8-hour on-site if needed
  • High (individual sensor or gateway offline, partial PM coverage lost): 8-hour remote response, next business day on-site
  • Medium (notification delivery issues, dashboard errors, non-critical integration issues): next business day remote response
  • Low (reporting enhancements, threshold adjustments, new asset additions): scheduled during next regular maintenance window

Escalation Path: Tier 1 (MSP helpdesk) → Tier 2 (MSP IoT/integration specialist) → Tier 3 (vendor support: NCD for sensor hardware issues, Moxa for gateway issues, Fiix for CMMS issues). Maintain active support contracts with all hardware vendors.

Alternatives

...

UpKeep CMMS + UpKeep Edge Sensors (All-in-One Vendor)

Instead of the Fiix + NCD + Node-RED stack, use UpKeep's integrated solution. UpKeep offers its own branded wireless IoT sensors (UpKeep Edge) that connect directly to the UpKeep CMMS platform without needing a separate MQTT broker, Node-RED, or custom integration logic. Sensors communicate via cellular or Wi-Fi gateways to UpKeep's cloud, and the platform handles run-hour calculation and work order triggering natively.

PLC-Direct Integration via OPC UA (No New Sensors)

If the client's equipment already has PLCs (Siemens S7, Allen-Bradley ControlLogix/CompactLogix, Mitsubishi, Fanuc), extract run-hour data directly from the PLC's internal counters via OPC UA. Use Kepware KEPServerEX or an open-source OPC UA client to read PLC tags, bridge to MQTT, and feed into the same Node-RED → Fiix pipeline. No CT sensors needed.

MaintainX + Manual Meter Entry (Lowest Cost)

Deploy MaintainX CMMS (Essentials at $16/user/month) with meter-based PM triggers, but have operators manually enter run-hour readings from existing analog hour meters or machine displays rather than deploying automated sensors. Operators record readings at the start and end of each shift via the MaintainX mobile app.

Samsara Full-Stack IoT Platform

Use Samsara's industrial IoT platform, which provides gateways, sensors, cloud platform, and dashboarding in a single integrated solution. Samsara gateways connect to equipment via CT sensors, vibration sensors, or direct PLC connections, and the Samsara cloud handles data collection, visualization, and integrates with CMMS platforms via API.

Vibration-Based Condition Monitoring (Predictive Enhancement)

Extend beyond run-hour-based PM to include vibration analysis on critical rotating equipment (motors, pumps, spindles, fans). Deploy NCD or ifm vibration sensors on the 5-10 most critical assets. Use vibration trending in Grafana to detect bearing wear, imbalance, and misalignment BEFORE failure, enabling true condition-based maintenance that supplements the hour-based schedule.

Want early access to the full toolkit?