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

version

integer

API version (currently 7)

format

string

Response format (json)

api_key

string

OpenEI API key (embedded in Navien app)

detail

string

Level of detail (full for complete rate structure)

address

string

ZIP code or address to search

sector

string

Customer sector (Residential, Commercial, etc.)

orderby

string

Sort field (startdate for most recent rates)

direction

string

Sort direction (desc for descending)

limit

integer

Maximum number of results (100)

Example Request

GET https://api.openei.org/utility_rates?version=7&format=json&api_key=YOUR_API_KEY&detail=full&address=94903&sector=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

utility

string

Name of the utility company

eiaid

integer

EIA (Energy Information Administration) utility ID

name

string

Rate plan name

startdate

integer

Unix timestamp when rate plan becomes effective

energyratestructure

array

Tiered rate structure by season and tier

energyweekdayschedule

array

24-hour schedule by month (0=off-peak, 1=on-peak)

energyweekendschedule

array

24-hour weekend schedule by month

mincharge

float

Minimum daily charge

fixedchargeunits

string

Units for fixed charges (e.g., $/month)

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

  • rate field contains the price per kWh

  • max field 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 energyratestructure

  • 0 typically represents off-peak, 1 represents 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 address

  • additional_value: Additional device identifier

  • controller_id: Controller serial number

  • user_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 (from OpenEIClient.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 name

  • name: Rate plan name

  • schedule: List of TOUSchedule with 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 address

  • additional_value: Additional device identifier

  • tou_info: Converted TOU schedule (from convert_tou())

  • source_data: Original OpenEI rate plan dictionary

  • zip_code: Service area zip code

  • register_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 API

  • controller_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 object

  • enabled: True to enable TOU, False to 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 object

  • controller_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 applies

  • week_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_KEY environment variable before running

  • Get a free key at https://openei.org/services/api/signup/

  • convert_tou() handles the complex format conversion server-side

  • The update_tou() method stores the plan in the Navien cloud

  • Use 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

reservationUse

integer

0 = disabled, 2 = enabled

season

integer

Bitfield of months (bit 0 = Jan, … bit 11 = Dec). 4095 = all months

week

integer

Bitfield of days (bit 7 = Sun, bit 6 = Mon, … bit 1 = Sat; bit 0 unused). 124 = weekdays

startHour

integer

Start hour (0-23)

startMinute

integer

Start minute (0-59)

endHour

integer

End hour (0-23)

endMinute

integer

End minute (0-59)

priceMin

integer

Encoded minimum price (see encode_price())

priceMax

integer

Encoded maximum price (see encode_price())

decimalPoint

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: 1 if TOU scheduling is enabled/active, 0 if disabled/inactive

  • touOverrideStatus: 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

  1. Obtain controller serial number first

    The controller serial number is required for TOU operations. Request it via device info before configuring TOU.

  2. Limit number of periods

    The device supports up to 16 TOU periods. Design schedules efficiently to stay within this limit.

  3. Use appropriate decimal precision

    Use decimal_point=5 for most rate plans, which provides precision down to $0.00001/kWh.

  4. Validate overlapping periods

    Ensure time periods don’t overlap within the same day and month combination.

  5. Test with simulation

    Use set_tou_enabled(False) to disable TOU temporarily for testing without losing the schedule.

  6. Monitor response topics

    Always subscribe to response topics before sending commands to confirm successful configuration.

  7. 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