TOU Schedule API — Read, Write, Verify & Error Handling¶
[!CAUTION] Use at your own risk — test extensively before relying on this in production.
The FranklinWH Cloud API does not have native "force charge" or "force discharge" commands with fixed time windows or target SoC parameters. TOU schedule manipulation via
GRID_CHARGE(dispatchId=8) andGRID_EXPORT(dispatchId=7) dispatch codes is the nearest equivalent — it tells the aGate to charge/discharge during specific time periods, but behaviour depends on grid conditions, battery state, and firmware.Known limitations: -
maxChargeSoc— accepted by the API but not verified in all firmware versions -minDischargeSoc— does not work in recent testing (aGate ignores this field) - Not all advanced TOU fields (gridChargeMax,dischargePower,rampTime, etc.) are fully supported or implemented by the aGate firmware - For compatibility with the official FranklinWH mobile app, keep time periods in 30-minute boundaries (e.g. 11:30, 12:00, 14:30). Arbitrary times like 11:49 work via the API but may display incorrectly or be overwritten in the app -set_tou_scheduletargets the season that owns today's date (or a specificmonth=you provide). For explicit multi-season creation or full schedule restore, useset_tou_schedule_multi(strategy_list).
Prerequisites — Check TOU Availability¶
[!WARNING] Not all sites have TOU configured. Before calling TOU endpoints, check
get_entrance_info()to avoid errors on sites without a tariff setup.
| Flag | Meaning | 0 / False = |
|---|---|---|
tariffSettingFlag |
TOU tariff is configured | No tariff — TOU schedule endpoints may return empty/error |
pcsEntrance |
PCS power control available | Power control settings not accessible |
bbEntrance |
Battery Bonus enrolled | No Battery Bonus programme |
sgipEntrance |
SGIP (California Self-Generation Incentive Program) | Not enrolled in SGIP |
ja12Entrance |
JA12 (California Energy Code, Joint Appendix 12 — battery storage compliance) | Not in JA12 programme |
info = await client.get_entrance_info()
if not info.get("tariffSettingFlag"):
print("No TOU tariff configured — schedule endpoints unavailable")
TOU Setup Workflows¶
There are two distinct paths to configure a TOU schedule. The template-based path mirrors the FranklinWH mobile app's multi-step wizard. The direct dispatch path is what franklinwh-cli tou --set uses.
Workflow A: Template-Based (App Wizard)¶
This is the full tariff setup used by the app when changing electric company, rate plan, or creating a new schedule from a utility template. Each step depends on the previous step's output.
flowchart TD
A["1. get_entrance_info()
Check tariffSettingFlag, pcsEntrance"] --> B
B["2. get_utility_companies(country_id, province_id)
Returns: dataList → [{id, companyName}]"] --> C
C["3. get_tariff_list(company_id)
Returns: dataList → [{id, name, electricityType}]"] --> D
D["4. get_tariff_detail(tariff_id)
Returns: template, strategyList, advancedSettings"] --> E
E{"User reviews rates/waves"}
E -->|Accept| F
E -->|Customise| G
G["5a. get_custom_dispatch_list(strategy_list)
Returns: valid dispatch codes for custom blocks"] --> F
F["5b. get_recommend_dispatch_list(strategy_list)
Returns: AI-recommended dispatch codes"] --> H
H["6. calculate_expected_earnings(template)
Returns: estimatedSavings30, estimatedSavings365"] --> I
I["7. apply_tariff_template(template_id, name)
WRITE: saveTouDispatchUseTemplate
Returns: {id: touId}"]
style A fill:#2d5016,color:#fff
style I fill:#7a1a1a,color:#fff
style E fill:#5a4a00,color:#fff
[!IMPORTANT]
apply_tariff_templateis a write operation. It replaces the active TOU schedule with the selected template. It is NOT suitable as a standalone CLI command — it requires the prior steps to provide validtemplate_idandstrategy_listinputs.
Workflow B: Direct Dispatch (CLI)¶
Used by franklinwh-cli tou --set. Skips utility/tariff selection — directly writes dispatch windows to the existing schedule.
flowchart TD
A["1. get_entrance_info()
Check tariffSettingFlag"] --> B
B["2. get_gateway_tou_list()
Get current schedule, dispatchId, seasons"] --> C
C["3. Build payload
construct_tou_payload(windows, mode, rates)"] --> D
D["4. set_tou_schedule(payload)
WRITE: saveTouDispatch
Returns: {id: touId}"] --> E
E["5. Switch Operating Mode (Optional)
set_mode(TIME_OF_USE) to active schedule"] --> F
F["6. Verify dispatch
get_gateway_tou_list() — confirm new schedule active"]
style A fill:#2d5016,color:#fff
style D fill:#7a1a1a,color:#fff
[!WARNING] Saving/applying a schedule does NOT automatically change the active operating mode of the aGate. Once the schedule is saved via
saveTouDispatch, the configuration is stored. However, if the system is currently running in a different operating mode (e.g.SELF_CONSUMPTIONorEMERGENCY_BACKUP), you must callset_mode(TIME_OF_USE)separately to activate schedule-based dispatching.
API Endpoint Reference¶
| Step | Method | HTTP | Endpoint | R/W |
|---|---|---|---|---|
| Prerequisites | get_entrance_info |
GET | terminal/getEntranceInfo |
Read |
| Utility search | get_utility_companies |
POST | terminal/tou/getTouCompanyListPageV2 |
Read |
| Tariff plans | get_tariff_list |
GET | terminal/tou/getTariffListByCompanyId |
Read |
| Tariff detail | get_tariff_detail |
GET | terminal/tou/getTariffDetailByIdV2 |
Read |
| Saved TOU detail | get_tou_detail_by_id |
POST | terminal/tou/getIotTouDetailById |
Read |
| Custom dispatches | get_custom_dispatch_list |
POST | terminal/tou/getCustomEnergyDispatchList |
Read |
| AI dispatch | get_recommend_dispatch_list |
POST | terminal/tou/getRecommendEnergyDispatchList |
Read |
| Savings estimate | calculate_expected_earnings |
POST | terminal/tou/calculate/expected/earnings |
Read |
| Apply template | apply_tariff_template |
POST | terminal/tou/saveTouDispatchUseTemplate |
Write |
| Direct dispatch | set_tou_schedule |
POST | terminal/tou/saveTouDispatch |
Write |
| Bonus info | get_bonus_info |
GET | terminal/tou/getBonusInfo |
Read |
| VPP tips | get_vpp_tip |
GET | terminal/tou/getVppTipForUpdateTou |
Read |
Dispatch Code Reference¶
| Code | dispatchId | Enum Name | Battery Behaviour |
|---|---|---|---|
HOME |
1 |
HOME_LOADS |
aPower → home loads, surplus solar → grid |
STANDBY |
2 |
STANDBY |
Battery idle, solar → home, excess → grid |
SOLAR |
3 |
SOLAR_CHARGE |
Charge battery from solar only |
SELF |
6 |
SELF_CONSUMPTION |
Solar → battery → home, excess → grid |
GRID_EXPORT |
7 |
GRID_EXPORT |
Force discharge battery → grid |
GRID_CHARGE |
8 |
GRID_CHARGE |
Force charge battery from grid + solar |
Wave types (tariff periods): OFF_PEAK=0, MID_PEAK=1, ON_PEAK=2, SUPER_OFF_PEAK=4
API Methods¶
Reading¶
| Method | Endpoint | Returns |
|---|---|---|
get_tou_dispatch_detail() |
GET /tou/getTouDispatchDetail |
Full template + strategies + dispatch list |
get_gateway_tou_list() |
POST /tou/getGatewayTouListV2 |
TOU config status, send status, alerts |
get_tou_info(option) |
(wraps above) | 0=raw, 1=current+next, 2=full detailVoList |
get_current_tou_price() |
(wraps above) | Current tier, wave type, rates, and remaining time |
get_charge_power_details() |
GET /chargePowerDetails |
SoC, estimated runtime, consumption |
Writing¶
| Method | Endpoint | Purpose |
|---|---|---|
set_tou_schedule(touMode, touSchedule) |
→ POST /tou/saveTouDispatch |
Set a single 24h schedule for all days/months |
set_tou_schedule_multi(strategy_list) |
→ POST /tou/saveTouDispatch |
Submit a multi-season or weekday/weekend schedule |
Multi-Season & Weekday/Weekend Scheduling (V2 API)¶
The FranklinWH App recently transitioned to a complex multi-season, multi-day-type representation of TOU schedules. The set_tou_schedule_multi method exposes this capability directly to Python.
Backwards Compatibility¶
The original set_tou_schedule method has been internally refactored to perform transparent season resolution. When called without an explicit seasons= argument, it reads the existing schedule, identifies which season owns today's month (or the month=N you specify), updates only that season's blocks, and leaves all other seasons untouched. Single-season code continues to work exactly as before. Code that previously passed seasons=[{"name":...,"months":"1,2,..."}] to isolate a dispatch block should remove that argument — the library now does this automatically.
Live Testing & Verification¶
Both set_tou_schedule and set_tou_schedule_multi have undergone live regression testing (Session 11) against out-of-the-box physical aGates. The testing confirmed:
1. FranklinWH strictly validates the gateway serial string case (A02F vs a02f) at the authentication layer before writing schedules.
2. The saveTouDispatch API strictly enforces Spring Boot @NotBlank restrictions on the payload strings.
3. Successfully dispatched the complex JSON block and comprehensively restored the configurations without degradation. (See tests/results/live_tou_results.md or GitHub issue trackers for full payloads).
Syntax Example¶
Instead of passing a single flat list of detailVoList blocks, you pass an array of strategyList blocks. Each block demarcates the seasonName, the month spans, and the sub-arrays for Weekday (dayType=1) vs Weekend (dayType=2):
import json
# Example syntax mapping
multi_season = [
{
"seasonName": "Summer (Peak)",
"month": "6,7,8",
"dayTypeVoList": [
{
"dayName": "weekday", "dayType": 1,
"eleticRateShoulder": 1.25,
"detailVoList": [...] # Full 24h block
},
{
"dayName": "weekend", "dayType": 2,
"detailVoList": [...] # Full 24h block
}
]
},
{
"seasonName": "Winter (Off-Peak)",
"month": "1,2,3,4,5,9,10,11,12",
"dayTypeVoList": [...]
}
]
# Dispatches seamlessly
await client.set_tou_schedule_multi(multi_season)
Schedule Entry Format & Pre-Flight Validation¶
The franklinwh-cloud library enforces strict pre-flight validation on schedule structures before any network call is dispatched. This client-side shield intercepts structural issues, preventing bad requests from hitting the cloud.
🛡️ Pre-Flight Schema Verification (JSON Schema / XSD Equivalent)¶
Custom schedule lists are validated against a formal JSON Schema. This client-side schema verification acts as a strict validation contract—the modern REST equivalent of a traditional XML XSD schema validation.
If the input is missing required/mandatory fields (such as dispatchId or waveType) or uses invalid types/patterns, set_tou_schedule instantly fails client-side before any network request is initiated. In this case, the SDK raises an InvalidTOUScheduleOption exception with a detailed, specific validation message (e.g., JSON Validation failed: 'dispatchId' is a required property).
📖 Discovering Valid Fields & Enums (Interactive API Docs)¶
When integrating a client (such as Home Assistant, Node-RED, or custom scripts), you need to know exactly which enums, field names, and values are structurally valid. Rather than guessing, you can inspect the formal API specification interactively:
- Local OpenAPI Specification:
-
The repository ships with a complete OpenAPI 3.0 specification file: franklinwh_openapi.json. This file describes every endpoint, request body schema, and model representation in detail.
-
FastAPI Emulator Docs Endpoint:
- When running the local FastAPI Cloud API Emulator (located in
emulator/main.py), you can access the interactive Swagger/ReDoc documentation UI directly in your browser:- Swagger UI:
http://localhost:8080/docs - ReDoc UI:
http://localhost:8080/redoc
- Swagger UI:
-
This provides a live playground to inspect parameter types, view validation rules, and test requests.
-
FHAI (FranklinWH Home Assistant Integrator) Docs Endpoint:
- When running the FHAI integration gateway service, it hosts a local FastAPI web server exposing a dedicated
/docsAPI endpoint directly at:- Swagger UI:
http://localhost:8099/docs
- Swagger UI:
- Developers and downstream client applications can query this endpoint to retrieve the fully-resolved interactive Swagger schema, detailing exactly what enums (like
waveTypeanddispatchId) and attributes are valid and how the schema is structured.
Each time block is a dictionary requiring 5 mandatory fields:
{
"name": "Off-Peak", # Human label (e.g. tariff tier name)
"startHourTime": "11:49", # HH:MM format (24h)
"endHourTime": "14:59", # HH:MM format (24h) or "24:00"
"waveType": 0, # Tariff period (0=Off-Peak, 2=On-Peak, etc.)
"dispatchId": 8 # Dispatch mode ID (e.g. 8=GRID_CHARGE)
}
[!IMPORTANT] The full 24 hours (00:00 → 24:00 = 1440 minutes) must be covered. If your entries have gaps,
set_tou_scheduleauto-fills them usingdefault_mode(default:SELF) anddefault_tariff(default:OFF_PEAK).
⚙️ Dynamic Payload Enrichment (Spring Boot Compatibility)¶
Modern Spring Boot validation on the FranklinWH backend strictly rejects partial strategy lists. Rather than forcing developers to manage low-level hardware parameters manually, the library automatically enriches newly constructed or custom time blocks prior to saving.
The enrichment layer performs:
1. Dynamic Priority Look-up: Resolves correct system-matching priority settings (e.g., "solarPriority": "1,2,3", "loadPriority": "1,2,0") directly from the gateway's active configuration templates (touDispatchList).
2. Metadata Retention & Merge: Intelligently read-merges and preserves database primary keys (id, strategyId) and hardware state keys (useModeFlag, solarCutoff, briefDescribe, etc.) for overlapping time windows, ensuring zero strategy table inconsistency rejections.
📋 Published JSON Schema Reference¶
Developers integrating this library into larger platforms (e.g. Home Assistant, Node-RED) can utilize this formal JSON Schema specification for schedule validation:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["name", "startHourTime", "endHourTime", "waveType", "dispatchId"],
"properties": {
"name": {"type": "string"},
"startHourTime": {
"type": "string",
"pattern": "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]|24:00$"
},
"endHourTime": {
"type": "string",
"pattern": "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]|24:00$"
},
"waveType": {
"type": "integer",
"enum": [0, 1, 2, 4]
},
"dispatchId": {
"type": "integer",
"enum": [1, 2, 3, 6, 7, 8]
},
"id": {"type": ["integer", "null"]},
"strategyId": {"type": ["integer", "null"]},
"gridDischargeMax": {"type": ["integer", "null"]},
"gridChargeMax": {"type": ["integer", "null"]},
"chargeMax": {"type": ["integer", "null"]},
"chargePower": {"type": ["integer", "null"]},
"gridFeedMax": {"type": ["integer", "null"]},
"dischargePower": {"type": ["integer", "null"]},
"dischargeMax": {"type": ["integer", "null"]},
"solarCutoff": {"type": ["integer", "null"]},
"gridMax": {"type": ["integer", "null"]},
"maxChargeSoc": {"type": ["integer", "null"]},
"minDischargeSoc": {"type": ["integer", "null"]},
"heatEnable": {"type": ["integer", "null"]},
"powerOffApower": {"type": ["integer", "null"]},
"offGrid": {"type": ["integer", "null"]},
"gcaoMax": {"type": ["integer", "null"]},
"rampTime": {"type": ["integer", "null"]},
"useModeFlag": {"type": ["integer", "null"]},
"briefDescribe": {"type": ["string", "null"]},
"solarPriority": {"type": ["string", "null"]},
"loadPriority": {"type": ["string", "null"]},
"dispatch": {"type": ["string", "null"]}
}
}
}
Flow Diagram — Setting a TOU Schedule¶
sequenceDiagram
participant App as Your Code
participant Lib as franklinwh-cloud
participant API as FranklinWH Cloud API
participant Gate as aGate
App->>Lib: set_tou_schedule("CUSTOM", schedule)
Note over Lib: 1. Validate touMode ∈ valid_tou_modes
Note over Lib: 2. JSON schema validation (jsonschema)
Lib->>API: GET getTouDispatchDetail
API-->>Lib: template + strategyList + detailDefaultVo
Lib->>API: GET getHomeGatewayList
API-->>Lib: account info for payload
Note over Lib: 3. Compute durations per block
Note over Lib: 4. Sort by startHourTime
Note over Lib: 5. Gap-fill to 1440 minutes
Note over Lib: 6. Verify total = 1440 min
Lib->>API: GET getPcsHintInfo (dispatchId check)
API-->>Lib: PCS validation result
Lib->>API: POST saveTouDispatch (payload)
API-->>Lib: {code: 200, result: {id: touId}}
Note over API,Gate: Schedule queued → touSendStatus=1
Note over Gate: aGate applies within ~1 min
Lib-->>App: response dict
Flow Diagram — Verification After Submission¶
flowchart TD
A[Submit schedule] --> B[Wait 5-10s]
B --> C[get_gateway_tou_list]
C --> D{touSendStatus?}
D -->|1 = Pending| E[Wait & retry up to 60s]
E --> C
D -->|0 or None| F[get_tou_dispatch_detail]
F --> G{Compare detailVoList}
G -->|Matches| H["✅ Schedule applied"]
G -->|Mismatch| I["⚠️ Retry or alert"]
style H fill:#2d5016,color:#fff
style I fill:#8b1a1a,color:#fff
🔄 Post-Apply Operating Mode Switch Behavior¶
[!WARNING] Saving or applying a schedule does NOT automatically change the active operating mode of the aGate.
Historically, saving/applying a new TOU schedule (
saveTouDispatch) automatically forced the gateway to switch into TOU operating mode. In modern firmware and Cloud API updates, this is no longer the case.The official mobile app mirrors this: when a user updates or applies a schedule, the app specifically prompts the user asking if they want to switch their active operating mode to Time-of-Use. If they decline (or via direct API integration if not called separately), the gateway stays in its current operating mode (e.g. Self-Consumption or Emergency Backup) and does not execute the newly saved schedule.
To make the system run your new schedule, you must perform a separate call to configure the system's operating mode to TOU:
from franklinwh_cloud.const import TIME_OF_USE # 1. Save / Update the TOU schedule result = await client.set_tou_schedule( touMode="CUSTOM", touSchedule=schedule ) # 2. Explicitly switch the system's operating mode to Time-of-Use # (Only required if the system is not already running in TOU mode) current_mode_info = await client.get_mode() if current_mode_info.get("workMode") != TIME_OF_USE: logger.info("Switching active operating mode to Time-of-Use...") success = await client.set_mode(TIME_OF_USE) if success: logger.info("Operating mode changed successfully to TOU!")
Example 1 — Grid Charging 11:49 → 14:59, Self-Consumption Otherwise¶
Visual timeline¶
00:00 ─────────────── 11:49 ──────── 14:59 ─────────────── 24:00
│ SELF-CONSUMPTION │ GRID CHARGE │ SELF-CONSUMPTION │
│ (auto-filled gap) │ (your block)│ (auto-filled gap) │
│ dispatchId=6 │ dispatchId=8│ dispatchId=6 │
Python code¶
import asyncio
import logging
from franklinwh_cloud.client import Client, TokenFetcher
from franklinwh_cloud.const import dispatchCodeType, WaveType
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(message)s")
logger = logging.getLogger("tou_example")
async def set_grid_charge_window():
"""Set grid charging from 11:49 to 14:59, self-consumption all other times."""
fetcher = TokenFetcher("your@email.com", "your_password")
await fetcher.get_token()
client = Client(fetcher, "YOUR-AGATE-SN")
# ── Step 1: Read current schedule ────────────────────────────
logger.info("Reading current TOU schedule...")
current = await client.get_tou_dispatch_detail()
template = current.get("result", {}).get("template", {})
logger.info(f"Current tariff: {template.get('name', '?')}")
# Read current status
tou_status = await client.get_gateway_tou_list()
send_status = tou_status.get("result", {}).get("touSendStatus", 0)
if send_status:
logger.warning("A schedule change is already pending (touSendStatus=1)")
# ── Step 2: Define the schedule ──────────────────────────────
# Only define the NON-DEFAULT block.
# Everything else auto-fills with default_mode="SELF" (dispatchId=6)
schedule = [
{
"name": "Off-Peak",
"startHourTime": "11:49",
"endHourTime": "14:59",
"waveType": WaveType.OFF_PEAK.value, # 0
"dispatchId": dispatchCodeType.GRID_CHARGE.value, # 8
}
]
# ── Step 3: Submit ───────────────────────────────────────────
logger.info("Submitting TOU schedule...")
try:
result = await client.set_tou_schedule(
touMode="CUSTOM",
touSchedule=schedule,
default_mode="SELF", # Gap-fill with self-consumption
default_tariff="OFF_PEAK", # Gap-fill tariff tier
)
if result.get("code") == 200:
tou_id = result["result"]["id"]
logger.info(f"✅ Schedule submitted — touId={tou_id}")
else:
logger.error(f"❌ Unexpected response: {result}")
return
except Exception as e:
logger.error(f"❌ Schedule submission failed: {e}")
return
# ── Step 4: Verify ───────────────────────────────────────────
logger.info("Verifying schedule was applied...")
await asyncio.sleep(5) # Wait for aGate processing
for attempt in range(6): # Retry up to 30s
verify = await client.get_gateway_tou_list()
status = verify.get("result", {}).get("touSendStatus", 0)
if not status:
break
logger.info(f" Still pending... (attempt {attempt + 1}/6)")
await asyncio.sleep(5)
# Read back and confirm
readback = await client.get_tou_info(2) # option=2: full detailVoList
logger.info(f"Schedule has {len(readback)} time blocks:")
for block in readback:
disp = block.get("dispatchId", "?")
logger.info(f" {block['startHourTime']} → {block['endHourTime']} "
f"dispatch={disp} wave={block.get('waveType', '?')}")
asyncio.run(set_grid_charge_window())
What set_tou_schedule generates (after gap-fill)¶
The library auto-fills the gaps, producing a 3-block schedule:
[
{"startHourTime": "00:00", "endHourTime": "11:49", "dispatchId": 6, "waveType": 0, "name": "Off-Peak"}, # auto-filled
{"startHourTime": "11:49", "endHourTime": "14:59", "dispatchId": 8, "waveType": 0, "name": "Off-Peak"}, # your block
{"startHourTime": "14:59", "endHourTime": "24:00", "dispatchId": 6, "waveType": 0, "name": "Off-Peak"}, # auto-filled
]
Example 2 — Custom: Self → Grid Export → Home Loads¶
Visual timeline¶
00:00 ─────────────── 18:30 ──────── 19:45 ─────────────── 24:00
│ SELF-CONSUMPTION │ GRID EXPORT │ HOME LOADS │
│ dispatchId=6 │ dispatchId=7│ dispatchId=1 │
│ waveType=0 OffPeak │ waveType=2 │ waveType=2 OnPeak │
Python code¶
async def set_custom_three_phase_schedule():
"""Self-consumption → Grid export 18:30-19:45 → Home loads after."""
fetcher = TokenFetcher("your@email.com", "your_password")
await fetcher.get_token()
client = Client(fetcher, "YOUR-AGATE-SN")
# Define all 3 blocks explicitly (covers full 24h — no gap-fill needed)
schedule = [
{
"name": "Off-Peak",
"startHourTime": "00:00",
"endHourTime": "18:30",
"waveType": WaveType.OFF_PEAK.value, # 0
"dispatchId": dispatchCodeType.SELF_CONSUMPTION.value, # 6
},
{
"name": "On-Peak",
"startHourTime": "18:30",
"endHourTime": "19:45",
"waveType": WaveType.ON_PEAK.value, # 2
"dispatchId": dispatchCodeType.GRID_EXPORT.value, # 7
},
{
"name": "On-Peak",
"startHourTime": "19:45",
"endHourTime": "24:00",
"waveType": WaveType.ON_PEAK.value, # 2
"dispatchId": dispatchCodeType.HOME_LOADS.value, # 1
},
]
try:
result = await client.set_tou_schedule(
touMode="CUSTOM",
touSchedule=schedule,
)
if result.get("code") == 200:
print(f"✅ touId={result['result']['id']}")
else:
print(f"❌ API error: code={result.get('code')} msg={result.get('msg', '?')}")
except InvalidTOUScheduleOption as e:
# Bad touMode, invalid predefined name, or JSON schema failure
print(f"❌ Validation error: {e}")
except ValidationError as e:
# Schedule doesn't cover 24h, or saveTouDispatch returned non-200
print(f"❌ Schedule error: {e}")
except Exception as e:
print(f"❌ Unexpected error: {type(e).__name__}: {e}")
Error Handling Reference¶
flowchart TD
A[set_tou_schedule called] --> B{touMode valid?}
B -->|No| C["InvalidTOUScheduleOption\n'Invalid TOU mode requested: X'"]
B -->|Yes| D{Mode = CUSTOM?}
D -->|Yes| E{JSON schema valid?}
E -->|No| F["InvalidTOUScheduleOption\n'JSON Validation failed: ...'"]
E -->|Yes| G[Parse times + sort]
D -->|PREDEFINED| H{Fixture name exists?}
H -->|No| I["InvalidTOUScheduleOption\n'tou_predefined specified is invalid'"]
H -->|Yes| G
D -->|Simple mode| G
G --> J{Total = 1440 min?}
J -->|No| K["ValidationError\n'Total elapsed minutes not equal to 1440'"]
J -->|Yes| L[POST saveTouDispatch]
L --> M{code == 200?}
M -->|No| N["ValidationError\n'saveTouDispatch failed with response: ...'"]
M -->|Yes| O["✅ Return touId"]
style C fill:#8b1a1a,color:#fff
style F fill:#8b1a1a,color:#fff
style I fill:#8b1a1a,color:#fff
style K fill:#8b1a1a,color:#fff
style N fill:#8b1a1a,color:#fff
style O fill:#2d5016,color:#fff
Exception Types¶
| Exception | When | Common Cause |
|---|---|---|
InvalidTOUScheduleOption |
Before API call | Bad touMode, invalid predefined name, JSON schema violation |
ValidationError |
After gap-fill | Schedule ≠ 1440 min (overlapping/impossible times) |
ValidationError |
After API call | saveTouDispatch returned non-200 (server rejected payload) |
httpx exceptions |
Network | Timeout, DNS, auth token expired |
Logging¶
All set_tou_schedule operations log to "franklinwh_cloud" logger. Enable with:
logging.getLogger("franklinwh_cloud").setLevel(logging.INFO)
# or for full trace:
logging.getLogger("franklinwh_cloud").setLevel(logging.DEBUG)
Key log messages to watch for:
- set_tou_schedule: Inserting missing time period entry at start/end — gap-fill triggered
- set_tou_schedule: Amended sorted_data with missing time periods — gaps were repaired
- set_tou_schedule: saveTouDispatch successful, touId = X — success
- set_tou_schedule: Error: saveTouDispatch failed — API rejection
CLI Usage¶
[!TIP] The
franklinwh-cli toucommand is fully implemented. See the dedicated CLI_TOU_COMMAND.md for the complete flag reference, implementation status matrix, and worked examples.
Quick examples¶
# Read
franklinwh-cli tou # Full schedule (all seasons)
franklinwh-cli tou --next # Current block + remaining time
franklinwh-cli tou --price # Active pricing tier and rates
franklinwh-cli tou --dispatch # Include raw dispatch metadata
franklinwh-cli tou --json # Machine-readable JSON
# Write
franklinwh-cli tou --set GRID_CHARGE --start 11:30 --end 15:00
franklinwh-cli tou --set GRID_EXPORT --start 18:00 --end 20:00 --default HOME
franklinwh-cli tou --set CUSTOM --file schedule.json
franklinwh-cli tou --multi-season seasons.json
# Supervised (auto-restore when window ends)
franklinwh-cli tou --set GRID_CHARGE --start 23:00 --end 01:00 --wait
Key Constraints & Gotchas¶
| Constraint | Detail |
|---|---|
| No native force charge/discharge | Cloud API has no dedicated timed charge/discharge or target-SoC command. TOU dispatch is the nearest equivalent |
minDischargeSoc broken |
API accepts the field but aGate ignores it in recent testing — does not enforce minimum SoC during discharge |
maxChargeSoc unverified |
May work on some firmware versions — test before relying on it |
| Advanced fields unsupported | gridChargeMax, dischargePower, rampTime, gridFeedMax, etc. are accepted but may be ignored by aGate |
| 30-minute boundaries | For mobile app compatibility, use times like 11:30, 12:00, 14:30. Arbitrary times (e.g. 11:49) work via API but may render incorrectly in the official app or be overwritten when the user edits the schedule |
| 24h coverage | Schedule must total exactly 1440 minutes. Gap-fill handles this automatically |
| All months coverage | Multi-season schedules must cover all 12 months exactly once (no gaps, no overlaps). |
| Day Types | dayType=3 is everyday. dayType=1 is weekday, dayType=2 is weekend. |
| Tariff must be TOU | May not work if user has "Flat" or "Tiered" rate plans configured |
| touSendStatus | After submit, =1 means pending. May persist as false positive even after applied |
String 'null' |
The code uses the string 'null' (not Python None) in some payload fields — this is intentional for the API |
Multi-Season and Day-Type Setup¶
For complex tariffs (like Australian/Californian separate Summer/Winter rates, or Weekday/Weekend splits), use set_tou_schedule_multi(strategy_list).
This accepts a list of season objects matching the exact format from the API's getTouDispatchDetail response.
Payload Structure¶
[
{
"months": "10,11,12,1,2,3",
"name": "Summer",
"dayTypeVoList": [
{
"dayType": 1,
"name": "Weekday",
"detailVoList": [
{ "name": "Off-Peak", "startHourTime": "00:00", "endHourTime": "15:00", "waveType": 0, "dispatchId": 6 },
{ "name": "Peak", "startHourTime": "15:00", "endHourTime": "21:00", "waveType": 2, "dispatchId": 8 },
{ "name": "Off-Peak", "startHourTime": "21:00", "endHourTime": "24:00", "waveType": 0, "dispatchId": 6 }
]
},
{
"dayType": 2,
"name": "Weekend",
"detailVoList": [
{ "name": "Off-Peak", "startHourTime": "00:00", "endHourTime": "24:00", "waveType": 0, "dispatchId": 6 }
]
}
]
},
{
"months": "4,5,6,7,8,9",
"name": "Winter",
"dayTypeVoList": [
{
"dayType": 3,
"name": "Everyday",
"detailVoList": [
{ "name": "Off-Peak", "startHourTime": "00:00", "endHourTime": "24:00", "waveType": 0, "dispatchId": 6 }
]
}
]
}
]
Validation Rules¶
- Months: Every integer from 1 to 12 must appear exactly once across all seasons.
- Day Types: Each season must define
dayType=3(Everyday) OR bothdayType=1(Weekday) anddayType=2(Weekend). - 24-hour Coverage: Inside every
detailVoList, the time blocks must span exactly00:00to24:00(1440 minutes).
set_tou_schedule_multi automatically validates these rules, strips existing database IDs (strategyId, id), and preserves the existing TOU template metadata.
Real-Time Pricing¶
To determine the current active tariff block, use get_current_tou_price(). It downloads the current schedule and matches the local system clock against the season, day type, and time block.
Returns:
{
"tier": 2,
"wave_type": 2,
"wave_type_name": "On-Peak",
"season_name": "Summer",
"day_type_name": "Weekday",
"block_name": "Peak",
"block_start": "15:00",
"block_end": "21:00",
"minutes_remaining": 120,
"dispatch_id": 8,
"buy_rates": {"peak": 0.55, "valley": 0.20},
"sell_rates": {"peak": 0.40, "valley": 0.05}
}