
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
$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
$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
$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)
$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
$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
$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
$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)
$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
$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)
$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
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
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
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
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)
$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.
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)
# 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 100NCD 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:
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.
# 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/helloIf 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.
Step 5: Deploy Edge Server (Optional but Recommended)
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.
# 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 psThe 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.
- 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)
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, SN98765POST 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"
}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
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
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
# 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 ~/.bashrcFiix 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.
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.
# 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)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 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 notificationTesting 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.
# 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-REDExpansion 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:
# 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=securepassword4Run-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.
# 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": []
}
]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.
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:
# 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
// 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:
// 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 mqttv311Testing & 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.
# 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:
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?