Back to Energy Monitoring
Home Intelligence — Phase 3 of 3

Visual House Model
& Unified Dashboard

Prerequisite: Phases 1 & 2 complete. This phase adds a floor-plan-based visual interface as the primary dashboard view — imported from RoomSketcher screenshots, annotated with sensor and equipment positions, and overlaid with live temperature, humidity, and system status data. All Phase 1 and Phase 2 data panels become secondary views accessible from a unified navigation bar.
Phase 1 ERV + Weather ✓ Phase 2 Heat Pump + Power ✓ RoomSketcher Floor Plans Canvas Heat Map Overlay Unified Navigation
What Phase 3 Delivers

The House Becomes the Interface

Phases 1 and 2 give you excellent data — but it lives in tables, charts, and log entries. Phase 3 maps all of that data onto a spatial model of your actual house so the question "which room is cold right now and why?" can be answered by looking at the screen rather than reading numbers.

🗺️
Floor plan as the default view
The first thing you see when you open the dashboard. Each floor rendered as an importable image layer with live sensor data overlaid directly onto room locations.
Primary view
🌡️
Temperature heat map
A color gradient computed from your Aqara sensor readings using inverse distance weighting. Warm rooms glow amber. Cool areas shift blue. The gradient between sensors is interpolated — not estimated.
IDW interpolation
📍
Equipment markers with live status
ERV units, heat pump zones, and every Aqara sensor shown as clickable markers at their real positions on the floor plan. Click any marker to see full current readings.
Interactive markers
🧭
Unified navigation
Floor plan is the home view. One click to Energy, Planning, History, and Controls. All Phase 1 and Phase 2 data panels are still fully accessible — just no longer the first thing you see.
All views preserved
UI Concept

What the Dashboard Looks Like

🏠 Floor Plan ⚡ Energy 📅 Planning 📈 History ⚙️ Controls
Living Room Bedroom 1 Bed 2 Hallway Bath Kitchen / Dining 72° 38%rh 64° 45%rh 66° 43%rh 69° 41%rh ERV1 ERV2 HP ZONE outdoor 32°F FLOOR 1 B1 F1 warm ideal cool
Live Conditions
Living Room
72°F · 38% rh
Bedroom 1
64°F · 45% rh
Bedroom 2
66°F · 43% rh
Kitchen
69°F · 41% rh
Equipment
ERV Unit 1
Med · 42W
ERV Unit 2
Med · 40W
Heat Pump
Heat · 68°F SP
AUTOMATION ERV: Med — balanced
HP: pre-heat in 3h
Step 1 — Preparing Your Floor Plan

Getting a Clean Plan from RoomSketcher

RoomSketcher (and similar tools like Planner 5D, Floorplanner, or even a hand-drawn plan scanned flat) produces the background image layer. The web UI doesn't need a special format — a clean PNG screenshot is all it needs. The sensor positions are stored separately in a JSON config file, so the image never needs to be re-exported when sensors are added or moved.

📐
What makes a good source floor plan Draw one floor at a time in RoomSketcher. Use 2D top-down view — not 3D. Remove furniture if it makes room boundaries harder to see. Export at 1200–2000px wide minimum for crisp rendering on high-DPI screens. Name each exported PNG by floor: floor_basement.png, floor_1.png, floor_2.png.
📏
The most important step: consistent dimensions Before marking any sensor positions, decide on a final crop and resolution for each floor plan image and stick to it. Sensor positions are stored as percentages of image width and height — if you re-export the image at a different crop, all positions shift. Export once, save the original, use that file as-is forever.

Recommended RoomSketcher export settings

SettingValueWhy
View2D Floor PlanClean top-down — no perspective distortion
Resolution1600 × 1200px minimumSharp on retina/4K screens when scaled down in the UI
BackgroundWhite or very light grayHeat map overlay colors need light background to show clearly
WallsMedium weight, dark grayVisible through semi-transparent overlay
FurnitureRemove or keep minimalEquipment markers get confused with furniture symbols
LabelsRoom names on — measurements offRoom names help at-a-glance orientation
FormatPNGLossless — no JPEG compression artifacts around wall edges
Step 2 — Sensor Position Config

floor_plan_config.json — Mapping Sensors to Space

This is the bridge between your floor plan images and your live MQTT data. Every sensor and piece of equipment gets a position expressed as a percentage of the image's width and height. Percentages means the config works regardless of what screen size or zoom level the dashboard is viewed at.

// floor_plan_config.json
// Run the position editor tool (see Step 4) to set x/y values
// x: 0 = left edge, 100 = right edge of the floor plan image
// y: 0 = top edge, 100 = bottom edge
{
  "floors": [
    {
      "id":    "floor_1",
      "label": "Main Floor",
      "image": "static/floor_1.png",
      "sensors": [
        {
          "id":         "aqara_living_room",
          "label":      "Living Room",
          "mqtt_temp":  "aqara/living_room/temperature",
          "mqtt_humid": "aqara/living_room/humidity",
          "x": 22.5,  // percent of image width
          "y": 61.0   // percent of image height
        },
        {
          "id":         "aqara_bedroom_1",
          "label":      "Bedroom 1",
          "mqtt_temp":  "aqara/bedroom_1/temperature",
          "mqtt_humid": "aqara/bedroom_1/humidity",
          "x": 55.2, "y": 19.0
        }
      ],
      "equipment": [
        {
          "id":    "erv_unit_1",
          "type":  "erv",
          "label": "ERV 1",
          "mqtt_state": "erv/unit1/sensor/fan_mode/state",
          "mqtt_power": "erv/unit1/sensor/power_draw",
          "x": 4.5, "y": 44.0
        },
        {
          "id":    "hp_zone_1",
          "type":  "heatpump",
          "label": "HP Zone",
          "mqtt_state": "heatpump/unit1/mode",
          "mqtt_setpoint": "heatpump/unit1/target_temperature",
          "x": 41.0, "y": 57.5
        }
      ]
    },
    {
      "id":    "basement",
      "label": "Basement",
      "image": "static/floor_basement.png",
      "sensors": [
        {
          "id":         "aqara_basement",
          "label":      "Mechanical Room",
          "mqtt_temp":  "aqara/basement/temperature",
          "mqtt_humid": "aqara/basement/humidity",
          "x": 50.0, "y": 50.0
        }
      ],
      "equipment": [
        {
          "id":    "erv_unit_2",
          "type":  "erv",
          "label": "ERV 2",
          "mqtt_state": "erv/unit2/sensor/fan_mode/state",
          "mqtt_power": "erv/unit2/sensor/power_draw",
          "x": 12.0, "y": 35.0
        }
      ]
    }
  ]
}
Step 3 — Heat Map Rendering

How the Temperature Overlay Works

The heat map is rendered in real time on an HTML5 Canvas element that sits directly on top of your floor plan image. It uses Inverse Distance Weighting (IDW) — a simple algorithm that interpolates the temperature at any pixel based on its distance from all known sensor readings. Sensors closer to a pixel have more influence than sensors further away.

🧮
Why IDW is the right algorithm here It doesn't require training data, it uses every sensor you have regardless of count, new sensors automatically integrate, and you can tune one parameter (the power factor) to control how sharply the gradient transitions between sensor readings. A power of 2 gives smooth, natural-looking gradients. A power of 3–4 makes each sensor more "local," which is physically more accurate for separate rooms.
// JavaScript — floor plan canvas renderer
// Runs in the browser as part of the FastAPI-served web UI

const COLOR_SCALE = [
  { temp: 55, color: [70, 130, 200] },   // cold blue
  { temp: 65, color: [100, 180, 150] },  // teal — comfortable cool
  { temp: 70, color: [122, 196, 160] },  // green — ideal
  { temp: 74, color: [220, 160,  80] },  // amber — warm
  { temp: 80, color: [220,  90,  50] },  // orange — hot
];

function tempToColor(tempF) {
  for (let i = 0; i < COLOR_SCALE.length - 1; i++) {
    const lo = COLOR_SCALE[i], hi = COLOR_SCALE[i+1];
    if (tempF >= lo.temp && tempF <= hi.temp) {
      const t = (tempF - lo.temp) / (hi.temp - lo.temp);
      return lo.color.map((c, j) => Math.round(c + t * (hi.color[j] - c)));
    }
  }
  return COLOR_SCALE[COLOR_SCALE.length - 1].color;
}

function idwInterpolate(px, py, sensors, power = 2) {
  // px, py: pixel position as fraction of canvas width/height
  let weightedSum = 0, totalWeight = 0;
  for (const s of sensors) {
    const dx = px - s.x / 100;
    const dy = py - s.y / 100;
    const dist = Math.sqrt(dx*dx + dy*dy);
    if (dist < 0.001) return s.temp; // at the sensor itself
    const w = 1 / Math.pow(dist, power);
    weightedSum += w * s.temp;
    totalWeight += w;
  }
  return weightedSum / totalWeight;
}

function renderHeatmap(canvas, sensors, alpha = 0.45) {
  const ctx = canvas.getContext('2d');
  const { width, height } = canvas;
  const step = 6;  // pixel step — lower = smoother but slower
  const imgData = ctx.createImageData(width, height);

  for (let y = 0; y < height; y += step) {
    for (let x = 0; x < width; x += step) {
      const temp  = idwInterpolate(x/width, y/height, sensors);
      const [r,g,b] = tempToColor(temp);
      // Fill a step×step block for performance
      for (let dy = 0; dy < step && y+dy < height; dy++) {
        for (let dx = 0; dx < step && x+dx < width; dx++) {
          const idx = ((y+dy) * width + (x+dx)) * 4;
          imgData.data[idx]   = r;
          imgData.data[idx+1] = g;
          imgData.data[idx+2] = b;
          imgData.data[idx+3] = Math.round(alpha * 255);
        }
      }
    }
  }
  ctx.putImageData(imgData, 0, 0);
}

// Refresh every 30 seconds when sensor data updates via MQTT
async function refreshFloorPlan() {
  const data    = await fetch('/api/indoor').then(r => r.json());
  const config  = await fetch('/api/floor_plan_config').then(r => r.json());
  const floor   = config.floors.find(f => f.id === currentFloor);

  const sensors = floor.sensors.map(s => ({
    x: s.x, y: s.y,
    temp: data[s.id]?.temperature ?? 68
  }));

  renderHeatmap(document.getElementById('heatmap-canvas'), sensors);
  updateMarkers(floor, data);
}
Step 4 — Placing Sensors on the Map

Built-in Position Editor

Rather than manually measuring pixel coordinates, the dashboard includes a one-time setup tool that lets you click directly on the floor plan image to place each sensor. It writes the percentages back to floor_plan_config.json automatically.

# Access the position editor at:
http://your-server:8080/setup/floor-plan

# Editor workflow:
# 1. Select a floor from the dropdown
# 2. Select a sensor or equipment item from the sidebar list
# 3. Click its location on the floor plan image
# 4. x/y percentages are calculated and saved automatically
# 5. Repeat for all sensors and equipment
# 6. Toggle "Edit Mode" off to switch back to live view
🖱️
Position editor is only needed once per sensor Once you've placed a sensor it stays put in the config file. If you move a physical sensor to a new room, open the editor, select it, and click the new location. Nothing else needs to change — the MQTT topic stays the same.
Step 5 — Unified Dashboard Navigation

All Views Under One Roof

Phase 3 replaces the existing Phase 1/2 dashboard with a single unified application. The floor plan is the default landing view. All existing data panels from Phases 1 and 2 become tabs in the navigation bar — nothing is removed, just reorganized spatially.

📱
The floor plan view scales to phone screens On a small screen the floor plan fills the viewport and the sidebar collapses to a bottom drawer. The nav bar shrinks to icons only. This is the most valuable screen for walking around the house with your phone — you see what every room is doing at a glance without opening a single menu.

What appears when you click a sensor marker

Sensor typePopover shows
Aqara temp/humidityCurrent temp, humidity, dewpoint. 24h sparkline for both. Time since last reading. Battery level if applicable.
ERV unitCurrent fan mode and speed. Intake temp, exhaust temp, CFM. Power draw now and today's total. Filter life remaining. One-click mode override (Min/Med/Max).
Heat pump zoneCurrent mode and setpoint. Actual room temp. Compressor state and outdoor temp (from CN105). Current watts from Emporia. Active automation reason.
Rollout Sequence

Implementation Order

Phases 1 & 2 — Complete
All data must be flowing before the visual layer is built
Phase 3 is purely a presentation layer on top of the existing data pipeline. Every MQTT topic, every SQLite table, and every API endpoint from Phases 1 and 2 is used as-is. Nothing in the backend changes.
Week 1
Create and export floor plan images
Build each floor in RoomSketcher (or your preferred tool). Export as PNG at consistent dimensions. Copy to the static/ directory of the FastAPI backend. Confirm images load at the right path.
Week 1–2
Build the floor plan viewer + position editor
Add the canvas-based floor plan page to the FastAPI app. Implement the position editor. Work through each floor placing all sensors and equipment. Verify the config JSON looks right.
Week 2
Wire live MQTT data to the canvas overlay
Connect the /api/indoor endpoint to the heat map renderer. Add the equipment marker status layer. Test that clicking a marker shows correct live data. Tune the IDW power factor to taste — start at 2, increase if the gradient looks too smeared.
Week 2–3
Migrate to unified navigation
Replace the existing dashboard HTML with the unified single-page layout. Move Energy, Planning, History, and Controls into their respective tab panels. Make floor plan the default. Test on phone screen.
Ongoing
Expand overlays over time
Once the base view is working, additional overlays are straightforward to add: a humidity mode, an air quality mode (if CO₂ sensors are added in future), a power consumption mode that colors rooms by their contribution to current energy draw.
Future Overlay Modes

What Can Be Added Later

💧
Humidity mode
Same IDW gradient but driven by humidity readings instead of temperature. Immediately shows which room is at risk of mold or condensation without reading a single number.
Aqara data already available
🔥
Heat delta mode
Shows change in temperature since last hour rather than absolute temperature. Makes stratification, cold drafts, and ERV effects visible as movement rather than state.
SQLite history query
⚡
Power density mode
Colors rooms by their associated equipment energy draw — mechanical room glows brighter when the heat pump is working hard. Intuitive cost visualization without reading a watt meter.
Emporia data already available
🌬️
Air flow mode
Animate directional arrows showing which way air is moving based on ERV intake/exhaust positions and fan mode. Shows whether fresh air is reaching every room or short-circuiting.
ERV data already available