Time of Use (TOU) Pricing¶
The Navien NWP500 supports Time of Use (TOU) pricing schedules, allowing the water heater to optimize heating based on electricity rates that vary throughout the day. The Navien mobile app integrates with the OpenEI (Open Energy Information) API to retrieve utility rate information.
Overview¶
Time of Use pricing enables:
Cost optimization: Heat water during off-peak hours when electricity rates are lower
Demand response: Reduce energy consumption during peak rate periods
Custom schedules: Configure up to 16 different time periods with varying rates
Seasonal support: Different schedules for different months of the year
Weekday/weekend support: Separate schedules for weekdays and weekends
The system uses utility rate data from OpenEI to configure heating schedules based on your location and utility provider.
OpenEI API Integration¶
The Navien mobile app queries the OpenEI Utility Rates API to retrieve current electricity rate information for the user’s location, then presents available rate plans and configures TOU schedules.
API Endpoint¶
GET https://api.openei.org/utility_rates
Query Parameters¶
The following parameters are used to query utility rates:
Parameter |
Type |
Description |
|---|---|---|
|
integer |
API version (currently |
|
string |
Response format ( |
|
string |
OpenEI API key (embedded in Navien app) |
|
string |
Level of detail ( |
|
string |
ZIP code or address to search |
|
string |
Customer sector ( |
|
string |
Sort field ( |
|
string |
Sort direction ( |
|
integer |
Maximum number of results ( |
Example Request¶
GET https://api.openei.org/utility_rates?version=7&format=json&api_key=YOUR_API_KEY&detail=full&address=94903§or=Residential&orderby=startdate&direction=desc&limit=100
Response Format¶
The API returns a JSON response with an array of utility rate plans:
{
"items": [
{
"label": "67575942fe4f0b50f5027994",
"uri": "https://apps.openei.org/IURDB/rate/view/67575942fe4f0b50f5027994",
"approved": true,
"is_default": false,
"utility": "Pacific Gas & Electric Co",
"eiaid": 14328,
"name": "E-1 -Residential Service Baseline Region Y",
"startdate": 1727766000,
"sector": "Residential",
"servicetype": "Bundled",
"description": "This schedule is applicable to single-phase and polyphase residential service...",
"energyratestructure": [
[
{
"max": 10.5,
"unit": "kWh daily",
"rate": 0.40206
},
{
"max": 42,
"unit": "kWh daily",
"rate": 0.50323
}
]
],
"energyweekdayschedule": [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]
],
"energyweekendschedule": [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]
]
}
]
}
Response Fields¶
Field |
Type |
Description |
|---|---|---|
|
string |
Name of the utility company |
|
integer |
EIA (Energy Information Administration) utility ID |
|
string |
Rate plan name |
|
integer |
Unix timestamp when rate plan becomes effective |
|
array |
Tiered rate structure by season and tier |
|
array |
24-hour schedule by month (0=off-peak, 1=on-peak) |
|
array |
24-hour weekend schedule by month |
|
float |
Minimum daily charge |
|
string |
Units for fixed charges (e.g., |
Rate Structure¶
The energyratestructure field contains tiered pricing:
Each outer array element represents a season or month
Each inner array element represents a usage tier
ratefield contains the price per kWhmaxfield indicates the upper limit for that tier (optional)
Hour-by-Hour Schedules¶
The energyweekdayschedule and energyweekendschedule arrays map rate periods:
12 elements (one per month)
Each month has 24 elements (one per hour)
Values map to indices in
energyratestructure0typically represents off-peak,1represents on-peak
TOU API Methods¶
The library provides methods for working with TOU information through both REST API and MQTT.
REST API: Get TOU Info¶
async def get_tou_info(
mac_address: str,
additional_value: str,
controller_id: str,
user_type: str = "O"
) -> TOUInfo
Retrieves stored TOU configuration from the Navien cloud API.
Parameters:
mac_address: Device MAC addressadditional_value: Additional device identifiercontroller_id: Controller serial numberuser_type: User type (default:"O"for owner)
Returns:
TOUInfo object containing:
@dataclass
class TOUInfo:
register_path: str # Path where TOU data is stored
source_type: str # Source of rate data (e.g., "openei")
controller_id: str # Controller serial number
manufacture_id: str # Manufacturer ID
name: str # Rate plan name
utility: str # Utility company name
zip_code: int # ZIP code
schedule: List[TOUSchedule] # TOU schedule periods
REST API: Convert TOU Rate Plans¶
async def convert_tou(
source_data: List[Dict[str, Any]],
source_type: str = "openei",
source_version: int = 7
) -> List[ConvertedTOUPlan]
Sends raw OpenEI rate plan data to the Navien backend for conversion into device-ready TOU schedules with season/week bitfields and scaled pricing.
Parameters:
source_data: List of OpenEI rate plan dictionaries (fromOpenEIClient.fetch_rates())source_type: Data source type (default:"openei")source_version: OpenEI API version (default:7)
Returns:
List of ConvertedTOUPlan objects, each containing:
utility: Utility company namename: Rate plan nameschedule: List ofTOUSchedulewith device-ready intervals
REST API: Apply TOU Rate Plan¶
async def update_tou(
mac_address: str,
additional_value: str,
tou_info: Dict[str, Any],
source_data: Dict[str, Any],
zip_code: str,
register_path: str = "wifi",
source_type: str = "openei",
user_type: str = "O"
) -> TOUInfo
Applies a converted TOU rate plan to a device.
Parameters:
mac_address: Device MAC addressadditional_value: Additional device identifiertou_info: Converted TOU schedule (fromconvert_tou())source_data: Original OpenEI rate plan dictionaryzip_code: Service area zip coderegister_path: Connection type ("wifi"or"bt")
Returns:
TOUInfo object with the applied configuration.
OpenEI Client¶
from nwp500 import OpenEIClient
async with OpenEIClient() as client:
# List utilities for a zip code
utilities = await client.list_utilities("94903")
# List rate plans (optionally filtered by utility)
plans = await client.list_rate_plans("94903", utility="Pacific Gas")
# Get a specific rate plan
plan = await client.get_rate_plan("94903", "EV Rate A")
The OpenEIClient reads the API key from the OPENEI_API_KEY environment
variable or accepts it as a constructor parameter. Get a free key at
https://openei.org/services/api/signup/
MQTT: Configure TOU Schedule¶
async def configure_tou_schedule(
device: Device,
controller_serial_number: str,
periods: List[Dict[str, Any]],
enabled: bool = True
) -> None
Configures the TOU schedule directly on the device via MQTT.
Parameters:
device: Device object from APIcontroller_serial_number: Controller serial number (obtain via device info)periods: List of TOU period dictionaries (up to 16 periods)enabled: Whether to enable TOU scheduling (default:True)
MQTT: Enable/Disable TOU¶
async def set_tou_enabled(
device: Device,
enabled: bool
) -> None
Enables or disables TOU operation without changing the schedule.
Parameters:
device: Device objectenabled:Trueto enable TOU,Falseto disable
MQTT: Request TOU Settings¶
async def request_tou_settings(
device: Device,
controller_serial_number: str
) -> None
Requests the current TOU configuration from the device.
Parameters:
device: Device objectcontroller_serial_number: Controller serial number
The device will respond on the topic:
cmd/{deviceType}/{deviceId}/res/tou/rd
Building TOU Periods¶
Helper Methods¶
The library provides helper functions for building TOU period configurations:
build_tou_period()¶
def build_tou_period(
season_months: Union[List[int], range],
week_days: List[str],
start_hour: int,
start_minute: int,
end_hour: int,
end_minute: int,
price_min: float,
price_max: float,
decimal_point: int = 5
) -> Dict[str, Any]
Creates a TOU period configuration dictionary.
Parameters:
season_months: List of months (1-12) when this period appliesweek_days: List of day names (e.g.,["Monday", "Tuesday"])start_hour: Start hour (0-23)start_minute: Start minute (0-59)end_hour: End hour (0-23)end_minute: End minute (0-59)price_min: Minimum electricity price ($/kWh)price_max: Maximum electricity price ($/kWh)decimal_point: Number of decimal places for price encoding (default: 5)
Returns:
Dictionary with encoded TOU period data ready for MQTT transmission.
encode_price()¶
def encode_price(price: float, decimal_point: int = 5) -> int
Encodes a floating-point price into an integer for transmission.
Example:
from nwp500 import encode_price
# Encode $0.45000 per kWh
encoded = encode_price(0.45, decimal_point=5)
# Returns: 45000
decode_price()¶
def decode_price(encoded_price: int, decimal_point: int = 5) -> float
Decodes an integer price back to floating-point.
Example:
from nwp500 import decode_price
# Decode price from device
price = decode_price(45000, decimal_point=5)
# Returns: 0.45
encode_week_bitfield()¶
def encode_week_bitfield(day_names: List[str]) -> int
Encodes a list of day names into a bitfield.
Valid day names:
"Monday"(bit 6, value 64)"Tuesday"(bit 5, value 32)"Wednesday"(bit 4, value 16)"Thursday"(bit 3, value 8)"Friday"(bit 2, value 4)"Saturday"(bit 1, value 2)"Sunday"(bit 7, value 128)
Example:
from nwp500 import encode_week_bitfield
# Weekdays only
bitfield = encode_week_bitfield([
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
])
# Returns: 0b1111100 = 124
decode_week_bitfield()¶
def decode_week_bitfield(bitfield: int) -> List[str]
Decodes a bitfield back into a list of day names.
Example:
from nwp500 import decode_week_bitfield
# Decode weekday bitfield
days = decode_week_bitfield(62)
# Returns: ["Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
Usage Examples¶
Example 1: Simple TOU Schedule¶
Configure two rate periods - off-peak and peak pricing:
import asyncio
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient, build_tou_period
async def configure_simple_tou():
async with NavienAuthClient("user@example.com", "password") as auth_client:
# Get device
api_client = NavienAPIClient(auth_client=auth_client)
device = await api_client.get_first_device()
# Connect MQTT and get controller serial
mqtt_client = NavienMqttClient(auth_client)
await mqtt_client.connect()
# Request device info to get controller serial number
feature_future = asyncio.Future()
def capture_feature(feature):
if not feature_future.done():
feature_future.set_result(feature)
await mqtt_client.subscribe_device_feature(device, capture_feature)
await mqtt_client.request_device_info(device)
feature = await asyncio.wait_for(feature_future, timeout=15)
controller_serial = feature.controllerSerialNumber
# Define off-peak period (midnight to 2 PM, weekdays)
off_peak = build_tou_period(
season_months=range(1, 13), # All months
week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
start_hour=0,
start_minute=0,
end_hour=14,
end_minute=59,
price_min=0.12, # $0.12/kWh
price_max=0.12,
decimal_point=5
)
# Define peak period (3 PM to 8 PM, weekdays)
peak = build_tou_period(
season_months=range(1, 13),
week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
start_hour=15,
start_minute=0,
end_hour=20,
end_minute=59,
price_min=0.35, # $0.35/kWh
price_max=0.35,
decimal_point=5
)
# Configure TOU schedule
await mqtt_client.configure_tou_schedule(
device=device,
controller_serial_number=controller_serial,
periods=[off_peak, peak],
enabled=True
)
print("TOU schedule configured successfully")
await mqtt_client.disconnect()
asyncio.run(configure_simple_tou())
Example 2: Complex Seasonal Schedule¶
Configure different rates for summer and winter:
async def configure_seasonal_tou():
async with NavienAuthClient("user@example.com", "password") as auth_client:
api_client = NavienAPIClient(auth_client=auth_client)
device = await api_client.get_first_device()
mqtt_client = NavienMqttClient(auth_client)
await mqtt_client.connect()
# ... get controller_serial (same as Example 1) ...
# Summer off-peak (June-September, all day except 2-8 PM)
summer_off_peak = build_tou_period(
season_months=[6, 7, 8, 9],
week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
start_hour=0,
start_minute=0,
end_hour=13,
end_minute=59,
price_min=0.15,
price_max=0.15,
decimal_point=5
)
# Summer peak (June-September, 2-8 PM)
summer_peak = build_tou_period(
season_months=[6, 7, 8, 9],
week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
start_hour=14,
start_minute=0,
end_hour=20,
end_minute=59,
price_min=0.45,
price_max=0.45,
decimal_point=5
)
# Winter rates (October-May)
winter_off_peak = build_tou_period(
season_months=[10, 11, 12, 1, 2, 3, 4, 5],
week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
start_hour=0,
start_minute=0,
end_hour=13,
end_minute=59,
price_min=0.10,
price_max=0.10,
decimal_point=5
)
winter_peak = build_tou_period(
season_months=[10, 11, 12, 1, 2, 3, 4, 5],
week_days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
start_hour=17,
start_minute=0,
end_hour=21,
end_minute=59,
price_min=0.28,
price_max=0.28,
decimal_point=5
)
# Configure all periods
await mqtt_client.configure_tou_schedule(
device=device,
controller_serial_number=controller_serial,
periods=[summer_off_peak, summer_peak, winter_off_peak, winter_peak],
enabled=True
)
await mqtt_client.disconnect()
asyncio.run(configure_seasonal_tou())
Example 3: Retrieve Current TOU Settings¶
Query the device for its current TOU configuration:
from nwp500 import decode_week_bitfield, decode_price
async def check_tou_settings():
async with NavienAuthClient("user@example.com", "password") as auth_client:
api_client = NavienAPIClient(auth_client=auth_client)
device = await api_client.get_first_device()
mqtt_client = NavienMqttClient(auth_client)
await mqtt_client.connect()
# ... get controller_serial (same as Example 1) ...
# Set up response handler
response_topic = f"cmd/{device.device_info.device_type}/{mqtt_client.config.client_id}/res/tou/rd"
def on_tou_response(topic: str, message: dict):
response = message.get("response", {})
enabled = response.get("reservationUse")
periods = response.get("reservation", [])
print(f"TOU Enabled: {enabled}")
print(f"Number of periods: {len(periods)}")
for i, period in enumerate(periods, 1):
days = decode_week_bitfield(period.get("week", 0))
price_min = decode_price(
period.get("priceMin", 0),
period.get("decimalPoint", 0)
)
price_max = decode_price(
period.get("priceMax", 0),
period.get("decimalPoint", 0)
)
print(f"\nPeriod {i}:")
print(f" Days: {', '.join(days)}")
print(f" Time: {period['startHour']:02d}:{period['startMinute']:02d} "
f"- {period['endHour']:02d}:{period['endMinute']:02d}")
print(f" Price: ${price_min:.5f} - ${price_max:.5f}/kWh")
await mqtt_client.subscribe(response_topic, on_tou_response)
# Request current settings
await mqtt_client.request_tou_settings(device, controller_serial)
# Wait for response
await asyncio.sleep(5)
await mqtt_client.disconnect()
asyncio.run(check_tou_settings())
Example 4: Toggle TOU On/Off¶
Enable or disable TOU operation:
async def toggle_tou(enable: bool):
async with NavienAuthClient("user@example.com", "password") as auth_client:
api_client = NavienAPIClient(auth_client=auth_client)
device = await api_client.get_first_device()
mqtt_client = NavienMqttClient(auth_client)
await mqtt_client.connect()
# Enable or disable TOU
await mqtt_client.set_tou_enabled(device, enabled=enable)
print(f"TOU {'enabled' if enable else 'disabled'}")
await mqtt_client.disconnect()
# Enable TOU
asyncio.run(toggle_tou(True))
# Disable TOU
asyncio.run(toggle_tou(False))
Example 5: Apply Rate Plan from OpenEI¶
Use the OpenEIClient and convert_tou()/update_tou() methods to
browse, convert, and apply a rate plan from OpenEI. This is the same workflow the
Navien mobile app uses:
import asyncio
from nwp500 import (
NavienAPIClient,
NavienAuthClient,
NavienMqttClient,
OpenEIClient,
)
async def apply_openei_rate_plan():
async with NavienAuthClient("user@example.com", "password") as auth:
api_client = NavienAPIClient(auth_client=auth)
device = await api_client.get_first_device()
# 1. Browse available rate plans
async with OpenEIClient() as openei:
rates = await openei.fetch_rates("94903")
items = rates.get("items", [])
# 2. Convert all plans to device format
converted = await api_client.convert_tou(source_data=items)
# 3. Find the plan you want
plan = next(p for p in converted if "EV" in p.name)
# 4. Build tou_info dict for update
tou_info = {
"name": plan.name,
"utility": plan.utility,
"schedule": [s.model_dump() for s in plan.schedule],
"zipCode": "94903",
}
# 5. Find matching source data
source = next(
i for i in items if i.get("name") == plan.name
)
# 6. Apply to device
result = await api_client.update_tou(
mac_address=device.device_info.mac_address,
additional_value=str(device.device_info.additional_value),
tou_info=tou_info,
source_data=source,
zip_code="94903",
)
print(f"Applied: {result.name} ({result.utility})")
# 7. Enable TOU via MQTT
mqtt_client = NavienMqttClient(auth)
await mqtt_client.connect()
await mqtt_client.set_tou_enabled(device, enabled=True)
await mqtt_client.disconnect()
asyncio.run(apply_openei_rate_plan())
Notes:
Set the
OPENEI_API_KEYenvironment variable before runningGet a free key at https://openei.org/services/api/signup/
convert_tou()handles the complex format conversion server-sideThe
update_tou()method stores the plan in the Navien cloudUse
set_tou_enabled()to activate TOU mode on the device
MQTT Message Format¶
TOU Control Topic¶
To configure TOU settings, publish to:
cmd/{deviceType}/{macAddress}/ctrl/tou/rd
Message payload:
{
"cmd": "tou",
"controllerId": "controller-serial-number",
"operation": {
"reservationUse": 2,
"reservation": [
{
"season": 4095,
"week": 62,
"startHour": 0,
"startMinute": 0,
"endHour": 14,
"endMinute": 59,
"priceMin": 12000,
"priceMax": 12000,
"decimalPoint": 5
}
]
},
"requestTopic": "cmd/{deviceType}/{macAddress}/ctrl/tou/rd",
"responseTopic": "cmd/{deviceType}/{clientId}/res/tou/rd"
}
Field Descriptions¶
Field |
Type |
Description |
|---|---|---|
|
integer |
|
|
integer |
Bitfield of months (bit 0 = Jan, … bit 11 = Dec). |
|
integer |
Bitfield of days (bit 7 = Sun, bit 6 = Mon, … bit 1 = Sat; bit 0 unused). |
|
integer |
Start hour (0-23) |
|
integer |
Start minute (0-59) |
|
integer |
End hour (0-23) |
|
integer |
End minute (0-59) |
|
integer |
Encoded minimum price (see |
|
integer |
Encoded maximum price (see |
|
integer |
Number of decimal places in price encoding |
TOU Response Topic¶
The device responds on:
cmd/{deviceType}/{clientId}/res/tou/rd
Response payload matches the control payload format.
TOU Status in Device State¶
The device status includes TOU-related fields:
{
"touStatus": 1,
"touOverrideStatus": 2
}
touStatus:1if TOU scheduling is enabled/active,0if disabled/inactivetouOverrideStatus:2(ON) = TOU schedule is operating normally,1(OFF) = user has overridden TOU to force immediate heating
See Device Status Fields for more details.
Best Practices¶
Obtain controller serial number first
The controller serial number is required for TOU operations. Request it via device info before configuring TOU.
Limit number of periods
The device supports up to 16 TOU periods. Design schedules efficiently to stay within this limit.
Use appropriate decimal precision
Use
decimal_point=5for most rate plans, which provides precision down to $0.00001/kWh.Validate overlapping periods
Ensure time periods don’t overlap within the same day and month combination.
Test with simulation
Use
set_tou_enabled(False)to disable TOU temporarily for testing without losing the schedule.Monitor response topics
Always subscribe to response topics before sending commands to confirm successful configuration.
Handle timeouts gracefully
Use
asyncio.wait_for()with appropriate timeouts when waiting for device responses.
Limitations¶
Maximum 16 TOU periods per configuration
Time resolution limited to minutes (no seconds)
Price encoding limited by decimal point precision
Cannot specify different rates for individual days within a period
No support for variable rate structures (e.g., tiered rates) - only flat rate per period
Further Reading¶
API Client - API client documentation and
get_tou_info()methodMQTT Client - MQTT client and TOU configuration methods
MQTT Protocol - MQTT message formats including TOU commands
Device Status Fields - Device status fields including
touStatusOpenEI Utility Rates API - Official OpenEI API documentation
OpenEI IURDB - Interactive Utility Rate Database