Back to Energy Monitoring
Home Intelligence — Phase 2 of 3

Heat Pump Intelligence
& Power Validation

Prerequisite: Phase 1 complete. This phase adds Mitsubishi Hyper Heat control via CN105, Emporia Vue as an independent power-truth validator, temperature-aware pre-heat planning from Open-Meteo forecasts, and a watchdog enforcement loop. Goal: meaningful, measurable reduction in heating costs through forecast-driven decisions rather than reactive setpoint chasing.
Phase 1 ERV + Weather ✓ Mitsubishi Hyper Heat CN105 Emporia Vue Power Monitor COP-Aware Scheduler Watchdog + Enforcement
Prerequisites

Phase 1 Must Be Complete

Phase 1 ComponentWhy Phase 2 Depends on ItStatus
ERV on MQTT + SQLite loggingPhase 2 automation coordinates ERV and heat pump togetherREQUIRED
Aqara indoor sensors on MQTTIndoor temps drive pre-heat decisions and setpoint targetsREQUIRED
Open-Meteo polling runningForecast data is the core input to pre-heat schedulingREQUIRED
FastAPI backend runningPhase 2 adds modules to the same service — not a separate appREQUIRED
Read First
🚨
Mitsubishi Kumo Cloud is broken — do not disturb it As of early 2025 Mitsubishi migrated to a new "Comfort" app and the Kumo Cloud V3 API broke all third-party integrations. If your current Kumo Cloud setup is working, leave it completely alone — do not uninstall, do not update to the Comfort app. The replacement for Phase 2 is an ESPHome CN105 adapter which provides fully local control with more data than Kumo ever exposed. Once CN105 is live, Kumo Cloud access is no longer needed.
What Phase 2 Delivers

New Capabilities on Top of Phase 1

🌡️
True outdoor temp from the heat pump
The CN105 adapter exposes the Hyper Heat's own outdoor sensor — more accurate than Open-Meteo for the exact conditions the compressor is operating in.
CN105 → MQTT
⚡
Actual $/hour cost curve
Emporia Vue per-circuit wattage × your electricity rate = real cost per hour at any outdoor temperature. Not spec sheet estimates — your actual unit, your actual house.
Emporia + SQLite
📅
Pre-heat scheduling
When forecast shows conditions will get significantly worse, the system pre-warms the house during the current high-COP window rather than fighting low COP all night.
Open-Meteo forecast
🔍
Power validation watchdog
Emporia is the independent truth source. If the heat pump draws power when it shouldn't, or fails to respond to a command, the system detects it from the wire — not from what the unit says it's doing.
Ground truth enforcement
Step 1 — Mitsubishi Hyper Heat Hardware

CN105 Adapter — ESPHome Local Control

The CN105 port is a 5-pin JST connector on every Mitsubishi mini-split — it's the same port the Kumo Cloud WiFi adapter uses. An ESP32 running the SwiCago HeatPump ESPHome component replaces it entirely, giving fully local control with zero cloud dependency.

Hardware options

🛒
Pre-built adapter board
Search Etsy or GitHub for "mahtanar ESP32 CN105" or "SwiCago Mitsubishi ESPHome". Pre-wired JST connector, just flash ESPHome and plug in. May have limited stock.
Easiest — ~$25
🔧
DIY with spare ESP32-S3
You already have extra ESP32-S3 DevKitC boards and MP1584 buck converters from Phase 1. Add a JST PAP-5V-S connector (~$2). Wire it the same way as the ERV boards.
~$5 parts if you DIY

CN105 pinout

PinSignalConnect toNotes
112VMP1584 IN+Power for ESP32 via buck converter
2GNDMP1584 IN−, ESP GNDCommon ground
35VSkip — use buck outputLimited current — use the buck converter output instead
4TX (unit → ESP)ESP32 GPIO18 (RX)2400 baud, 8E1
5RX (ESP → unit)ESP32 GPIO17 (TX)Commands sent to unit

ESPHome config for CN105

# Add to a new yaml file: mitsubishi_hp.yaml
# Same secrets.yaml, same MQTT broker as Phase 1

external_components:
  - source:
      type: git
      url: https://github.com/SwiCago/HeatPump
    components: [mitsubishi_heatpump]

uart:
  id: hp_uart
  tx_pin: GPIO17
  rx_pin: GPIO18
  baud_rate: 2400

climate:
  - platform: mitsubishi_heatpump
    name: "Hyper Heat"
    uart_id: hp_uart

sensor:
  - platform: template
    name: "HP Outdoor Temp"         # exposed by component
  - platform: template
    name: "HP Compressor Frequency"    # 0 = off, >0 = running

mqtt:
  broker: 192.168.1.XXX
  topic_prefix: "heatpump/unit1"
✅
What CN105 exposes that Kumo Cloud never did True outdoor temperature from the heat pump's own sensors, compressor operating frequency (tells you actual load), defrost cycle status, actual vs. requested operating mode, and filter reminder state. Compressor frequency is especially useful — it's how you verify the unit is genuinely running vs. just in fan mode.
Step 2 — Emporia Vue Power Monitoring

Per-Circuit Watts as Ground Truth

The Emporia Vue doesn't care what the CN105 says. It sees electrons on the wire. This is what makes it the right validation layer — it's completely independent of the heat pump's self-reported state.

🏷️
Label these circuits in the Emporia app before starting Heat Pump — Compressor, Heat Pump — Air Handler, ERV Unit 1, ERV Unit 2. Accurate labeling is what makes the validation logic unambiguous. If the compressor and air handler are on the same circuit, label it as one.

Emporia polling — add to erv_monitor.py

# pip install pyemvue
import pyemvue
from pyemvue.enums import Scale, Unit

HP_IDLE_WATTS   = 50    # fan only / standby
HP_ACTIVE_WATTS = 800   # compressor running threshold

async def poll_emporia_loop(mqtt_client):
    vue = pyemvue.PyEmVue()
    vue.login(username='you@email.com', password='yourpassword',
              token_storage_file='emporia_keys.json')

    while True:
        channels = vue.get_recent_usage(
            scale=Scale.MINUTE.value, unit=Unit.WATTS.value
        )
        for ch in channels:
            mqtt_client.publish(
                f"power/{ch.name.lower().replace(' ','_')}/watts",
                ch.usage, qos=1
            )
        await asyncio.sleep(60)  # Emporia API: max 1/min
Step 3 — Validation Loop & Watchdog

Emporia as the Independent Enforcer

The full control + validation flow for every heat pump command. The CN105 tells you what the unit thinks it's doing. The Emporia tells you what it's actually drawing. The gap between those two is where failures live.

Automation Engine Decision HP should be: OFF / ON / setpoint X°F Send command via CN105 → MQTT climate/set: mode=off OR temperature=68 wait 90 seconds validate with both sources CN105 State Check mode = off / compressor_hz = 0 Emporia Power Check hp_circuit_watts < 50W ✓ CONFIRMED ⚠ MISMATCH → retry

Validation implementation

# In erv_monitor.py — add to automation engine

async def enforce_hp_command(unit_id: str, mode: str, setpoint: float = None):
    for attempt in range(3):
        # Send command via CN105 MQTT topic
        payload = {"mode": mode}
        if setpoint: payload["temperature"] = setpoint
        mqtt.publish(f"heatpump/{unit_id}/climate/set", json.dumps(payload))

        await asyncio.sleep(90)  # wait for compressor ramp-down

        cn105_mode  = await get_mqtt_state(f"heatpump/{unit_id}/mode")
        emporia_w   = await get_latest_watts("heat_pump_compressor")

        if mode == "off":
            confirmed = (cn105_mode in ("off", "fan_only")) \
                        and (emporia_w < HP_IDLE_WATTS)
        else:
            confirmed = cn105_mode == mode \
                        and emporia_w > HP_IDLE_WATTS

        if confirmed:
            await log_event("HP_COMMAND_CONFIRMED",
                            {"mode": mode, "attempts": attempt+1,
                             "watts": emporia_w})
            return True

        await log_event("HP_COMMAND_RETRY",
                        {"attempt": attempt+1, "cn105": cn105_mode,
                         "emporia_w": emporia_w})

    # Three attempts failed — alert and log
    await send_alert(f"HP {unit_id} not responding to {mode} after 3 attempts")
    return False


async def watchdog_loop():
    """60-second loop: if state drifts from intended, re-issue command."""
    while True:
        for unit_id in HP_UNITS:
            intended = INTENDED_STATE.get(unit_id)
            if not intended: continue

            cn105_mode = await get_mqtt_state(f"heatpump/{unit_id}/mode")
            emporia_w  = await get_latest_watts("heat_pump_compressor")

            # Detect unexpected power draw
            hp_should_be_off = intended["mode"] == "off"
            hp_drawing_power = emporia_w > HP_ACTIVE_WATTS

            if hp_should_be_off and hp_drawing_power:
                await log_event("HP_UNEXPECTED_POWER",
                                {"watts": emporia_w, "cn105": cn105_mode})
                await enforce_hp_command(unit_id, "off")

            # Detect state drift from external input
            elif cn105_mode != intended["mode"]:
                await log_event("HP_STATE_DRIFT",
                                {"actual": cn105_mode, "intended": intended})
                await enforce_hp_command(unit_id, **intended)

        await asyncio.sleep(60)
Step 4 — Pre-Heat Planning Engine

Forecast-Driven COP Scheduling

This is the highest-value part of Phase 2. The Hyper Heat's efficiency (COP) drops significantly as outdoor temperature falls. A pre-heat decision made 6–12 hours in advance — based on the Open-Meteo forecast already running from Phase 1 — can shift expensive low-COP runtime to cheap high-COP windows without reducing comfort at all.

💡
The fundamental insight Your house stores heat. Running the heat pump harder at 40°F (COP ~3.5) and letting the house coast through a predicted 15°F night (COP ~1.5) costs significantly less than maintaining temperature reactively through that cold night. The Emporia data builds your real COP curve — not spec sheet estimates.

Hyper Heat approximate COP by outdoor temperature

Outdoor TempApprox. COP$/hr at 1.5kW draw (example)Automation stance
47°F3.5 – 4.0~$0.14 effectiveRun freely — excellent efficiency
35°F2.5 – 3.0~$0.19 effectiveNormal operation, good window
17°F1.8 – 2.2~$0.26 effectiveMaintain, avoid extra cycling
5°F1.2 – 1.5~$0.38 effectiveReduce setpoint slightly, let thermal mass work
−13°F~1.0~$0.50 effectiveMinimum effective — hold, don't chase
📊
After ~2 weeks of data, replace the table above with your real curve Query SQLite: average heat pump watts grouped by 5°F outdoor temp buckets × Emporia circuit data. Your actual COP curve for your specific unit, your specific house, your specific electrical rate. This is what makes the scheduler genuinely accurate rather than spec-sheet theoretical.
# Real COP curve query — run after 2+ weeks of data
SELECT
  ROUND(hp_outdoor_temp / 5) * 5     AS temp_bucket_f,
  AVG(hp_power_watts)                AS avg_watts,
  AVG(hp_power_watts) / 1000.0
    * 0.14                            AS cost_per_hour,
  -- replace 0.14 with your actual $/kWh
  COUNT(*)                            AS samples
FROM hp_readings h
JOIN power_readings p ON h.ts = p.ts
WHERE hp_mode = 'heat'
  AND hp_outdoor_temp IS NOT NULL
GROUP BY temp_bucket_f
ORDER BY temp_bucket_f DESC;

Pre-heat scheduling logic

Forecast ConditionActionReason
now > 40°F AND forecast shows < 20°F within 8h → Raise setpoint +2°F now Pre-load thermal mass at high COP before cold arrives
20°F < outdoor < 45°F, steady for next 6h → Normal setpoint, run continuously Hyper Heat sweet spot — steady low-load is most efficient
outdoor < 5°F AND house within 1°F of setpoint → Drop setpoint −1°F Lowest COP window — let stored heat carry, reduce compressor demand
forecast shows warming > 15°F rise within 4h → Hold current setpoint — don't pre-heat Better conditions coming — inefficient to push heat in now
Emporia shows > 150% expected watts for outdoor temp → Alert: efficiency anomaly Unit working harder than COP curve predicts — possible coil/refrigerant issue
# Pre-heat scheduler — runs every 5 minutes
async def hp_scheduling_loop():
    while True:
        weather      = await get_latest_weather()
        indoor_temps = await get_indoor_temps()   # from Aqara via MQTT
        avg_indoor   = sum(indoor_temps.values()) / len(indoor_temps)

        now_f        = weather["outdoor_temp_f"]
        forecast_8h  = weather["temp_in_8h"]           # from Open-Meteo (Phase 1)
        forecast_4h  = weather["temp_in_4h"]

        current_sp   = await get_hp_setpoint("unit1")
        base_sp      = 68.0  # your normal setpoint

        if now_f > 40 and forecast_8h < 20:
            target_sp = base_sp + 2.0
            reason    = f"pre-heat: {now_f}°F now → {forecast_8h}°F in 8h"

        elif now_f < 5 and avg_indoor >= base_sp - 1:
            target_sp = base_sp - 1.0
            reason    = "extreme cold: drop setpoint, use stored heat"

        elif forecast_4h > now_f + 15:
            target_sp = base_sp
            reason    = "warming coming — hold normal setpoint"

        else:
            target_sp = base_sp
            reason    = "normal operation"

        if target_sp != current_sp:
            await enforce_hp_command("unit1", "heat", setpoint=target_sp)
            await log_event("HP_SETPOINT_CHANGE",
                            {"from": current_sp, "to": target_sp, "reason": reason})

        await asyncio.sleep(300)  # 5 minutes
Step 5 — Web UI Phase 2 Additions

New Dashboard Panels

New PanelContent
Heat Pump StatusCurrent mode, setpoint, actual indoor temp, compressor frequency, outdoor temp (from HP sensor), run hours today
Power & CostLive Watts per circuit (HP compressor, HP air handler, ERV ×2), $/hour at current draw, today's total kWh and cost estimate
Planning Horizon12-hour forecast with overlaid COP estimate and scheduled setpoint changes. Shows why the current decision was made.
Automation LogLast 20 decisions across both ERV and heat pump: timestamp, action taken, reason, outcome (confirmed/retry/failed)
Override ControlSingle toggle: pause all automation for 1h / 2h / 4h. During pause the watchdog suspends. Manual setpoint entry for heat pump. ERV manual mode select. All logged with timestamp.
Efficiency Trend30-day chart of actual $/day heating cost. Makes seasonal improvement and the impact of automation visible.
Rollout Sequence

Recommended Order

Phase 1 — Complete first
ERV + Aqara + Open-Meteo + Web UI
Both ERVs logging. Indoor sensors on MQTT. Weather-driven ERV automation running. Dashboard live. This baseline data is what makes Phase 2 scheduling accurate.
Week 1–2
Emporia Vue integration
Add pyemvue polling to the Python backend. Label circuits. Verify per-circuit wattage is flowing into SQLite. No automation yet — just data collection.
Week 2–3
CN105 adapter for Mitsubishi
Order or build the adapter. Flash ESPHome. Connect to MQTT. Verify HP state reporting — mode, setpoint, outdoor temp, compressor frequency all visible on dashboard. Still no automated control yet.
Week 3–5
Baseline data collection
Let the system log without issuing HP commands for 2 weeks. Build your real COP curve from Emporia + HP outdoor sensor data. Understand your house's thermal decay rate (how fast it cools per degree of outdoor temp). This shapes all future scheduling.
Week 5–6
Enable watchdog and validation
Turn on the enforcement loop. The system now monitors HP state against intended state and validates every commanded change against Emporia. No proactive scheduling yet — just ensuring commands are obeyed.
Week 6–8
Pre-heat scheduling live
Enable the forecast-driven scheduler with conservative rules tuned to your observed cost curve. Review the automation log daily for the first two weeks. Adjust thresholds based on what you see.