Roof and porch complete. Now the stone chimney with firebox glow and a volumetric smoke column:

import bpy, math, random

scene = bpy.context.scene
CX = 6.8
MAT_STONE = bpy.data.materials["StoneMat"]
MAT_DARK  = bpy.data.materials["DarkWood"]

def link_to(obj, cname):
    col = bpy.data.collections.get(cname)
    if col is None:
        col = bpy.data.collections.new(cname); scene.collection.children.link(col)
    for c in list(obj.users_collection): c.objects.unlink(obj)
    col.objects.link(obj)

CHI_X = CX + 4.75   # outside right wall (wall at CX+4.2)
CHI_Y = 1.1         # toward back half
BASE_Z = -0.35      # below ground slightly
TOP_Z  = 6.55       # above ridge (peak ~5.5)

# main stack: tapered via two boxes
stack1 = bpy.ops.mesh.primitive_cube_add(size=1, location=(CHI_X, CHI_Y, BASE_Z+(2.9)/2))
s1 = bpy.context.active_object; s1.name="ChimneyStack"
s1.scale=(1.15, 1.0, 1.45); bpy.ops.object.transform_apply(scale=True)
s1.data.materials.append(MAT_STONE); link_to(s1,"Chimney")
stack2 = bpy.ops.mesh.primitive_cube_add(size=1, location=(CHI_X, CHI_Y, BASE_Z+2.9+(TOP_Z-BASE_Z-2.9)/2))
s2 = bpy.context.active_object; s2.name="ChimneyTop"
s2.scale=(0.95, 0.82, (TOP_Z-BASE_Z-2.9)/2); bpy.ops.object.transform_apply(scale=True)
s2.data.materials.append(MAT_STONE); link_to(s2,"Chimney")

# shoulder piece bridging wall to chimney between window heights
sh = bpy.ops.mesh.primitive_cube_add(size=1, location=(CX+3.85, CHI_Y, 0.62))
sho = bpy.context.active_object; sho.name="ChimneyShoulder"
sho.scale=(1.95, 1.7, 0.5); bpy.ops.object.transform_apply(scale=True)
sho.data.materials.append(MAT_STONE); link_to(sho,"Chimney")

# crown slab + flue hole ring
crown = bpy.ops.mesh.primitive_cube_add(size=1, location=(CHI_X, CHI_Y, TOP_Z+0.10))
cr = bpy.context.active_object; cr.name="ChimneyCrown"
cr.scale=(0.62,0.54,0.11); bpy.ops.object.transform_apply(scale=True)
cr.data.materials.append(MAT_STONE); link_to(cr,"Chimney")

# inner dark flue box
flue = bpy.ops.mesh.primitive_cube_add(size=1, location=(CHI_X, CHI_Y, TOP_Z+0.28))
fl = bpy.context.active_object; fl.name="FlueDark"
fl.scale=(0.30,0.26,0.20); bpy.ops.object.transform_apply(scale=True)
flm = bpy.data.materials.get("FlueBlack") or bpy.data.materials.new("FlueBlack")
flm.use_nodes=True; nf=flm.node_tree; nf.nodes.clear()
of=nf.nodes.new('ShaderNodeOutputMaterial'); bf=nf.nodes.new('ShaderNodeBsdfPrincipled')
bf.inputs['Base Color'].default_value=(0.008,0.006,0.005,1); bf.inputs['Roughness'].default_value=1.0
nf.links.new(bf.outputs['BSDF'], of.inputs['Surface'])
fl.data.materials.append(flm); link_to(fl,"Chimney")

# ember glow inside flue
emb = bpy.ops.mesh.primitive_cube_add(size=1, location=(CHI_X, CHI_Y, TOP_Z+0.16))
em = bpy.context.active_object; em.name="EmberGlow"
em.scale=(0.22,0.19,0.06); bpy.ops.object.transform_apply(scale=True)
emmat = bpy.data.materials.new("EmberMat"); emmat.use_nodes=True
ne=emmat.node_tree; ne.nodes.clear()
oe=ne.nodes.new('ShaderNodeOutputMaterial'); ee=ne.nodes.new('ShaderNodeEmission')
ee.inputs['Color'].default_value=(1.0,0.36,0.08,1); ee.inputs['Strength'].default_value=24.0
ne.links.new(ee.outputs['Emission'], oe.inputs['Surface'])
em.data.materials.append(emmat); link_to(em,"Chimney")

# soot stain above flue (dark decal plane on crown top area)
soot = bpy.ops.mesh.primitive_plane_add(size=1, location=(CHI_X, CHI_Y, TOP_Z+0.215), rotation=(math.pi/2,0,0))
so = bpy.context.active_object; so.name="SootStain"
so.scale=(0.52,0.44,1); bpy.ops.object.transform_apply(scale=True)
smat=bpy.data.materials.new("SootMat"); smat.use_nodes=True
ns=smat.node_tree; ns.nodes.clear()
os_=ns.nodes.new('ShaderNodeOutputMaterial')
mixs=ns.nodes.new('ShaderNodeMixShader'); tr=ns.nodes.new('ShaderNodeBsdfTransparent')
df=ns.nodes.new('ShaderNodeBsdfDiffuse'); df.inputs['Color'].default_value=(0.01,0.01,0.01,1)
grad=ns.nodes.new('ShaderNodeTexGradient'); grad.gradient_type='LINEAR'
mapn=ns.nodes.new('ShaderNodeMapping'); mapn.inputs['Rotation'].default_value[2]=math.radians(-90)
tc=ns.nodes.new('ShaderNodeTexCoord')
rmp=ns.nodes.new('ShaderNodeValToRGB')
rmp.color_ramp.elements[0].position=0.25; rmp.color_ramp.elements[0].color=(1,1,1,1)
rmp.color_ramp.elements[1].position=0.75; rmp.color_ramp.elements[1].color=(0,0,0,1)
ns.links.new(tc.outputs['Generated'], mapn.inputs['Vector'])
ns.links.new(mapn.outputs['Vector'], grad.inputs['Vector'])
ns.links.new(grad.outputs['Fac'], rmp.inputs['Fac'])
ms=ns.nodes.new('ShaderNodeMixShader'); ms.inputs['Fac'].default_value=0.5
ns.links.new(tr.outputs['BSDF'], ms.inputs[1])
ns.links.new(df.outputs['BSDF'], ms.inputs[2])
ns.links.new(rmp.outputs['Color'], ms.inputs['Fac'])
ns.links.new(ms.outputs['Shader'], os_.inputs['Surface'])
so.data.materials.append(smat); link_to(so,"Chimney")

# ---------------- volumetric smoke column ----------------
bpy.ops.mesh.primitive_cylinder_add(vertices=16, radius=0.42, depth=13.0,
                                    location=(CHI_X, CHI_Y, TOP_Z+0.3+6.5))
smk = bpy.context.active_object; smk.name="SmokeColumn"
smk.rotation_euler=(math.radians(6), math.radians(3), 0)
for p in smk.data.polygons: p.use_smooth=True
skmat = bpy.data.materials.new("SmokeMat"); skmat.use_nodes=True
nk=skmat.node_tree; nk.nodes.clear()
ok_=nk.nodes.new('ShaderNodeOutputMaterial')
princ_out_vol = None
sc = nk.nodes.new('ShaderNodeVolumePrincipled'); sc.location=(-100,0)
sc.inputs['Density'].default_value=0.028
sc.inputs['Color'].default_value=(0.75,0.73,0.71,1)
sc.inputs['Anisotropy'].default_value=0.12
# density varies with noise so it looks broken up
nz = nk.nodes.new('ShaderNodeTexNoise'); nz.location=(-380,-60)
nz.inputs['Scale'].default_value=1.4; nz.inputs['Detail'].default_value=6
mr = nk.nodes.new('ShaderNodeMapRange'); mr.location=(-180,-60)
mr.inputs['From Min'].default_value=0.35; mr.inputs['From Max'].default_value=0.72
mr.inputs['To Min'].default_value=0.004; mr.inputs['To Max'].default_value=0.075
nk.links.new(nz.outputs['Fac'], mr.inputs['Value'])
# object coords for noise
tck = nk.nodes.new('ShaderNodeTexCoord'); tck.location=(-600,-60)
nk.links.new(tck.outputs['Object'], nz.inputs['Vector'])
nk.links.new(mr.outputs['Result'], sc.inputs['Density'])
nk.links.new(sc.outputs['Volume'], ok_.inputs['Volume'])
smk.data.materials.append(skmat)
smk.visible_camera=False
link_to(smk,"Chimney")

result={"chimney_top":TOP_Z}