Blog Post

Microsoft Developer Community Blog
8 MIN READ

Building a Smart Building HVAC Digital Twin with AI Copilot Using Foundry Local

Lee_Stott's avatar
Lee_Stott
Icon for Microsoft rankMicrosoft
Feb 17, 2026

Introduction

Building operations teams face a constant challenge: optimizing HVAC systems for energy efficiency while maintaining occupant comfort and air quality. Traditional building management systems display raw sensor data, temperatures, pressures, CO₂ levels—but translating this into actionable insights requires deep HVAC expertise. What if operators could simply ask "Why is the third floor so warm?" and get an intelligent answer grounded in real building state?

This article demonstrates building a sample smart building digital twin with an AI-powered operations copilot, implemented using DigitalTwin, React, Three.js, and Microsoft Foundry Local. You'll learn how to architect physics-based simulators that model thermal dynamics, implement 3D visualizations of building systems, integrate natural language AI control, and design fault injection systems for testing and training.

Whether you're building IoT platforms for commercial real estate, designing energy management systems, or implementing predictive maintenance for building automation, this sample provides proven patterns for intelligent facility operations.

Why Digital Twins Matter for Building Operations

Physical buildings generate enormous operational data but lack intelligent interpretation layers. A 50,000 square foot office building might have 500+ sensors streaming metrics every minute, zone temperatures, humidity levels, equipment runtimes, energy consumption. Traditional BMS (Building Management Systems) visualize this data as charts and gauges, but operators must manually correlate patterns, diagnose issues, and predict failures.

Digital twins solve this through physics-based simulation coupled with AI interpretation. Instead of just displaying current temperature readings, a digital twin models thermal dynamics, heat transfer rates, HVAC response characteristics, occupancy impacts. When conditions deviate from expectations, the twin compares observed versus predicted states, identifying root causes. Layer AI on top, and operators get natural language explanations: "The conference room is 3 degrees too warm because the VAV damper is stuck at 40% open, reducing airflow by 60%."

This application focuses on HVAC, the largest building energy consumer, typically 40-50% of total usage. Optimizing HVAC by just 10% through better controls can save thousands of dollars monthly while improving occupant satisfaction. The digital twin enables "what-if" scenarios before making changes: "What happens to energy consumption and comfort if we raise the cooling setpoint by 2 degrees during peak demand response events?"

Architecture: Three-Tier Digital Twin System

The application implements a clean three-tier architecture separating visualization, simulation, and state management:

The frontend uses React with Three.js for 3D visualization. Users see an interactive 3D model of the three-floor building with color-coded zones indicating temperature and CO₂ levels. Click any equipment, AHUs, VAVs, chillers, to see detailed telemetry. The control panel enables adjusting setpoints, running simulation steps, and activating demand response scenarios. Real-time charts display KPIs: energy consumption, comfort compliance, air quality levels.

The backend Node.js/Express server orchestrates simulation and state management. It maintains the digital twin state as JSON, the single source of truth for all equipment, zones, and telemetry. REST API endpoints handle control requests, simulation steps, and AI copilot queries. WebSocket connections push real-time updates to the frontend for live monitoring. The HVAC simulator implements physics-based models: 1R1C thermal models for zones, affinity laws for fan power, chiller COP calculations, CO₂ mass balance equations.

Foundry Local provides AI copilot capabilities. The backend uses foundry-local-sdk to query locally running models. Natural language queries ("How's the lobby temperature?") get answered with building state context. The copilot can explain anomalies, suggest optimizations, and even execute commands when explicitly requested.

Implementing Physics-Based HVAC Simulation

Accurate simulation requires modeling actual HVAC physics. The simulator implements several established building energy models:

// backend/src/simulator/thermal-model.js

class ZoneThermalModel {
  // 1R1C (one resistance, one capacitance) thermal model
  static calculateTemperatureChange(zone, delta_t_seconds) {
    const C_thermal = zone.volume * 1.2 * 1000; // Heat capacity (J/K)
    const R_thermal = zone.r_value * zone.envelope_area; // Thermal resistance
    
    // Internal heat gains (occupancy, equipment, lighting)
    const Q_internal = zone.occupancy * 100 + // 100W per person
                      zone.equipment_load + 
                      zone.lighting_load;
    
    // Cooling/heating from HVAC
    const airflow_kg_s = zone.vav.airflow_cfm * 0.0004719; // CFM to kg/s
    const c_p_air = 1006; // Specific heat of air (J/kg·K)
    const Q_hvac = airflow_kg_s * c_p_air * 
                   (zone.vav.supply_temp - zone.temperature);
    
    // Envelope losses
    const Q_envelope = (zone.outdoor_temp - zone.temperature) / R_thermal;
    
    // Net energy balance
    const Q_net = Q_internal + Q_hvac + Q_envelope;
    
    // Temperature change: Q = C * dT/dt
    const dT = (Q_net / C_thermal) * delta_t_seconds;
    
    return zone.temperature + dT;
  }
}

This model captures essential thermal dynamics while remaining computationally fast enough for real-time simulation. It accounts for internal heat generation from occupants and equipment, HVAC cooling/heating contributions, and heat loss through the building envelope.

The CO₂ model uses mass balance equations:

class AirQualityModel {
  static calculateCO2Change(zone, delta_t_seconds) {
    // CO₂ generation from occupants
    const G_co2 = zone.occupancy * 0.0052; // L/s per person at rest
    
    // Outdoor air ventilation rate
    const V_oa = zone.vav.outdoor_air_cfm * 0.000471947; // CFM to m³/s
    
    // CO₂ concentration difference (indoor - outdoor)
    const delta_CO2 = zone.co2_ppm - 400; // Outdoor ~400ppm
    
    // Mass balance: dC/dt = (G - V*ΔC) / Volume
    const dCO2_dt = (G_co2 - V_oa * delta_CO2) / zone.volume;
    
    return zone.co2_ppm + (dCO2_dt * delta_t_seconds);
  }
}

These models execute every simulation step, updating the entire building state:

async function simulateStep(twin, timestep_minutes) {
  const delta_t = timestep_minutes * 60; // Convert to seconds
  
  // Update each zone
  for (const zone of twin.zones) {
    zone.temperature = ZoneThermalModel.calculateTemperatureChange(zone, delta_t);
    zone.co2_ppm = AirQualityModel.calculateCO2Change(zone, delta_t);
  }
  
  // Update equipment based on zone demands
  for (const vav of twin.vavs) {
    updateVAVOperation(vav, twin.zones);
  }
  
  for (const ahu of twin.ahus) {
    updateAHUOperation(ahu, twin.vavs);
  }
  
  updateChillerOperation(twin.chiller, twin.ahus);
  updateBoilerOperation(twin.boiler, twin.ahus);
  
  // Calculate system KPIs
  twin.kpis = calculateSystemKPIs(twin);
  
  // Detect alerts
  twin.alerts = detectAnomalies(twin);
  
  // Persist updated state
  await saveTwinState(twin);
  
  return twin;
}

3D Visualization with React and Three.js

The frontend renders an interactive 3D building view that updates in real-time as conditions change. Using React Three Fiber simplifies Three.js integration with React's component model:

// frontend/src/components/BuildingView3D.jsx
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';

export function BuildingView3D({ twinState }) {
  return (
    
      
      
      
      
      {/* Render building floors */}
      {twinState.zones.map(zone => (
         selectZone(zone.id)}
        />
      ))}
      
      {/* Render equipment */}
      {twinState.ahus.map(ahu => (
        
      ))}
      
      
      
      
      
    
  );
}

function ZoneMesh({ zone, onClick }) {
  const color = getTemperatureColor(zone.temperature, zone.setpoint);
  
  return (
    
      
      
    
  );
}

function getTemperatureColor(current, setpoint) {
  const deviation = current - setpoint;
  if (Math.abs(deviation) < 1) return '#00ff00'; // Green: comfortable
  if (Math.abs(deviation) < 3) return '#ffff00'; // Yellow: acceptable
  return '#ff0000'; // Red: uncomfortable
}

This visualization immediately shows building state at a glance, operators see "hot spots" in red, comfortable zones in green, and can click any area for detailed metrics.

Integrating AI Copilot for Natural Language Control

The AI copilot transforms building data into conversational insights. Instead of navigating multiple screens, operators simply ask questions:

// backend/src/routes/copilot.js
import { FoundryLocalClient } from 'foundry-local-sdk';

const foundry = new FoundryLocalClient({
  endpoint: process.env.FOUNDRY_LOCAL_ENDPOINT
});

router.post('/api/copilot/chat', async (req, res) => {
  const { message } = req.body;
  
  // Load current building state
  const twin = await loadTwinState();
  
  // Build context for AI
  const context = buildBuildingContext(twin);
  
  const completion = await foundry.chat.completions.create({
    model: 'phi-4',
    messages: [
      {
        role: 'system',
        content: `You are an HVAC operations assistant for a 3-floor office building.
        
Current Building State:
${context}

Answer questions about equipment status, comfort conditions, and energy usage. Provide specific, actionable information based on the current data. Do not speculate beyond provided information.`
      },
      {
        role: 'user',
        content: message
      }
    ],
    temperature: 0.3,
    max_tokens: 300
  });
  
  res.json({
    response: completion.choices[0].message.content,
    model: 'phi-4',
    timestamp: new Date().toISOString()
  });
});

function buildBuildingContext(twin) {
  const alerts = twin.alerts.filter(a => a.active);
  const zoneStatus = twin.zones.map(z => 
    `${z.name}: ${z.temperature.toFixed(1)}°F (setpoint: ${z.setpoint}°F), CO₂: ${z.co2_ppm}ppm, ${z.occupancy} people`
  );
  
  return `
Zones:
${zoneStatus.join('\n')}

Active Alerts: ${alerts.length}
${alerts.map(a => `- ${a.severity}: ${a.message}`).join('\n')}

Energy: ${twin.kpis.total_power_kw.toFixed(1)} kW
Chiller: ${twin.chiller.percent_load}% load, ${twin.chiller.cop.toFixed(2)} COP
  `.trim();
}

Example interactions:

User: "Why is the third floor conference room warm?"

AI: "The Executive Suite on Floor 3 is currently 75.2°F, 3 degrees above the 72°F setpoint. This is due to high occupancy (8 people) and VAV-F3-EXEC operating at reduced airflow (285 CFM vs 450 CFM capacity). AHU-2 is supplying adequate cool air at 55°F. Recommend checking VAV damper position—it may be partially stuck."

User: "What's our current energy usage?"

AI: "Total building power consumption is 47.3 kW. Breakdown: Chiller (24.1 kW, 51%), AHU fans (12.8 kW, 27%), Pumps (7.2 kW, 15%), Other (3.2 kW, 7%). Chiller is operating at 62% load with a COP of 4.8, which is efficient for current conditions."

User: "Activate demand response level 2"

AI: "I cannot directly control building systems for safety reasons. To activate DR Level 2, use the Control Panel and click 'Demand Response' → 'Level 2'. This will raise cooling setpoints by 3°F and reduce auxiliary loads, targeting 15% energy reduction."

The AI provides grounded, specific answers citing actual equipment IDs and metrics. It refuses to directly execute control commands, instead guiding operators to explicit control interfaces, a critical safety pattern for building systems.

Fault Injection for Testing and Training

Real building operations experience equipment failures, stuck dampers, sensor drift, communication losses. The digital twin includes comprehensive fault injection capabilities to train operators and test control logic:

// backend/src/simulator/fault-injector.js

const FAULT_CATALOG = {
  chillerFailure: {
    description: 'Chiller compressor failure',
    apply: (twin) => {
      twin.chiller.status = 'FAULT';
      twin.chiller.cooling_output = 0;
      twin.alerts.push({
        id: 'chiller-fault',
        severity: 'CRITICAL',
        message: 'Chiller compressor failure - no cooling available',
        equipment: 'CHILLER-01'
      });
    }
  },
  
  stuckVAVDamper: {
    description: 'VAV damper stuck at current position',
    apply: (twin, vavId) => {
      const vav = twin.vavs.find(v => v.id === vavId);
      vav.damper_stuck = true;
      vav.damper_position_fixed = vav.damper_position;
      twin.alerts.push({
        id: `vav-stuck-${vavId}`,
        severity: 'HIGH',
        message: `VAV ${vavId} damper stuck at ${vav.damper_position}%`,
        equipment: vavId
      });
    }
  },
  
  sensorDrift: {
    description: 'Temperature sensor reading 5°F high',
    apply: (twin, zoneId) => {
      const zone = twin.zones.find(z => z.id === zoneId);
      zone.sensor_drift = 5.0;
      zone.temperature_measured = zone.temperature_actual + 5.0;
    }
  },
  
  communicationLoss: {
    description: 'Equipment communication timeout',
    apply: (twin, equipmentId) => {
      const equipment = findEquipmentById(twin, equipmentId);
      equipment.comm_status = 'OFFLINE';
      equipment.stale_data = true;
      twin.alerts.push({
        id: `comm-loss-${equipmentId}`,
        severity: 'MEDIUM',
        message: `Lost communication with ${equipmentId}`,
        equipment: equipmentId
      });
    }
  }
};

router.post('/api/twin/fault', async (req, res) => {
  const { faultType, targetEquipment } = req.body;
  
  const twin = await loadTwinState();
  const fault = FAULT_CATALOG[faultType];
  
  if (!fault) {
    return res.status(400).json({ error: 'Unknown fault type' });
  }
  
  fault.apply(twin, targetEquipment);
  await saveTwinState(twin);
  
  res.json({
    message: `Applied fault: ${fault.description}`,
    affectedEquipment: targetEquipment,
    timestamp: new Date().toISOString()
  });
});

Operators can inject faults to practice diagnosis and response. Training scenarios might include: "The chiller just failed during a heat wave, how do you maintain comfort?" or "Multiple VAV dampers are stuck, which zones need immediate attention?"

Key Takeaways and Production Deployment

Building a physics-based digital twin with AI capabilities requires balancing simulation accuracy with computational performance, providing intuitive visualization while maintaining technical depth, and enabling AI assistance without compromising safety.

Key architectural lessons:

  • Physics models enable prediction: Comparing predicted vs observed behavior identifies anomalies that simple thresholds miss
  • 3D visualization improves spatial understanding: Operators immediately see which floors or zones need attention
  • AI copilots accelerate diagnosis: Natural language queries get answers in seconds vs. minutes of manual data examination
  • Fault injection validates readiness: Testing failure scenarios prepares operators for real incidents
  • JSON state enables integration: Simple file-based state makes connecting to real BMS systems straightforward

For production deployment, connect the twin to actual building systems via BACnet, Modbus, or MQTT integrations. Replace simulated telemetry with real sensor streams. Calibrate model parameters against historical building performance. Implement continuous learning where the twin's predictions improve as it observes actual building behavior.

The complete implementation with simulation engine, 3D visualization, AI copilot, and fault injection system is available at github.com/leestott/DigitalTwin. Clone the repository and run the startup scripts to explore the digital twin, no building hardware required.

Resources and Further Reading

Updated Jan 30, 2026
Version 1.0
No CommentsBe the first to comment