import { Command } from "commander" ; import { log } from "console" ; import * as fs from "fs" ; import { AssetManagementModels , MindSphereSdk } from "../../api/sdk" ; import { checkAssetId , throwError } from "../../api/utils" ; import { adjustColor , directoryReadyLog , errorLog , getColor , getSdk , homeDirLog , proxyLog , subtractSecond , verboseLog , } from "./command-utils" ; import ora = require ( "ora" ) ; let color = getColor ( "magenta" ) ; const yellow = getColor ( "yellow" ) ; export default ( program ) => { program . command ( "prepare-bulk" ) . alias ( "pb" ) . option ( "-d, --dir <directoryname>" , "config file with agent configuration" , "bulkupload" ) . option ( "-w, --twintype <mode>" , "twintype of asset [performance|simulation]" ) . option ( "-i, --assetid <assetid>" , "mindsphere asset id " ) . option ( "-t, --typeid <typeid>" , "typeid e.g. castidev.Engine " ) . option ( "-s, --size <size>" , "entries per file " , "100" ) . option ( "-f, --files <files>" , "generated files " , "2" ) . option ( "-o, --offset <days>" , "offset in days from current date " , "0" ) . option ( "-y, --retry <number>" , "retry attempts before giving up" , "3" ) . option ( "-k, --passkey <passkey>" , "passkey" ) . option ( "-v, --verbose" , "verbose output" ) . description ( color ( "creates a template directory for timeseries (bulk) upload *" ) ) . action ( ( options ) => { ( async ( ) => { try { checkRequiredParamaters ( options ) ; const sdk = getSdk ( options ) ; color = adjustColor ( color , options ) ; homeDirLog ( options . verbose , color ) ; proxyLog ( options . verbose , color ) ; const path = makeCsvAndJsonDir ( options ) ; const { root , asset , aspects } = await getAssetAndAspects ( sdk , options ) ; checkTwinTypeOfAsset ( asset , options ) ; const spinner = ora ( "generating data..." ) ; ! options . verbose && spinner . start ( ) ; for ( const aspect of aspects ) { createAspectDirs ( path , aspect , options ) ; const variables = aspect . variables || aspect . aspectType . variables || [ ] ; await generateCsv ( { name : aspect . name , variables : variables , options : options , path : path , mode : options . twintype , } , spinner ) ; } options . typeid ? writeNewAssetJson ( options , root , path , options . twintype ) : fs . writeFileSync ( `${ path }/asset.json` , JSON . stringify ( asset , null , 2 ) ) ; ! options . verbose && spinner . succeed ( "done." ) ; directoryReadyLog ( { path : `${ path }` , jobCommand : "check-bulk" , runCommand : "run-bulk" , } ) ; options . twintype === AssetManagementModels . TwinType . Performance && options . offset < 7 && console . log ( `\n${ yellow ( "Warning!" ) } The data has to be older then 7 days for bulk upload of performance assets.\n` ) ; } catch ( err ) { errorLog ( err , options . verbose ) ; } } ) ( ) ; } ) . on ( "--help" , ( ) => { log ( "\n  Examples:\n" ) ; log ( `    mc prepare-bulk  --typeid castidev.Engine \t this creates a directory called ${ color ( "bulkimport" ) } for new asset of type castidev.Engine` ) ; log ( `    mc pb --dir asset1 -i 123456...abc \t\t this creates a directory called ${ color ( "asset1" ) } for existing asset` ) ; log ( `    mc pb -of 3 -t castidev.Engine \t\t start data creation template 3 days before now` ) ; log ( `\n\tuse --mode ${ color ( "performance" ) } for standard data generation or --mode ${ color ( "simulation" ) } for high frequency data generation ` ) ; log ( `\tThe typeid must be derived from ${ color ( "core.basicdevice" ) } and asset ${ color ( "twintype" ) } must be ${ color ( "simulation" ) } for high frequency data upload\n` ) ; } ) ; } ; async function getAssetAndAspects ( sdk , options ) { const assetMgmt = sdk . GetAssetManagementClient ( ) ; let aspects = [ ] ; let asset ; if ( options . assetid ) { asset = await assetMgmt . GetAsset ( options . assetid ) ; const embAspects = await assetMgmt . GetAspects ( options . assetid ) ; if ( embAspects . _embedded && embAspects . _embedded . aspects ) { aspects = embAspects . _embedded . aspects . filter ( ( x ) => { return x . category === AssetManagementModels . AspectResource . CategoryEnum . Dynamic ; } ) ; } } if ( options . typeid ) { const assetType = await assetMgmt . GetAssetType ( options . typeid , { exploded : true , } ) ; if ( assetType . aspects ) { aspects = assetType . aspects . filter ( ( x ) => { return x . aspectType && x . aspectType . category === "dynamic" ; } ) ; } aspects = aspects || [ ] ; } const root = await assetMgmt . GetRootAsset ( ) ; return { root , asset , aspects } ; } function checkRequiredParamaters ( options ) { options . twintype !== "performance" && options . twintype !== "simulation" && throwError ( "you have to specify the twin type performance or simulation" ) ; ! options . typeid && ! options . assetid && throwError ( "You have to specify either a typeid or assetid" ) ; ! ! options . typeid === ! ! options . assetid && throwError ( "You can't specify typeid and assetid at the same time" ) ; verboseLog ( `creating directory template for: ${ color ( options . typeid || options . assetid ) }` , options . verbose ) ; options . assetid && checkAssetId ( options . assetid ) ; verboseLog ( `creating directory: ${ color ( options . dir ) }` , options . verbose ) ; fs . existsSync ( options . dir ) && throwError ( `the directory ${ color ( options . dir ) } already exists` ) ; } const generateRandom = ( ( ) => { const variables = [ ] ; return ( timestamp , type , variableName ) => { ! variables . includes ( variableName ) && variables . push ( variableName ) ; let result ; const factor = variables . indexOf ( variableName ) + 1 ; switch ( type ) { case AssetManagementModels . VariableDefinition . DataTypeEnum . DOUBLE : result = ( Math . sin ( timestamp . getTime ( ) ) * factor * 10 ) . toFixed ( 2 ) + 20 ; break ; case AssetManagementModels . VariableDefinition . DataTypeEnum . INT : case AssetManagementModels . VariableDefinition . DataTypeEnum . LONG : result = Math . floor ( Math . sin ( timestamp . getTime ( ) ) * factor * 20 ) + 40 ; break ; case AssetManagementModels . VariableDefinition . DataTypeEnum . BOOLEAN : result = true ; break ; case AssetManagementModels . VariableDefinition . DataTypeEnum . STRING : case AssetManagementModels . VariableDefinition . DataTypeEnum . BIGSTRING : result = `${ type }_${ Math . random ( ) }` ; break ; default : throw new Error ( `invalid type ${ type }` ) ; } return result ; } ; } ) ( ) ; async function generateCsv ( { name , variables , options , path , mode , } , spinner ? ) { verboseLog ( `Generating ${ options . size } entries for ${ name }` , options . verbose , spinner ) ; verboseLog ( `Asset TwinType: ${ mode }` , options . verbose , spinner ) ; const startDate = new Date ( ) ; startDate . setUTCDate ( startDate . getUTCDate ( ) - parseInt ( options . offset ) ) ; for ( let file = options . files ; file > 0 ; file -- ) { const date = new Date ( startDate ) ; if ( mode === AssetManagementModels . TwinType . Performance ) { date . setUTCDate ( date . getUTCDate ( ) - ( file - 1 ) ) ; date . setUTCHours ( 0 , 0 , 0 , 0 ) ; } else { date . setUTCHours ( file - 1 , 0 , 0 , 0 ) ; } const fileName = `${ path }/csv/${ name }/${ file }.csv` ; verboseLog ( `generating: ${ color ( fileName ) }` , options . verbose , spinner ) ; const stream = fs . createWriteStream ( fileName , { highWaterMark : 12 * 16384 , } as any ) ; let headers = `_time, ` ; variables . forEach ( ( variable ) => { headers += variable . name + ", " ; if ( variable . qualityCode ) headers += variable . name + "_qc, " ; } ) ; stream . write ( headers . trimRight ( ) . slice ( 0 , - 1 ) + "\n" ) ; variables . forEach ( ( variable ) => { headers += variable . name + ", " ; if ( variable . qualityCode ) headers += variable . name + "_qc, " ; } ) ; for ( let index = options . size ; index > 0 ; index -- ) { const currentDate = mode === AssetManagementModels . TwinType . Performance ? subtractSecond ( date , ( 86400 / options . size ) * index ) : subtractSecond ( date , ( 3600 / options . size ) * index ) ; let line = currentDate . toISOString ( ) + ", " ; variables . forEach ( ( variable ) => { line += generateRandom ( currentDate , variable . dataType ! , `${ variable . name }` ) + ", " ; if ( variable . qualityCode ) line += "0, " ; } ) ; const result = stream . write ( line . trimRight ( ) . slice ( 0 , - 1 ) + "\n" ) ; if ( ! result ) { await new Promise ( ( resolve ) => stream . once ( "drain" , resolve ) ) ; } } stream . end ( ) ; } } function writeNewAssetJson ( options , root , path , twintype ) { const asset = { name : `${ twintype }Asset` , twinType : twintype === "performance" ? AssetManagementModels . TwinType . Performance : AssetManagementModels . TwinType . Simulation , typeId : options . typeid , parentId : root . assetId , location : { country : "Germany" , postalCode : "91052" , region : "Bayern" , streetAddress : "Schuhstr. 60" , locality : "Erlangen" , latitude : 49.59099 , longitude : 11.00783 , } , } ; const newAssetJson = `${ path }/asset.json` ; verboseLog ( `Writing ${ color ( newAssetJson ) }` , options . verbose ) ; fs . writeFileSync ( `${ path }/asset.json` , JSON . stringify ( asset , null , 2 ) ) ; } function createAspectDirs ( path , element , options ) { const csvDir = `${ path }/csv/${ element . name }` ; verboseLog ( `creating directory: ${ color ( csvDir ) }` , options . verbose ) ; fs . mkdirSync ( csvDir ) ; const jsonDir = `${ path }/json/${ element . name }` ; verboseLog ( `creating directory: ${ color ( jsonDir ) }` , options . verbose ) ; fs . mkdirSync ( jsonDir ) ; } function makeCsvAndJsonDir ( options ) { const path = options . dir ; fs . mkdirSync ( path ) ; fs . mkdirSync ( `${ path }/csv/` ) ; fs . mkdirSync ( `${ path }/json/` ) ; return path ; } function checkTwinTypeOfAsset ( asset , options ) { asset && asset . twinType !== options . twintype && throwError ( `You can't use the twintype mode ${ color ( options . twintype ) } for ${ asset . twinType } asset` ) ; }