From 112ea5e2b1fd0814d0323cf38b0cd3447eea5c92 Mon Sep 17 00:00:00 2001 From: Hanks <3210385502@qq.com> Date: Tue, 14 Jul 2026 16:55:21 +0800 Subject: [PATCH] Update README, device_bridge and hema_controller --- README.md | 94 ++++++++++++++++++- .../benchmark/baselines/hema/device_bridge.py | 14 ++- .../baselines/hema/hema_controller.py | 8 +- 3 files changed, 108 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d5e910a..aaa2f40 100644 --- a/README.md +++ b/README.md @@ -1108,13 +1108,103 @@ controllers on identical VPP avoidance, comfort, and task-completion metrics. Native HEMA ReAct agent baseline. This baseline requires the original HEMA repository as an external dependency. -```text +#### Architecture Overview + +The HEMA baseline consists of three layers (all located in `experiments/benchmark/baselines/hema/`): + +``` +experiments/benchmark/baselines/hema/ +├── hema_controller.py # HEMAControlBaseline: main controller class +├── device_bridge.py # EnergyBridgeToHEMA: state synchronization bridge +└── __init__.py # get_hema_controller() + sys.path setup +``` + +#### Step 1: Install HEMA + +```bash +# From your project root git clone https://github.com/humanbuildingsynergy/HEMA.git +``` + +Install HEMA dependencies: + +```bash cd HEMA -# Install dependencies pip install -r requirements.txt ``` +#### Step 2: How the Adapter Works + +The adapter bridges EnergyBridge and HEMA through three files in `experiments/benchmark/baselines/hema/`: + +##### `__init__.py` + +Factory function that returns the HEMA controller class. Manages `sys.path` so HEMA imports resolve correctly without polluting the global namespace. If you move the HEMA clone to a different directory, update `_HEMA_ROOT` here. + +##### `device_bridge.py` — `EnergyBridgeToHEMA` + +| Function | What it does | +|----------|-------------| +| `ensure_device_config()` | Resets HEMA's global device state and rebuilds it from EnergyBridge's `appliance_config` + live `eplus_state`. Maps EnergyBridge devices (washer, dishwasher, etc.) to HEMA's schema (washing_machine, dishwasher, etc.) with °C→°F conversion. | +| `build_query()` | Constructs the natural-language prompt sent to HEMA's ReAct agent. Pulls from persona schema (tags, preferences, schedule, appliances), live state (indoor/outdoor temp, setpoint), VPP context (event window, capacity target), and price data. | +| `extract_actions()` | Parses HEMA's tool calls and converts back to EnergyBridge control JSON. Fuzzy-matches device names, maps actions (e.g., `set_temperature` → `setpoint_f`, `set_schedule` → `*_start_h`), and converts °F→°C. | + +##### `hema_controller.py` — `HEMAControlBaseline` + +| Function | What it does | +|----------|-------------| +| `decide()` | Entry point called by `family_runner.py` each timestep. Initializes HEMA agent on first call, then delegates to `_decide_with_retry()`. | +| `_decide_with_retry()` | Invokes HEMA agent, checks if all present appliances received commands. If any are missing, retries up to 5 times with a strengthened mandatory-action prompt. Logs raw tool calls for debugging. Tracks token usage and latency. | +| `_to_energybridge()` | Converts parsed HEMA actions to EnergyBridge format: °F→°C setpoint conversion, assembles `appliance_actions` dict, sets `next_check_hour` to wake controller after VPP ends. | + +#### Step 3: Run HEMA as a Baseline + +```text +# Run full Tianjin 7-day personal-user matrix +python experiments/benchmark/run_baseline_matrix.py \ + --methods hema_agent \ + --city Tianjin \ + --days 7 \ + --mpc-horizon 6 \ + --date \ + --workers 5 \ + --resume + +# Run full Germany 7-day personal-user matrix +python experiments/benchmark/run_baseline_matrix.py \ + --methods hema_agent \ + --city Germany \ + --days 7 \ + --start-date 2025-06-01 \ + --mpc-horizon 6 \ + --price-csv experiments/real_data/germany_2025_price.csv \ + --date \ + --workers 5 \ + --resume + +# Run all five households for Tianjin +python experiments/benchmark/run_household_matrix.py \ + --methods hema_agent \ + --city Tianjin \ + --days 7 \ + --start-date 2025-06-01 \ + --date \ + --workers 5 \ + --resume + +# Run all five households for Germany +python experiments/benchmark/run_household_matrix.py \ + --methods hema_agent \ + --city Germany \ + --days 7 \ + --start-date 2025-06-01 \ + --price-csv experiments/real_data/germany_2025_price.csv \ + --date \ + --workers 5 \ + --resume +``` + + ```text experiments/benchmark/baselines/hema ``` diff --git a/experiments/benchmark/baselines/hema/device_bridge.py b/experiments/benchmark/baselines/hema/device_bridge.py index e049b59..1375271 100644 --- a/experiments/benchmark/baselines/hema/device_bridge.py +++ b/experiments/benchmark/baselines/hema/device_bridge.py @@ -666,7 +666,7 @@ def extract_actions(self, agent_result: Dict[str, Any]) -> Dict[str, Any]: washer_start = _parse_hod(time_str) elif dev in ("dishwasher", "Dishwasher"): dishwasher_start = _parse_hod(time_str) - elif dev in ("clothes_dryer", "dryer"): + elif dev in ("clothes_dryer", "dryer", "Clothes Dryer"): dryer_start = _parse_hod(time_str) @@ -679,9 +679,17 @@ def extract_actions(self, agent_result: Dict[str, Any]) -> Dict[str, Any]: wh_end = _parse_hod(time_str) wh_preheat = True elif action_str in ("set_temperature", "set_temp", "temperature"): - wh_temp_f = float(value) + try: + if value is None: + wh_temp_f = float(time_str) + else: + temp_str = str(value).replace("°", "").replace("F", "").replace("f","").replace("C","").replace("c", "").strip() + wh_temp_f = float(temp_str) + wh_preheat = True + except (ValueError, TypeError): + pass - elif dev in ("ev_charger", "electric_vehicle", "ev", "tesla_charger", "car_charger"): + elif dev in ("ev_charger", "electric_vehicle", "ev", "tesla_charger", "car_charger", "EV Charger"): action_str = str(action).lower().replace(" ", "_") if action_str in ("start", "start_charging", "begin_charging"): ev_charge_start = _parse_hod(time_str) diff --git a/experiments/benchmark/baselines/hema/hema_controller.py b/experiments/benchmark/baselines/hema/hema_controller.py index 16d079b..d906a07 100644 --- a/experiments/benchmark/baselines/hema/hema_controller.py +++ b/experiments/benchmark/baselines/hema/hema_controller.py @@ -39,7 +39,7 @@ # ------------------------------------------------------------------ # Read EnergyBridge .env config (fixes base_url and model) # ------------------------------------------------------------------ -_API_KEY = os.getenv("OPENAI_API_KEY") or os.getenv("LLM_API_KEY") or "" +_API_KEY = os.getenv("LLM_API_KEY") or "" if not _API_KEY: _pool = os.getenv("LLM_API_KEY_POOL", "") if _pool: @@ -165,7 +165,9 @@ def _decide_with_retry( "- clothes_dryer: MUST schedule a start time today using schedule_device_action. " " Choose a time outside the VPP window (18:00-19:00). Use 24-hour format like '10:00'.\n" "- water_heater: MUST set a preheat schedule using schedule_device_action. " - " Set start time with action 'start', end time with action 'stop', and temperature with action 'set_temperature'. Must finish end time before the VPP window (18:00-19:00)\n" + " ONLY provide 'value' parameter (temperature in °F as a number, e.g., '125'), DO NOT provide 'time' parameter for set_temperature. " + " Set start time with action 'start', end time with action 'stop', and temperature with action 'set_temperature'. " + " Must finish end time before the VPP window (18:00-19:00)\n" "- ev_charger: MUST set the charging schedule. " " Ensure the charging window greater or equal to Minimum charging hours to reach target_soc. " " EV CHARGING TIME CALCULATION GUIDE:\n" @@ -276,7 +278,7 @@ def _to_energybridge( if parsed.get("dryer_start_h") is not None: appl["dryer_start_h"] = float(parsed["dryer_start_h"]) appl["dryer_skip"] = False - if parsed.get("water_heater_preheat") is not None: + if parsed.get("water_heater_preheat"): appl["water_heater_preheat"] = True appl["water_heater_preheat_start_h"] = float(parsed["water_heater_preheat_start_h"]) appl["water_heater_preheat_end_h"] = float(parsed["water_heater_preheat_end_h"])