Device Profiles: TR-181 Parameter Templates
> This notebook demonstrates loading and working with device profiles for simulated CPE devices.
Device profiles are JSON files that define the TR-181 parameter tree for different device types. Caretaker includes profiles for:
- Fiber ONT - GPON optical network terminals
- Cable Modem - DOCSIS cable modems
- MikroTik - RouterOS-based routers with vendor extensions
Setup
Mix.install([
{:caretaker, path: "."}
])
alias Caretaker.CPE.{DeviceState, Fleet}
require Logger
# Define profile paths for easy access
profiles_dir = Path.join(File.cwd!(), "priv/profiles")
fiber_ont_path = Path.join(profiles_dir, "fiber_ont.json")
cable_modem_path = Path.join(profiles_dir, "cable_modem.json")
mikrotik_path = Path.join(profiles_dir, "mikrotik.json")
Logger.info("Profile directory: #{profiles_dir}")
Logger.info("Available profiles: fiber_ont.json, cable_modem.json, mikrotik.json")
%{
profiles_dir: profiles_dir,
fiber_ont: fiber_ont_path,
cable_modem: cable_modem_path,
mikrotik: mikrotik_path
}
Section 1: Loading a Fiber ONT Profile
Fiber ONTs (Optical Network Terminals) are common in FTTH deployments. This profile includes optical signal parameters and GPON-specific data.
# Create a device state and load the fiber ONT profile
{:ok, fiber_ont} = DeviceState.start_link(
device_id: %{
oui: "A1B2C3",
product_class: "FiberONT",
serial_number: "FON123456789"
}
)
# Load the profile from JSON file
:ok = DeviceState.load_profile(fiber_ont, fiber_ont_path)
Logger.info("Loaded Fiber ONT profile")
# Check basic device info
manufacturer = DeviceState.get(fiber_ont, "Device.DeviceInfo.Manufacturer")
model = DeviceState.get(fiber_ont, "Device.DeviceInfo.ModelName")
software = DeviceState.get(fiber_ont, "Device.DeviceInfo.SoftwareVersion")
Logger.info("Manufacturer: #{manufacturer}")
Logger.info("Model: #{model}")
Logger.info("Software Version: #{software}")
# Check fiber-specific optical parameters
optical_signal = DeviceState.get(fiber_ont, "Device.Optical.Interface.1.OpticalSignalLevel")
transmit_level = DeviceState.get(fiber_ont, "Device.Optical.Interface.1.TransmitOpticalLevel")
Logger.info("Optical Signal Level: #{optical_signal} dBm")
Logger.info("Transmit Optical Level: #{transmit_level} dBm")
%{
manufacturer: manufacturer,
model: model,
software: software,
optical_signal_dbm: optical_signal,
transmit_level_dbm: transmit_level
}
Section 2: Loading a Cable Modem Profile
Cable modems use DOCSIS technology with downstream/upstream channel configurations.
# Create a cable modem device
{:ok, cable_modem} = DeviceState.start_link(
device_id: %{
oui: "D4E5F6",
product_class: "CableModem",
serial_number: "CM987654321"
}
)
:ok = DeviceState.load_profile(cable_modem, cable_modem_path)
Logger.info("Loaded Cable Modem profile")
# Check device info
manufacturer = DeviceState.get(cable_modem, "Device.DeviceInfo.Manufacturer")
model = DeviceState.get(cable_modem, "Device.DeviceInfo.ModelName")
docsis_version = DeviceState.get(cable_modem, "Device.CableModem.DOCSISVersion")
Logger.info("Manufacturer: #{manufacturer}")
Logger.info("Model: #{model}")
Logger.info("DOCSIS Version: #{docsis_version}")
# Check downstream channel parameters
downstream_freq = DeviceState.get(cable_modem, "Device.CableModem.DownstreamChannel.1.Frequency")
downstream_power = DeviceState.get(cable_modem, "Device.CableModem.DownstreamChannel.1.PowerLevel")
downstream_snr = DeviceState.get(cable_modem, "Device.CableModem.DownstreamChannel.1.SNRLevel")
modulation = DeviceState.get(cable_modem, "Device.CableModem.DownstreamChannel.1.Modulation")
Logger.info("Downstream Frequency: #{downstream_freq} Hz")
Logger.info("Downstream Power: #{downstream_power} dBmV")
Logger.info("Downstream SNR: #{downstream_snr} dB")
Logger.info("Modulation: #{modulation}")
%{
manufacturer: manufacturer,
model: model,
docsis_version: docsis_version,
downstream: %{
frequency_hz: downstream_freq,
power_dbmv: downstream_power,
snr_db: downstream_snr,
modulation: modulation
}
}
Section 3: Loading a MikroTik Profile
MikroTik routers have extensive TR-181 support plus vendor-specific extensions (XMIKROTIK*).
# Create a MikroTik router device
{:ok, mikrotik} = DeviceState.start_link(
device_id: %{
oui: "D4CA6D",
product_class: "RouterOS",
serial_number: "A1B2C3D4E5F6"
}
)
:ok = DeviceState.load_profile(mikrotik, mikrotik_path)
Logger.info("Loaded MikroTik profile")
# Check device info with vendor extensions
manufacturer = DeviceState.get(mikrotik, "Device.DeviceInfo.Manufacturer")
model = DeviceState.get(mikrotik, "Device.DeviceInfo.ModelName")
board_name = DeviceState.get(mikrotik, "Device.DeviceInfo.X_MIKROTIK_BoardName")
architecture = DeviceState.get(mikrotik, "Device.DeviceInfo.X_MIKROTIK_Architecture")
cpu_count = DeviceState.get(mikrotik, "Device.DeviceInfo.X_MIKROTIK_CPUCount")
license_level = DeviceState.get(mikrotik, "Device.DeviceInfo.X_MIKROTIK_License")
Logger.info("Manufacturer: #{manufacturer}")
Logger.info("Model: #{model}")
Logger.info("Board Name: #{board_name}")
Logger.info("Architecture: #{architecture}")
Logger.info("CPU Count: #{cpu_count}")
Logger.info("License Level: #{license_level}")
# Check system resources
cpu_usage = DeviceState.get(mikrotik, "Device.DeviceInfo.ProcessStatus.CPUUsage")
memory_total = DeviceState.get(mikrotik, "Device.DeviceInfo.MemoryStatus.Total")
memory_free = DeviceState.get(mikrotik, "Device.DeviceInfo.MemoryStatus.Free")
Logger.info("CPU Usage: #{cpu_usage}%")
Logger.info("Memory: #{memory_free}/#{memory_total} KB free")
%{
manufacturer: manufacturer,
model: model,
vendor_extensions: %{
board_name: board_name,
architecture: architecture,
cpu_count: cpu_count,
license_level: license_level
},
resources: %{
cpu_usage_pct: cpu_usage,
memory_total_kb: memory_total,
memory_free_kb: memory_free
}
}
Section 4: Exploring Profile Parameters
DeviceState provides multiple ways to query parameters:
-
get/2- Get a single parameter value -
get_tree/2- Get all parameters under a path as a nested map -
get_parameters/2- Get parameters as a flat list with types
# Use get_tree to explore a subtree
Logger.info("=== Exploring MikroTik Routing Configuration ===")
routing_tree = DeviceState.get_tree(mikrotik, "Device.Routing.")
Logger.info("Routing tree keys: #{inspect(Map.keys(routing_tree))}")
# Get IPv4 forwarding rules
ipv4_forwarding = DeviceState.get_tree(mikrotik, "Device.Routing.Router.1.IPv4Forwarding.")
Logger.info("IPv4 Forwarding entries: #{map_size(ipv4_forwarding)}")
# Get the default route details
default_route = DeviceState.get_tree(mikrotik, "Device.Routing.Router.1.IPv4Forwarding.1.")
Logger.info("Default route: #{inspect(default_route)}")
%{
routing_tree: routing_tree,
ipv4_forwarding_count: map_size(ipv4_forwarding),
default_route: default_route
}
# Use get_parameters to get a flat list with type information
Logger.info("=== Using get_parameters for Flat Parameter Lists ===")
# Get all DeviceInfo parameters as a flat list
device_info_params = DeviceState.get_parameters(fiber_ont, "Device.DeviceInfo.")
Logger.info("Found #{length(device_info_params)} parameters under Device.DeviceInfo")
# Show first 5 parameters with their types
device_info_params
|> Enum.take(5)
|> Enum.each(fn param ->
Logger.info(" #{param.name} = #{param.value} (#{param.type})")
end)
device_info_params
# Use get_parameter_names for discovery
Logger.info("=== Parameter Name Discovery ===")
# Get immediate children (next_level: true)
children = DeviceState.get_parameter_names(mikrotik, "Device.", true)
Logger.info("Device. children: #{length(children)}")
Enum.each(children, fn p -> Logger.info(" #{p.name}") end)
# Get all leaf parameters (next_level: false)
all_ip_params = DeviceState.get_parameter_names(mikrotik, "Device.IP.", false)
Logger.info("\nAll Device.IP parameters: #{length(all_ip_params)}")
%{
device_children: children,
ip_param_count: length(all_ip_params)
}
Section 5: Comparing Device Capabilities
Different device types have different parameter trees. Let’s compare them.
Logger.info("=== Device Capability Comparison ===")
# Get top-level tree keys for each device
fiber_keys = DeviceState.get_tree(fiber_ont, "Device.") |> Map.keys() |> Enum.sort()
cable_keys = DeviceState.get_tree(cable_modem, "Device.") |> Map.keys() |> Enum.sort()
mikrotik_keys = DeviceState.get_tree(mikrotik, "Device.") |> Map.keys() |> Enum.sort()
Logger.info("Fiber ONT top-level: #{inspect(fiber_keys)}")
Logger.info("Cable Modem top-level: #{inspect(cable_keys)}")
Logger.info("MikroTik top-level: #{inspect(mikrotik_keys)}")
# Find device-specific capabilities
fiber_only = fiber_keys -- cable_keys -- mikrotik_keys
cable_only = cable_keys -- fiber_keys -- mikrotik_keys
mikrotik_only = mikrotik_keys -- fiber_keys -- cable_keys
common = fiber_keys -- fiber_only
Logger.info("\nFiber ONT specific: #{inspect(fiber_only)}")
Logger.info("Cable Modem specific: #{inspect(cable_only)}")
Logger.info("MikroTik specific: #{inspect(mikrotik_only)}")
Logger.info("Common to all: #{inspect(common)}")
%{
fiber_ont: fiber_keys,
cable_modem: cable_keys,
mikrotik: mikrotik_keys,
fiber_only: fiber_only,
cable_only: cable_only,
mikrotik_only: mikrotik_only,
common: common
}
# Compare interface statistics across devices
Logger.info("=== Interface Statistics Comparison ===")
# Get interface stats from each device
fiber_stats = DeviceState.get_tree(fiber_ont, "Device.IP.Interface.1.Stats.")
cable_stats = DeviceState.get_tree(cable_modem, "Device.IP.Interface.1.Stats.")
mikrotik_stats = DeviceState.get_tree(mikrotik, "Device.IP.Interface.1.Stats.")
# Compare bytes sent/received
devices_stats = [
{"Fiber ONT", fiber_stats},
{"Cable Modem", cable_stats},
{"MikroTik", mikrotik_stats}
]
Enum.each(devices_stats, fn {name, stats} ->
bytes_sent = Map.get(stats, "BytesSent", 0)
bytes_recv = Map.get(stats, "BytesReceived", 0)
Logger.info("#{name}: Sent=#{bytes_sent} bytes, Received=#{bytes_recv} bytes")
end)
%{
fiber_ont: fiber_stats,
cable_modem: cable_stats,
mikrotik: mikrotik_stats
}
Section 6: Creating Custom Profiles from Maps
You can create device profiles programmatically using Elixir maps.
# Define a custom profile as a map
custom_profile = %{
"Device" => %{
"DeviceInfo" => %{
"Manufacturer" => "CustomVendor",
"ManufacturerOUI" => "CUSTOM",
"ModelName" => "CUSTOM-ROUTER-X1",
"Description" => "Custom Router for Testing",
"ProductClass" => "CustomRouter",
"SerialNumber" => "CUSTOM001",
"HardwareVersion" => "1.0",
"SoftwareVersion" => "3.2.1",
"UpTime" => 3600
},
"ManagementServer" => %{
"URL" => "http://acs.custom.com:7547/",
"Username" => "custom_user",
"PeriodicInformEnable" => true,
"PeriodicInformInterval" => 600
},
"IP" => %{
"Interface" => %{
"1" => %{
"Enable" => true,
"Status" => "Up",
"Name" => "wan0",
"IPv4Address" => %{
"1" => %{
"Enable" => true,
"IPAddress" => "203.0.113.50",
"SubnetMask" => "255.255.255.0"
}
}
}
}
},
# Custom vendor extensions
"X_CUSTOM_Features" => %{
"AdvancedRouting" => true,
"QoSEnabled" => true,
"MaxConnections" => 10000
}
}
}
# Create device with custom profile params
{:ok, custom_device} = DeviceState.start_link(
device_id: %{
oui: "CUSTOM",
product_class: "CustomRouter",
serial_number: "CUSTOM001"
},
params: custom_profile
)
Logger.info("Created custom device with inline profile")
# Verify custom parameters
manufacturer = DeviceState.get(custom_device, "Device.DeviceInfo.Manufacturer")
custom_feature = DeviceState.get(custom_device, "Device.X_CUSTOM_Features.AdvancedRouting")
max_connections = DeviceState.get(custom_device, "Device.X_CUSTOM_Features.MaxConnections")
Logger.info("Manufacturer: #{manufacturer}")
Logger.info("Advanced Routing: #{custom_feature}")
Logger.info("Max Connections: #{max_connections}")
%{
manufacturer: manufacturer,
custom_features: %{
advanced_routing: custom_feature,
max_connections: max_connections
}
}
Section 7: Modifying Parameters and Checking Persistence
DeviceState maintains parameter changes in memory.
Logger.info("=== Parameter Modification and Persistence ===")
# Get original value
original_version = DeviceState.get(fiber_ont, "Device.DeviceInfo.SoftwareVersion")
Logger.info("Original SoftwareVersion: #{original_version}")
# Update the parameter
DeviceState.set(fiber_ont, "Device.DeviceInfo.SoftwareVersion", "3.0.0-upgraded")
# Verify the change persisted
new_version = DeviceState.get(fiber_ont, "Device.DeviceInfo.SoftwareVersion")
Logger.info("New SoftwareVersion: #{new_version}")
# Update multiple parameters
DeviceState.update_parameters(fiber_ont, [
%{name: "Device.DeviceInfo.ProvisioningCode", value: "PROV-2024-001"},
%{name: "Device.DeviceInfo.Description", value: "Updated Fiber ONT"},
%{name: "Device.IP.Interface.1.IPv4Address.1.IPAddress", value: "192.168.1.200"}
])
# Verify all changes
prov_code = DeviceState.get(fiber_ont, "Device.DeviceInfo.ProvisioningCode")
description = DeviceState.get(fiber_ont, "Device.DeviceInfo.Description")
ip_address = DeviceState.get(fiber_ont, "Device.IP.Interface.1.IPv4Address.1.IPAddress")
Logger.info("Provisioning Code: #{prov_code}")
Logger.info("Description: #{description}")
Logger.info("IP Address: #{ip_address}")
%{
original_version: original_version,
new_version: new_version,
provisioning_code: prov_code,
description: description,
ip_address: ip_address
}
# Adding and deleting object instances
Logger.info("=== Object Instance Management ===")
# Add a new IP interface instance
{:ok, instance_num} = DeviceState.add_object_instance(fiber_ont, "Device.IP.Interface.")
Logger.info("Added new interface instance: #{instance_num}")
# Configure the new interface
DeviceState.set(fiber_ont, "Device.IP.Interface.#{instance_num}.Enable", true)
DeviceState.set(fiber_ont, "Device.IP.Interface.#{instance_num}.Name", "eth1")
DeviceState.set(fiber_ont, "Device.IP.Interface.#{instance_num}.Status", "Up")
# Verify the new interface
new_interface = DeviceState.get_tree(fiber_ont, "Device.IP.Interface.#{instance_num}.")
Logger.info("New interface config: #{inspect(new_interface)}")
# List all interfaces now
interfaces = DeviceState.get_tree(fiber_ont, "Device.IP.Interface.")
Logger.info("Total interfaces: #{map_size(interfaces)}")
# Delete the instance
:ok = DeviceState.delete_object_instance(fiber_ont, "Device.IP.Interface.#{instance_num}.")
Logger.info("Deleted interface instance #{instance_num}")
# Verify deletion
remaining = DeviceState.get_tree(fiber_ont, "Device.IP.Interface.")
Logger.info("Remaining interfaces: #{map_size(remaining)}")
%{
added_instance: instance_num,
interface_config: new_interface,
final_interface_count: map_size(remaining)
}
Section 8: Using Profiles with Fleet for Mixed-Device Testing
The Fleet module supports mixed device populations using profile distributions.
Logger.info("=== Fleet with Mixed Device Profiles ===")
# Create a fleet with multiple device types
{:ok, mixed_fleet} = Fleet.start_link(
acs_url: "http://localhost:4000/cwmp",
count: 10,
oui_prefix: "MIXED",
connection_delay: 10, # Fast spawning for demo
profiles: [
{40, :fiber_ont}, # 40% fiber ONTs
{30, :cable_modem}, # 30% cable modems
{30, :mikrotik} # 30% MikroTik routers (uses default params)
]
)
Logger.info("Created mixed fleet")
# Spawn all devices
{:ok, count} = Fleet.spawn_devices(mixed_fleet)
Logger.info("Spawned #{count} devices")
# List devices and their profiles
devices = Fleet.list_devices(mixed_fleet)
Logger.info("\nDevice distribution:")
profile_counts = Enum.reduce(devices, %{}, fn device, acc ->
Map.update(acc, device.profile, 1, &(&1 + 1))
end)
Enum.each(profile_counts, fn {profile, count} ->
Logger.info(" #{profile}: #{count} devices")
end)
%{
total_devices: length(devices),
profile_distribution: profile_counts
}
# Query parameters across different device types in the fleet
Logger.info("=== Querying Fleet Devices by Type ===")
# Get a device of each type
devices = Fleet.list_devices(mixed_fleet)
fiber_device = Enum.find(devices, fn d -> d.profile == :fiber_ont end)
cable_device = Enum.find(devices, fn d -> d.profile == :cable_modem end)
Logger.info("Found devices:")
Logger.info(" Fiber ONT: #{fiber_device && fiber_device.serial_number}")
Logger.info(" Cable Modem: #{cable_device && cable_device.serial_number}")
# Update a parameter on all fiber ONTs
Fleet.update_all_params(mixed_fleet, "Device.DeviceInfo.ProvisioningCode", "FLEET-PROV-2024")
Logger.info("Updated provisioning code on all fleet devices")
# Get fleet statistics
stats = Fleet.stats(mixed_fleet)
Logger.info("""
Fleet Statistics:
Total: #{stats.total}
Spawned: #{stats.spawned}
Memory delta: #{if stats.memory_delta_bytes, do: Float.round(stats.memory_delta_bytes / 1024, 2), else: "N/A"} KB
Memory per device: #{if stats.memory_per_device_bytes, do: Float.round(stats.memory_per_device_bytes / 1024, 2), else: "N/A"} KB
""")
stats
# Add a device with a custom inline profile to the fleet
Logger.info("=== Adding Custom Profile Device to Fleet ===")
custom_router_profile = %{
"Device" => %{
"DeviceInfo" => %{
"Manufacturer" => "FleetCustom",
"ModelName" => "CUSTOM-FLEET-01",
"Description" => "Dynamically added custom device",
"SoftwareVersion" => "1.0.0"
},
"ManagementServer" => %{
"PeriodicInformEnable" => true,
"PeriodicInformInterval" => 300
}
}
}
# Add with custom profile (map is passed as the profile)
{:ok, custom_serial} = Fleet.add_device(mixed_fleet,
serial_number: "CUSTOM-FLEET-001",
profile: custom_router_profile
)
Logger.info("Added custom device: #{custom_serial}")
# Verify the device was added
{:ok, custom_info} = Fleet.get_device(mixed_fleet, custom_serial)
Logger.info("Custom device info: #{inspect(custom_info)}")
# Get updated device list
devices = Fleet.list_devices(mixed_fleet)
Logger.info("Total fleet size now: #{length(devices)}")
%{
custom_serial: custom_serial,
custom_info: custom_info,
total_devices: length(devices)
}
Cleanup
# Stop all fleet devices
Fleet.stop_all(mixed_fleet)
# Stop individual device agents
Agent.stop(fiber_ont)
Agent.stop(cable_modem)
Agent.stop(mikrotik)
Agent.stop(custom_device)
Logger.info("All devices stopped and cleaned up")
:ok