5959 python3 lf_interop_video_streaming.py --mgr 192.168.214.219 --url "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" --media_source hls
6060 --media_quality 1080P --duration 1m --device_list 1.10,1.12 --debug --test_name video_streaming_test --expected_passfail_value 5
6161
62+ Example-11: Command Line Interface to run the Test along with IOT without device list
63+ python3 -u lf_interop_video_streaming.py --mgr 192.168.242.2 --duration 1m --url https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd --test_name videostreaming
64+ --webgui_incremental 1 --media_source dash --media_quality 4k --postcleanup --precleanup --iot_test --iot_testname "IotVideoStreaming"
65+
66+ Example-12: Command Line Interface to run the Test along with IOT with device list
67+ python3 -u lf_interop_video_streaming.py --mgr 192.168.242.2 --duration 1m --url https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd --test_name videostreaming
68+ --webgui_incremental 1 --media_source dash --media_quality 4k --postcleanup --precleanup --iot_test --iot_testname "IotVideoStreaming" --iot_device_list "switch.smart_plug_1_socket_1"
6269
6370
6471 SCRIPT CLASSIFICATION: Test
97104from lf_graph import lf_bar_graph_horizontal
98105from lf_graph import lf_line_graph
99106import threading
107+ from collections import OrderedDict
100108
101109
102110if sys .version_info [0 ] != 3 :
107115base = importlib .import_module ('py-scripts.lf_base_interop_profile' )
108116lf_csv = importlib .import_module ("py-scripts.lf_csv" )
109117realm = importlib .import_module ("py-json.realm" )
118+ LFCliBase = realm .LFCliBase
110119Realm = realm .Realm
111120base_RealDevice = base .RealDevice
112121lf_report = importlib .import_module ("py-scripts.lf_report" )
@@ -1205,7 +1214,7 @@ def handle_passfail_criteria(self, data: dict):
12051214 'Average Rx Rate (Mbps)' : avg_rx_rate_list
12061215 }
12071216
1208- def generate_report (self , date , iterations_before_test_stopped_by_user , test_setup_info , realtime_dataset , report_path = '' ):
1217+ def generate_report (self , date , iterations_before_test_stopped_by_user , test_setup_info , realtime_dataset , report_path = '' , iot_summary = None ):
12091218 logging .info ("Creating Reports" )
12101219 # Initialize the report object
12111220 if self .dowebgui and report_path == '' :
@@ -1225,15 +1234,30 @@ def generate_report(self, date, iterations_before_test_stopped_by_user, test_set
12251234 keys = list (self .http_profile .created_cx .keys ())
12261235
12271236 # Set report title, date, and build banner
1228- report .set_title ("Video Streaming Test" )
1237+ report .set_title ("Video Streaming Test Including IoT Devices" if iot_summary else "Video Streaming Test " )
12291238 report .set_date (date )
12301239 report .build_banner ()
1231- report .set_obj_html ("Objective" , " The Candela Video streaming test is designed to measure the access point performance and stability by streaming the videos from the local browser"
1232- "or from over the Internet in real clients like android which are connected to the access point,"
1233- "this test allows the user to choose the options like video link, type of media source, media quality, number of playbacks."
1234- "Along with the performance other measurements like No of Buffers, Wait-Time, per client Video Bitrate, Video Quality, and more. "
1235- "The expected behavior is for the DUT to be able to handle several stations (within the limitations of the AP specs)"
1236- "and make sure all capable clients can browse the video. " )
1240+ if iot_summary :
1241+ report .set_obj_html (
1242+ "Objective" ,
1243+ "The Candela Video Streaming Test Including IoT Devices is designed to evaluate an Access Point’s "
1244+ "performance and stability when handling both Real clients (Android, Windows, Linux, iOS) and IoT devices "
1245+ "(controlled via Home Assistant) simultaneously. "
1246+ "For Real clients, the test measures video streaming performance by playing user-defined media "
1247+ "(from a local browser or the Internet) with configurable options such as video link, media source type, "
1248+ "quality, and number of playbacks. Metrics such as buffering events, wait-time, per-client video bitrate, "
1249+ "and video quality are captured to validate the AP’s ability to sustain smooth playback across multiple clients. "
1250+ "For IoT clients, the test concurrently executes device-specific actions (e.g., camera streaming, switch toggling, "
1251+ "lock/unlock) and tracks success rate, latency, and failure rate. The goal is to ensure the AP can reliably support "
1252+ "video streaming for multiple real clients while maintaining responsive and consistent performance for IoT devices."
1253+ )
1254+ else :
1255+ report .set_obj_html ("Objective" , " The Candela Video streaming test is designed to measure the access point performance and stability by streaming the videos from the local browser"
1256+ "or from over the Internet in real clients like android which are connected to the access point,"
1257+ "this test allows the user to choose the options like video link, type of media source, media quality, number of playbacks."
1258+ "Along with the performance other measurements like No of Buffers, Wait-Time, per client Video Bitrate, Video Quality, and more. "
1259+ "The expected behavior is for the DUT to be able to handle several stations (within the limitations of the AP specs)"
1260+ "and make sure all capable clients can browse the video. " )
12371261 report .build_objective ()
12381262 report .set_table_title ("Input Parameters" )
12391263 report .build_table_title ()
@@ -1247,7 +1271,8 @@ def generate_report(self, date, iterations_before_test_stopped_by_user, test_set
12471271 # Create a string by joining the mapped pairs
12481272 gp_map = ", " .join (f"{ group } -> { profile } " for group , profile in gp_pairs )
12491273 test_setup_info ["Configuration" ] = gp_map
1250-
1274+ if iot_summary :
1275+ test_setup_info = with_iot_params_in_table (test_setup_info , iot_summary )
12511276 report .test_setup_table (value = "Test Setup Information" , test_setup_data = test_setup_info )
12521277
12531278 device_type = []
@@ -1503,6 +1528,8 @@ def generate_report(self, date, iterations_before_test_stopped_by_user, test_set
15031528 dataframe3 = pd .DataFrame (dataframe2 )
15041529 report .set_table_dataframe (dataframe3 )
15051530 report .build_table ()
1531+ if iot_summary :
1532+ self .build_iot_report_section (report , iot_summary )
15061533 report .build_footer ()
15071534 report .write_html ()
15081535 report .write_pdf ()
@@ -1796,16 +1823,155 @@ def updating_webui_running_json(self):
17961823 with open (file_path , 'w' ) as file :
17971824 json .dump (data , file , indent = 4 )
17981825
1826+ def build_iot_report_section (self , report , iot_summary ):
1827+ """
1828+ Handles all IoT-related charts, tables, and increment-wise reports.
1829+ """
1830+ outdir = report .path_date_time
1831+ os .makedirs (outdir , exist_ok = True )
1832+
1833+ def copy_into_report (raw_path , new_name ):
1834+ """Resolve and copy image into report dir."""
1835+ if not raw_path :
1836+ return None
1837+
1838+ abs_src = os .path .abspath (raw_path )
1839+ if not os .path .exists (abs_src ):
1840+ # Search recursively under 'results' if absolute path missing
1841+ for root , _ , files in os .walk (os .path .join (os .getcwd (), "results" )):
1842+ if os .path .basename (raw_path ) in files :
1843+ abs_src = os .path .join (root , os .path .basename (raw_path ))
1844+ break
1845+ else :
1846+ return None
1847+
1848+ dst = os .path .join (outdir , new_name )
1849+ if os .path .abspath (abs_src ) != os .path .abspath (dst ):
1850+ shutil .copy2 (abs_src , dst )
1851+ return new_name
1852+
1853+ # section header
1854+ report .set_custom_html ('<div style="page-break-before: always;"></div>' )
1855+ report .build_custom ()
1856+ report .set_custom_html ('<h2><u>IoT Results</u></h2>' )
1857+ report .build_custom ()
1858+
1859+ # Statistics
1860+ stats_png = copy_into_report (iot_summary .get ("statistics_img" ), "iot_statistics.png" )
1861+ if stats_png :
1862+ report .build_chart_title ("Test Statistics" )
1863+ report .set_custom_html (f'<img src="{ stats_png } " style="width:100%; height:auto;">' )
1864+ report .build_custom ()
1865+
1866+ # Request vs Latency
1867+ rvl_png = copy_into_report (iot_summary .get ("req_vs_latency_img" ), "iot_request_vs_latency.png" )
1868+ if rvl_png :
1869+ report .build_chart_title ("Request vs Average Latency" )
1870+ report .set_custom_html (f'<img src="{ rvl_png } " style="width:100%;">' )
1871+ report .build_custom ()
1872+
1873+ # Overall results table
1874+ ort = iot_summary .get ("overall_result_table" ) or {}
1875+ if ort :
1876+ rows = [{
1877+ "Device" : dev ,
1878+ "Min Latency (ms)" : stats .get ("min_latency" ),
1879+ "Avg Latency (ms)" : stats .get ("avg_latency" ),
1880+ "Max Latency (ms)" : stats .get ("max_latency" ),
1881+ "Total Iterations" : stats .get ("total_iterations" ),
1882+ "Success Iters" : stats .get ("success_iterations" ),
1883+ "Failed Iters" : stats .get ("failed_iterations" ),
1884+ "No-Response Iters" : stats .get ("no_response_iterations" ),
1885+ } for dev , stats in ort .items ()]
1886+
1887+ df_overall = pd .DataFrame (rows ).round (2 )
1888+
1889+ report .set_custom_html ('<div style="page-break-inside: avoid;">' )
1890+ report .build_custom ()
1891+ report .set_obj_html (_obj_title = "Overall IoT Result Table" , _obj = " " )
1892+ report .build_objective ()
1893+ report .set_table_dataframe (df_overall )
1894+ report .build_table ()
1895+ report .set_custom_html ('</div>' )
1896+ report .build_custom ()
1897+
1898+ # Increment reports
1899+ inc = iot_summary .get ("increment_reports" ) or {}
1900+ if inc :
1901+ report .set_custom_html ('<h3>Reports by Increment Steps</h3>' )
1902+ report .build_custom ()
1903+
1904+ for step_name , rep in inc .items ():
1905+
1906+ report .set_custom_html (f'<h4><u>{ step_name .replace ("_" , " " )} </u></h4>' )
1907+ report .build_custom ()
1908+
1909+ # Latency graph
1910+ lat_png = copy_into_report (rep .get ("latency_graph" ), f"iot_{ step_name } _latency.png" )
1911+ if lat_png :
1912+ report .build_chart_title ("Average Latency" )
1913+ report .set_custom_html (f'<img src="{ lat_png } " style="width:100%; height:auto;">' )
1914+ report .build_custom ()
1915+
1916+ # Success count graph
1917+ res_png = copy_into_report (rep .get ("result_graph" ), f"iot_{ step_name } _results.png" )
1918+ if res_png :
1919+ report .build_chart_title ("Success Count" )
1920+ report .set_custom_html (f'<img src="{ res_png } " style="width:100%; height:auto;">' )
1921+ report .build_custom ()
1922+
1923+ # Tabular data for detailed iteration-level results
1924+ data_rows = rep .get ("data" ) or []
1925+ if data_rows :
1926+ df = pd .DataFrame (data_rows ).rename (
1927+ columns = {"latency__ms" : "Latency_ms" , "latency_ms" : "Latency_ms" }
1928+ )
1929+ if "Latency_ms" in df .columns :
1930+ df ["Latency_ms" ] = pd .to_numeric (df ["Latency_ms" ], errors = "coerce" ).round (3 )
1931+ if "Result" in df .columns :
1932+ df ["Result" ] = df ["Result" ].map (lambda x : "Success" if bool (x ) else "Failure" )
1933+
1934+ desired_cols = ["Iteration" , "Device" , "Current State" , "Latency_ms" , "Result" ]
1935+ df = df [[c for c in desired_cols if c in df .columns ]]
1936+
1937+ report .set_table_dataframe (df )
1938+ report .build_table ()
1939+
1940+ report .set_custom_html ('<hr>' )
1941+ report .build_custom ()
1942+
1943+
1944+ def with_iot_params_in_table (base : dict , iot_summary ) -> dict :
1945+ """
1946+ Append IoT params into the existing Throughput Input Parameters table.
1947+ Adds: IoT Test name, IoT Iterations, IoT Delay (s), IoT Increment.
1948+ Accepts dict or JSON string.
1949+ """
1950+ try :
1951+ if not iot_summary :
1952+ return base
1953+ if isinstance (iot_summary , str ):
1954+ try :
1955+ iot_summary = json .loads (iot_summary )
1956+ except Exception :
1957+ start = iot_summary .find ("{" )
1958+ end = iot_summary .rfind ("}" )
1959+ if start == - 1 or end == - 1 or end <= start :
1960+ return base
1961+ try :
1962+ iot_summary = json .loads (iot_summary [start :end + 1 ])
1963+ except Exception :
1964+ return base
17991965
1800- def duration_to_seconds ( duration ):
1801- duration = duration . strip (). lower ( )
1802- if duration . endswith ( "m" ):
1803- return int ( duration [: - 1 ]) * 60
1804- if duration . endswith ( "h" ):
1805- return int ( duration [: - 1 ]) * 3600
1806- if duration . endswith ( "s" ):
1807- return int ( duration [: - 1 ])
1808- return int ( duration ) * 60
1966+ ti = ( iot_summary . get ( "test_input_table" ) or {})
1967+ out = OrderedDict ( base )
1968+ out [ "Iot Device List" ] = ti . get ( "Device List" , "" )
1969+ out [ "IoT Iterations" ] = ti . get ( "Iterations" , "" )
1970+ out [ "IoT Delay (s)" ] = ti . get ( "Delay (seconds)" , "" )
1971+ out [ "IoT Increment" ] = ti . get ( "Increment Pattern" , "" )
1972+ return out
1973+ except Exception :
1974+ return base
18091975
18101976
18111977def trigger_iot (ip , port , iterations , delay , device_list , testname , increment ):
@@ -2301,7 +2467,7 @@ def main():
23012467 thread = threading .Thread (target = trigger_iot , args = (iot_ip , iot_port , iot_iterations , iot_delay , iot_device_list , iot_testname , iot_increment ))
23022468 thread .start ()
23032469 else :
2304- total_secs = duration_to_seconds ( args .duration )
2470+ total_secs = int ( LFCliBase . parse_time ( args .duration ). total_seconds () )
23052471 iot_iterations = max (1 , total_secs // args .iot_delay )
23062472 iot_thread = threading .Thread (
23072473 target = trigger_iot ,
@@ -2453,12 +2619,19 @@ def main():
24532619 break
24542620 obj .stop ()
24552621 date = str (datetime .now ()).split ("," )[0 ].replace (" " , "-" ).split ("." )[0 ]
2622+ iot_summary = None
2623+ if args .iot_test and args .iot_testname :
2624+ base = os .path .join ("results" , args .iot_testname )
2625+ p = os .path .join (base , "iot_summary.json" )
2626+ if os .path .exists (p ):
2627+ with open (p ) as f :
2628+ iot_summary = json .load (f )
24562629
24572630 # prev_inc_value = 0
24582631 if obj .resource_ids and obj .incremental :
2459- obj .generate_report (date , list (set (iterations_before_test_stopped_by_user )), test_setup_info = test_setup_info , realtime_dataset = individual_df )
2632+ obj .generate_report (date , list (set (iterations_before_test_stopped_by_user )), test_setup_info = test_setup_info , realtime_dataset = individual_df , iot_summary = iot_summary )
24602633 elif obj .resource_ids :
2461- obj .generate_report (date , list (set (iterations_before_test_stopped_by_user )), test_setup_info = test_setup_info , realtime_dataset = individual_df )
2634+ obj .generate_report (date , list (set (iterations_before_test_stopped_by_user )), test_setup_info = test_setup_info , realtime_dataset = individual_df , iot_summary = iot_summary )
24622635
24632636 # Perform post-cleanup operations
24642637 if args .postcleanup :
0 commit comments