{
  "instance_id": "2ef472d0-275ff01b",
  "hostname": "ip-10-92-53-53.ec2.internal",
  "dis": "Cornell",
  "override_count": 113,
  "overrides": [
    {
      "name": "action_createCalculatedPoint",
      "pod": "kwLinkModelExt",
      "pod_src": "//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   DD MMM 22   Dylan Bardin          - Creation.\n//   05 Jul 24   Thomas Kuhrke Limia   - Refactorization and code documentation.\n//\n// Description\n//   This action created a calculated point from the selected model.\n//\n// Parameters\n//   Name                 Type(s)                                                               DefVal\n//   1. sel               Dict\n//\n// Returns\n//   Type           Description\n//   1. None        - N/A\n//\n(sel) => do\n  // Check if Docker is Running\n  try func_DockerTest() catch throw \"Point cannot be created, docker is not running.\"\n\n  // Normalize Inputs\n  modelRec: actionNormInput(sel, \"id\")\n  pointRec: readById(modelRec->pointRef)\n\n  // Commit a market to the modelRec\n  commit(diff(readById(modelRec), {\"hasCalculatedPoint\" : marker()}))\n\n  // Update the hisRollupFun to get average\n  diff(pointRec, {\"hisRollupFunc\" : \"avg\"}, {update}).commit\n\n  // Retrieve specific data from modelRec\n  modelType: readById(modelRec)->modelName\n  modelTime: readById(modelRec)->modelTimespan\n  modelInt:  readById(modelRec)->dataInterval\n  modelGenMethod: readById(modelRec)->modelType\n  modelName:\"\"\n  if(modelInt == 1day) modelName= \"Daily\" else if (modelInt == 1hr) modelName= \"Hourly\" else if (modelInt == 30day) modelName= \"Monthly\"\n\n  // Get data using the point rec.\n  modelTz: pointRec->tz\n  modelSite: pointRec->siteRef\n  modelUnit: pointRec->unit\n  pointName: pointRec->navName\n  pointEquipRef: pointRec->equipRef\n  pointDefs: defs().findAll(r => r.has(\"lib\") and r.get(\"is\").toStr.contains(\"marker\") and r.get(\"lib\").toStr.contains(\"lib:ph\")).toGrid.colToList(\"def\")\n  pointTags: pointRec.names\n  intersectDict: {}\n  pointDefs.each r => do\n    tag: r.toStr\n    if(pointTags.contains(tag)) do\n      intersectDict = intersectDict.set(tag, marker())\n    end\n  end\n\n  // Throw if the calculated point already exists\n  if(readAll(point and modelRef == modelRec).size>0) throw \"Calculated point already exists for this model\"\n\n  // Create New Calculated Point\n  nmecCreatePoint: {\"modelRef\"           : modelRec,\n                    \"modelType\"          : modelType,\n                    \"unit\"               : modelUnit,\n                    \"basePointRef\"       : pointRec->id,\n                    \"modelHisFunction\"   : marker(),\n                    \"modelGenMethod\"     : modelGenMethod,\n                    \"modelPoint\"         : marker(),\n                    \"point\"              : marker(),\n                    \"navName\"            : pointRec->navName + \" (\" + modelName + \" Prediction)\",\n                    \"equipRef\"           : pointEquipRef,\n                    \"modeledDataSpan\"    : modelTime,\n                    \"disMacro\"           : \"\\$equipRef \\$navName\",\n                    \"his\"                : marker(),\n                    \"tz\"                 : modelTz,\n                    \"siteRef\"            : modelSite,\n                    \"kind\"               : \"Number\",\n                    \"calculated\"         : marker(),\n                    \"hisRollupInterval\"  : modelInt,\n                    \"hisRollupFunc\"      : \"avg\",\n                    \"modelPoint\"         : marker(),\n                    \"calculated\"         : marker(),\n                    \"strokeDasharray\"    : \"2,2\"\n                   }\n  // If the intersectDict has something in it, merge with the nmecCreatePoint and add\n  // the rec.\n  if(not intersectDict.isEmpty) nmecCreatePoint = nmecCreatePoint.merge(intersectDict)\n  record: diff(null, nmecCreatePoint, {add})\n  commit(record)\n\n  // Retrieve the modelRec model data, and format it.\n  data: readById(modelRec)->modelData.toGrid\n  data = data.keepCols([\"time\", \"y_fit\"])\n             .renameCol(\"time\", \"ts\")\n             .renameCol(\"y_fit\", \"v0\")\n             .map(j => return {\"ts\": j.get(\"ts\").toTimeZone(modelTz.toStr), \"v0\": j.get(\"v0\")})\n\n  // Write formatted data into the modelHisFunction rec.\n  rex: read(modelRef == modelRec and modelHisFunction)\n  data.hisWrite(rex)\n\n  // If there are no tasks, create one, otherwise don't do anything in this step.\n  tasks: readAll(modelTask).size\n  if(tasks == 0) do\n    modelTask: {\"dis\"                : \"Model Sync\",\n                \"modelTask\"          : marker(),\n                \"task\"               : marker(),\n                \"taskExpr\"           : \"func_syncCalculatedPoints\",\n                \"obsSchedule\"        : marker(),\n                \"obsScheduleFreq\"    : 1day}\n    commit(diff(null, modelTask, {add}))\n  end\nend",
      "local_src": "//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   DD MMM 22   Dylan Bardin          - Creation.\n//   05 Jul 24   Thomas Kuhrke Limia   - Refactorization and code documentation.\n//\n// Description\n//   This action created a calculated point from the selected model.\n//\n// Parameters\n//   Name                 Type(s)                                                               DefVal\n//   1. sel               Dict\n//\n// Returns\n//   Type           Description\n//   1. None        - N/A\n//\n(sel) => do\n  // Check if Docker is Running\n  try func_DockerTest() catch throw \"Point cannot be created, docker is not running.\"\n\n  // Normalize Inputs\n  modelRec: actionNormInput(sel, \"id\")\n  pointRec: readById(modelRec->pointRef)\n\n  // Commit a market to the modelRec\n  commit(diff(readById(modelRec), {\"hasCalculatedPoint\" : marker()}))\n\n  // Update the hisRollupFun to get average\n  diff(pointRec, {\"hisRollupFunc\" : \"avg\"}, {update}).commit\n\n  // Retrieve specific data from modelRec\n  modelType: readById(modelRec)->modelName\n  modelTime: readById(modelRec)->modelTimespan\n  modelInt:  readById(modelRec)->dataInterval\n  modelGenMethod: readById(modelRec)->modelType\n  modelName:\"\"\n  if(modelInt == 1day) modelName= \"Daily\" else if (modelInt == 1hr) modelName= \"Hourly\" else if (modelInt == 30day) modelName= \"Monthly\"\n\n  // Get data using the point rec.\n  modelTz: pointRec->tz\n  modelSite: pointRec->siteRef\n  modelUnit: pointRec->unit\n  pointName: pointRec->navName\n  pointEquipRef: pointRec->equipRef\n  pointDefs: defs().findAll(r => r.has(\"lib\") and r.get(\"is\").toStr.contains(\"marker\") and r.get(\"lib\").toStr.contains(\"lib:ph\")).toGrid.colToList(\"def\")\n  pointTags: pointRec.names\n  intersectDict: {}\n  pointDefs.each r => do\n    tag: r.toStr\n    if(pointTags.contains(tag)) do\n      intersectDict = intersectDict//.set(tag, marker()) NOTE: This was disabled to prevent automatic tagging of point\n    end\n  end\n\n  // Throw if the calculated point already exists\n  if(readAll(point and modelRef == modelRec).size>0) throw \"Calculated point already exists for this model\"\n\n  // Create New Calculated Point\n  nmecCreatePoint: {\"modelRef\"           : modelRec,\n                    \"modelType\"          : modelType,\n                    \"unit\"               : modelUnit,\n                    \"basePointRef\"       : pointRec->id,\n                    \"modelHisFunction\"   : marker(),\n                    \"modelGenMethod\"     : modelGenMethod,\n                    \"modelPoint\"         : marker(),\n                    \"point\"              : marker(),\n                    \"navName\"            : pointRec->navName + \" (\" + modelName + \" Prediction)\",\n                    \"equipRef\"           : pointEquipRef,\n                    \"modeledDataSpan\"    : modelTime,\n                    \"disMacro\"           : \"\\$equipRef \\$navName\",\n                    \"his\"                : marker(),\n                    \"tz\"                 : modelTz,\n                    \"siteRef\"            : modelSite,\n                    \"kind\"               : \"Number\",\n                    \"calculated\"         : marker(),\n                    \"hisRollupInterval\"  : modelInt,\n                    \"hisRollupFunc\"      : \"avg\",\n                    \"modelPoint\"         : marker(),\n                    \"calculated\"         : marker(),\n                    \"strokeDasharray\"    : \"2,2\"\n                   }\n  // If the intersectDict has something in it, merge with the nmecCreatePoint and add\n  // the rec.\n  if(not intersectDict.isEmpty) nmecCreatePoint = nmecCreatePoint.merge(intersectDict)\n  record: diff(null, nmecCreatePoint, {add})\n  commit(record)\n\n  // Retrieve the modelRec model data, and format it.\n  data: readById(modelRec)->modelData.toGrid\n  data = data.keepCols([\"time\", \"y_fit\"])\n             .renameCol(\"time\", \"ts\")\n             .renameCol(\"y_fit\", \"v0\")\n             .map(j => return {\"ts\": j.get(\"ts\").toTimeZone(modelTz.toStr), \"v0\": j.get(\"v0\")})\n\n  // Write formatted data into the modelHisFunction rec.\n  rex: read(modelRef == modelRec and modelHisFunction)\n  data.hisWrite(rex)\n\n  // If there are no tasks, create one, otherwise don't do anything in this step.\n  tasks: readAll(modelTask).size\n  if(tasks == 0) do\n    modelTask: {\"dis\"                : \"Model Sync\",\n                \"modelTask\"          : marker(),\n                \"task\"               : marker(),\n                \"taskExpr\"           : \"func_syncCalculatedPoints\",\n                \"obsSchedule\"        : marker(),\n                \"obsScheduleFreq\"    : 1day}\n    commit(diff(null, modelTask, {add}))\n  end\nend",
      "diff": "@@ -52,7 +52,7 @@   pointDefs.each r => do\n     tag: r.toStr\n     if(pointTags.contains(tag)) do\n-      intersectDict = intersectDict.set(tag, marker())\n+      intersectDict = intersectDict//.set(tag, marker()) NOTE: This was disabled to prevent automatic tagging of point\n     end\n   end\n \n"
    },
    {
      "name": "action_kwModeling_myModels_activateModel",
      "pod": "kwLinkModelExt",
      "pod_src": "//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   16 Sep 24   Apoorv Khanuja        - Creation.\n//\n// Description\n//   This action script reads in the modelConfigRef, updates the modelUseState tag to Active, creates a modelPoint rec if one doesn't exist, and calls the virtual func to sync modelPoint's hisData\n//\n// Parameters\n//   Name               Type(s)             DefVal\n//   sel                Ref                 NA\n//\n// Returns\n//   Type           Description\n//   str            - Returns a message\n//\n\n(sel) => do\n\n  // Normalize and retrieve the modelConfig rec.\n  modelConfigRef: actionNormInput(sel, \"id\")\n  modelConfigRec: readById(modelConfigRef)\n\n  // Check if modelConfig is Activated AND contains modelPoint  (AKA syntheticPoint) ///////// TODD: Include if it has modelPointStatus and is. filter\n  if (modelConfigRec.get(\"modelUseState\")==\"active\" and modelConfigRec.condition_modelPointRecExists and modelConfigRec.has(\"modelPointStatus\") and modelConfigRec.get(\"modelPointStatus\")==\"ok\") throw \"Model already Activated and Model Point Created\"\n  if (modelConfigRec.get(\"modelUseState\")==\"active\" and modelConfigRec.condition_modelPointRecExists and modelConfigRec.has(\"modelPointStatus\") and modelConfigRec.get(\"modelPointStatus\")==\"running\") throw \"Model Point Created. Getting predictions for the past year. Check Logs.\"\n\n  // [Create and] Extract modelPoint ref\n  if (not modelConfigRec.condition_modelPointRecExists) modelPointRec: modelConfigRec.logic_kwModeling_createModelPointRec_v2\n\n  // reread modelConfig\n  modelConfigRec = readById(modelConfigRef)\n  modelPointRef: modelConfigRec.get(\"modelPointRef\")\n\n  // Figure out activation span based on model engine.\n  startDate: if ((modelConfigRec.get(\"pointRef\").toRec.get(\"hisEnd\") - 52wk) <  modelConfigRec.get(\"pointRef\").toRec.get(\"hisStart\")) modelConfigRec.get(\"pointRef\").toRec.get(\"hisStart\")\n             else                                                                                                                     modelConfigRec.get(\"pointRef\").toRec.get(\"hisEnd\") - 52wk\n  dates: if (modelConfigRec.get(\"modelEngine\") == \"nmecpy\") (modelConfigRec.get(\"modelParameters\").get(\"trainDates\").start..modelConfigRec.get(\"modelParameters\").get(\"testDates\").end).toSpan\n         else                                               (startDate..(modelConfigRec.get(\"pointRef\").toRec.get(\"hisEnd\")))\n\n  // Populate the modelPoint with data\n  callTask(\"virtualSyncHis\", [modelPointRef, dates], getVal: true)\n\n  // Update modelUseState\n  modelConfigRec = diff(modelConfigRec, {\"modelUseState\": \"active\"}, {update}).commit\n  modelConfigRec = modelConfigRef.toRec\n  modelConfigRec = diff(modelConfigRec, {\"modelPointStatus\":\"running\"}, {transient}).commit\n\n  return \"Model Activated and Model Point Created. Getting predictions for the past year. Check Logs.\"\n\nend",
      "local_src": "//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   16 Sep 24   Apoorv Khanuja        - Creation.\n//\n// Description\n//   This action script reads in the modelConfigRef, updates the modelUseState tag to Active, creates a modelPoint rec if one doesn't exist, and calls the virtual func to sync modelPoint's hisData\n//\n// Parameters\n//   Name               Type(s)             DefVal\n//   sel                Ref                 NA\n//\n// Returns\n//   Type           Description\n//   str            - Returns a message\n//\n\n(sel) => do\n\n  // Normalize and retrieve the modelConfig rec.\n  modelConfigRef: actionNormInput(sel, \"id\")\n  modelConfigRec: readById(modelConfigRef)\n\n  // Check if modelConfig is Activated AND contains modelPoint  (AKA syntheticPoint) ///////// TODD: Include if it has modelPointStatus and is. filter\n  if (modelConfigRec.get(\"modelUseState\")==\"active\" and modelConfigRec.condition_modelPointRecExists and modelConfigRec.has(\"modelPointStatus\") and modelConfigRec.get(\"modelPointStatus\")==\"ok\") throw \"Model already Activated and Model Point Created\"\n  if (modelConfigRec.get(\"modelUseState\")==\"active\" and modelConfigRec.condition_modelPointRecExists and modelConfigRec.has(\"modelPointStatus\") and modelConfigRec.get(\"modelPointStatus\")==\"running\") throw \"Model Point Created. Getting predictions for the past year. Check Logs.\"\n\n  // [Create and] Extract modelPoint ref\n  if (not modelConfigRec.condition_modelPointRecExists) modelPointRec: modelConfigRec.logic_kwModeling_createModelPointRec_v2\n\n  // reread modelConfig\n  modelConfigRec = readById(modelConfigRef)\n  modelPointRef: modelConfigRec.get(\"modelPointRef\")\n  \n  // Figure out activation span based on model engine.\n  dates: if (modelConfigRec.get(\"modelEngine\") == \"nmecpy\") (modelConfigRec.get(\"modelParameters\").get(\"trainDates\").start..modelConfigRec.get(\"modelParameters\").get(\"testDates\").end).toSpan\n         else                                               (2025-01-01..(modelConfigRec.get(\"pointRef\").toRec.get(\"hisEnd\"))) // 2025-01-01 for campus level points 03-01 for others\n  \n  // Populate the modelPoint with data\n  callTask(\"virtualSyncHis\", [modelPointRef, dates], getVal: true)\n\n  // Update modelUseState\n  modelConfigRec = diff(modelConfigRec, {\"modelUseState\": \"active\"}, {update}).commit\n  modelConfigRec = modelConfigRef.toRec\n  modelConfigRec = diff(modelConfigRec, {\"modelPointStatus\":\"running\"}, {transient}).commit\n\n  return \"Model Activated and Model Point Created. Getting predictions for the past year. Check Logs.\"\n\nend",
      "diff": "@@ -36,10 +36,8 @@   modelPointRef: modelConfigRec.get(\"modelPointRef\")\n \n   // Figure out activation span based on model engine.\n-  startDate: if ((modelConfigRec.get(\"pointRef\").toRec.get(\"hisEnd\") - 52wk) <  modelConfigRec.get(\"pointRef\").toRec.get(\"hisStart\")) modelConfigRec.get(\"pointRef\").toRec.get(\"hisStart\")\n-             else                                                                                                                     modelConfigRec.get(\"pointRef\").toRec.get(\"hisEnd\") - 52wk\n   dates: if (modelConfigRec.get(\"modelEngine\") == \"nmecpy\") (modelConfigRec.get(\"modelParameters\").get(\"trainDates\").start..modelConfigRec.get(\"modelParameters\").get(\"testDates\").end).toSpan\n-         else                                               (startDate..(modelConfigRec.get(\"pointRef\").toRec.get(\"hisEnd\")))\n+         else                                               (2025-01-01..(modelConfigRec.get(\"pointRef\").toRec.get(\"hisEnd\"))) // 2025-01-01 for campus level points 03-01 for others\n \n   // Populate the modelPoint with data\n   callTask(\"virtualSyncHis\", [modelPointRef, dates], getVal: true)\n"
    },
    {
      "name": "action_virtualPoints_createSiteMeterPts",
      "pod": "kwLinkVirtualExt",
      "pod_src": "(dicts) => do\n\n  //normalize inputs to grid\n  //dict contains ONLY ID, mod, and CHANGES made to form\n  input: actionNormInput(dicts,\"dicts\").toGrid\n\n  //match input dict up with blueprint list\n  blueprints: wdg_virtualPoints_table_blueprintList()\n\n  //loop through results, convert\n  results: input.map blueprint => do\n\n    //get blueprint\n    bp: blueprints.find(v=>v->id==blueprint->id)\n\n    //convert to point\n    dict: try virtualBlueprintToPoint(bp.merge(blueprint))\n          catch (err) blueprint.merge({result:\"failure\", err:err})\n\n    //return result\n    out: if (dict.missing(\"err\")) diff(null, dict, {add}).commit.merge({result:\"succes\"})\n         else                     dict\n  end\n  recs: readByIds(results.colToList(\"id\"))\n\n  recs.each(r => autoMapVirtualDependencies(r, true))\n\n  recs.virtualSyncHis(2024-01-01..today())\n\nend",
      "local_src": "(list) => do\n  //normalize inputs to grid\n  //dict contains ONLY ID, mod, and CHANGES made to form\n  //input: actionNormInput(dicts,\"dicts\").toGrid\n  input: []\n  list.each(r => input=input.add(r.toStr.split(\"|\")[2].parseRef))\n  input = readByIds(input)\n  equip: list[0].toStr.split(\"|\")[1].parseRef\n  site: list[0].toStr.split(\"|\")[0].parseRef\n  //match input dict up with blueprint list\n  blueprints: wdg_virtualPoints_table_blueprintList()\n\n  //loop through results, convert\n  results: input.map blueprint => do\n\n    //get blueprint\n    bp: blueprints.find(v=>v->id==blueprint->id)\n\n    //convert to point\n    dict: try virtualBlueprintToPoint(bp.merge(blueprint))\n          catch (err) blueprint.merge({result:\"failure\", err:err})\n\n    //return result\n    out: if (dict.missing(\"err\")) diff(null, dict.set(\"equipRef\", equip).set(\"siteRef\", site), {add}).commit.merge({result:\"succes\"})\n         else                     dict\n  end\n  recs: readByIds(results.colToList(\"id\"))\n  \n  recs.each(r => autoMapVirtualDependencies(r, true))\n  \n  recs.virtualSyncHis(2024-01-01..today())\n  \nend",
      "diff": "@@ -1,9 +1,12 @@-(dicts) => do\n-\n+(list) => do\n   //normalize inputs to grid\n   //dict contains ONLY ID, mod, and CHANGES made to form\n-  input: actionNormInput(dicts,\"dicts\").toGrid\n-\n+  //input: actionNormInput(dicts,\"dicts\").toGrid\n+  input: []\n+  list.each(r => input=input.add(r.toStr.split(\"|\")[2].parseRef))\n+  input = readByIds(input)\n+  equip: list[0].toStr.split(\"|\")[1].parseRef\n+  site: list[0].toStr.split(\"|\")[0].parseRef\n   //match input dict up with blueprint list\n   blueprints: wdg_virtualPoints_table_blueprintList()\n \n@@ -18,7 +21,7 @@           catch (err) blueprint.merge({result:\"failure\", err:err})\n \n     //return result\n-    out: if (dict.missing(\"err\")) diff(null, dict, {add}).commit.merge({result:\"succes\"})\n+    out: if (dict.missing(\"err\")) diff(null, dict.set(\"equipRef\", equip).set(\"siteRef\", site), {add}).commit.merge({result:\"succes\"})\n          else                     dict\n   end\n   recs: readByIds(results.colToList(\"id\"))\n"
    },
    {
      "name": "buildingLoopDigest",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     This function is for finding the appropriate points assosciated with a chws and it's correct building loop\n*\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Grid                   target          - The system input data.\n*\n*   Work :\n*     Who                    When        Change\n*     William Ludwig         7/30/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/7/2024   - Added loop override support.\n*/\n(target, opts: {}) => do\n  targetRec: target.toRec                                                                                               //\n  targetId: targetRec->id\n  mode: if (opts.has(\"id\")) \"idMode\" else \"normal\"\n\n  nestedEquips: target.nestedEquips\n\n  primaryId:   try nestedEquips.find(r => r.has(\"primaryLoop\") and r.has(\"chilled\")).get(\"id\") catch null\n  secondaryId: try nestedEquips.find(r => r.has(\"secondaryLoop\") and r.has(\"chilled\")).get(\"id\") catch null\n  condenserId: try nestedEquips.find(r => r.has(\"plantLoop\") and r.has(\"condenser\")).get(\"id\") catch null\n\n  // Override Opts Handling\n  if (opts.get(\"primaryLoopOverride\").isRef)   primaryId   = opts.get(\"primaryLoopOverride\")\n  if (opts.get(\"secondaryLoopOverride\").isRef) secondaryId = opts.get(\"secondaryLoopOverride\")\n  if (opts.get(\"condenserLoopOverride\").isRef) condenserId = opts.get(\"condenserLoopOverride\")\n\n  if(mode == \"idMode\") do\n    if(secondaryId.isNonNull) return secondaryId else return primaryId\n  end\n\n  if(secondaryId.isNonNull) do\n    secondaryLoop: try secondaryLoopDigest(secondaryId).toGrid catch(err) err// \"No Secondary Loop\"\n    result : {buildingLoop:secondaryLoop}\n  else do\n    primaryLoop: try primaryLoopDigest(primaryId).toGrid catch \"No Primary Loop\"\n    result : {buildingLoop: primaryLoop}\n  end\n\n  //Create final Result\n\n  return result\n\nend",
      "local_src": "/*\n*   Description :\n*     This function is for finding the appropriate points assosciated with a chws and it's correct building loop\n*\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Grid                   target          - The system input data.\n*\n*   Work :\n*     Who                    When        Change\n*     William Ludwig         7/30/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/7/2024   - Added loop override support.\n*/\n(target, opts: {}) => do\n  targetRec: target.toRec                                                                                               //\n  targetId: targetRec->id\n  mode: if (opts.has(\"id\")) \"idMode\" else \"normal\"\n\n  nestedEquips: target.nestedEquips\n\n  primaryId:   try nestedEquips.find(r => r.has(\"primaryLoop\") and r.has(\"chilled\")).get(\"id\") catch null\n  secondaryId: try nestedEquips.find(r => r.has(\"secondaryLoop\") and r.has(\"chilled\")).get(\"id\") catch null\n  condenserId: try nestedEquips.find(r => r.has(\"plantLoop\") and r.has(\"condenser\")).get(\"id\") catch null\n\n  // Override Opts Handling\n  if (opts.get(\"primaryLoopOverride\").isRef)     primaryId   = opts.get(\"primaryLoopOverride\")\n  if (opts.get(\"primaryLoopOverride\") == \"null\")   primaryId   = null\n  if (opts.get(\"secondaryLoopOverride\").isRef)   secondaryId = opts.get(\"secondaryLoopOverride\")\n  if (opts.get(\"secondaryLoopOverride\") == \"null\") secondaryId = null\n  if (opts.get(\"condenserLoopOverride\").isRef)   condenserId = opts.get(\"condenserLoopOverride\")\n\n  if(mode == \"idMode\") do\n    if(secondaryId.isNonNull) return secondaryId else return primaryId\n  end\n\n  if(secondaryId.isNonNull) do\n    secondaryLoop: try secondaryLoopDigest(secondaryId).toGrid catch(err) err// \"No Secondary Loop\"\n    result : {buildingLoop:secondaryLoop}\n  else do\n    primaryLoop: try primaryLoopDigest(primaryId).toGrid catch \"No Primary Loop\"\n    result : {buildingLoop: primaryLoop}\n  end\n\n  //Create final Result\n\n  return result\n\nend",
      "diff": "@@ -24,9 +24,11 @@   condenserId: try nestedEquips.find(r => r.has(\"plantLoop\") and r.has(\"condenser\")).get(\"id\") catch null\n \n   // Override Opts Handling\n-  if (opts.get(\"primaryLoopOverride\").isRef)   primaryId   = opts.get(\"primaryLoopOverride\")\n-  if (opts.get(\"secondaryLoopOverride\").isRef) secondaryId = opts.get(\"secondaryLoopOverride\")\n-  if (opts.get(\"condenserLoopOverride\").isRef) condenserId = opts.get(\"condenserLoopOverride\")\n+  if (opts.get(\"primaryLoopOverride\").isRef)     primaryId   = opts.get(\"primaryLoopOverride\")\n+  if (opts.get(\"primaryLoopOverride\") == \"null\")   primaryId   = null\n+  if (opts.get(\"secondaryLoopOverride\").isRef)   secondaryId = opts.get(\"secondaryLoopOverride\")\n+  if (opts.get(\"secondaryLoopOverride\") == \"null\") secondaryId = null\n+  if (opts.get(\"condenserLoopOverride\").isRef)   condenserId = opts.get(\"condenserLoopOverride\")\n \n   if(mode == \"idMode\") do\n     if(secondaryId.isNonNull) return secondaryId else return primaryId\n"
    },
    {
      "name": "cacheGrid",
      "pod": "kwLinkCoreExt",
      "pod_src": "(records, specialTagName, dates) => do\n  cipherText :\n    \"AxD:T1BkkICIb4SOTMNJwbx-wA::PFhoj0MDm4_m4f9zzsJ0r_2M-Jv19NOtXHFa-dXnXemkHI9vgIdn\" +\n    \"KI3TBsEUEto9uneAdwPxA5TYYbg5op3Exs2g0mcAtC2nSFl-g-ZIX6N0H9xJLgsiF5MhjetDhkor5QdG\" +\n    \"qIzzrBZcQ_lZiZ-NLEvj6038-R0XV8tZFbPv3iIDcAxPX5Zfq-j82GnABIdiUd3v-tMDlLql3-TDf622\" +\n    \"0wrebG9FJDy7a7hZCwuK26r0UMve0W54qs_ADpr0GLK4EEEd5Ld0KRhYtlsLaWn11LXNx6M9qwLx_FLl\" +\n    \"JS7Av6E-2QoTCmFzPRiWhpu5m8m01-tpPaDyB--paC2NzvlVfP0NUqqjn0vKyoGsrBeoonpcqhFqYs5x\" +\n    \"B5bEUL1PWuS4xJLeUNSf_eCymbRzE1zwIJkrk9Cm_Yku3X8gQVbGarqBz7LD92WbxPxC2kaY0EL7fzZI\" +\n    \"MiPj7rmj9ep3y9sEqUoyfjYK7w3uQrG4Cd5AngMwprhL_lwYLwcTSA5Ci1f9DSIg92LU8w2wK6AyZogB\" +\n    \"tihcDWo3j7W2T8BS3ptKSZ80UGlcXWNAacH16FbjHC4mEvJ06wFIwwtnnrEhzdmlHpE8LDe0RUbheqo0\" +\n    \"oLoIiBWsKAO_ctuuEyN729NuwPpRS4tENIYqFWtkTD79eJIxnHSUo6N8paPpN1nCJ1PXK5_gBBth4xTx\" +\n    \"iBXgPJYoB9qutd5LxHIAnHIfyHO2IsjpbkX7VgYmdj3P_U2zije45u4\"\n  keyFunc : () => \"ddJNIRm2qio2Z7CXpKADiw\"\n  return afAxonEncryptorRt_callFunc(\"cacheGrid\", cipherText, [records, specialTagName, dates], keyFunc)\nend",
      "local_src": "(records, specialTagName, dates) => do\n  if(not dates.isSpan) dates = try dates.toSpan catch \"all\"\n  dateYear: try dates.start.year() catch \"all\"\n  if(records.isNull) return \"First parameter needs to be a grid\"\n  if(records.isEmpty) return \"First parameter needs to be a grid\"\n  if(not specialTagName.isStr) return \"Second parameter needs to be a string\"\n  //if(dateYear == \"all\") filter: \"cache and \" + specialTagName + \" and cacheYear==everything\"\n  filter: \"cache and \" + specialTagName \n  previousCachedRecs: readAll(parseFilter(filter)).findAll(r => r.get(\"cacheYear\") == dateYear)\n  if(previousCachedRecs.size > 0) do\n    //recDelete(previousCachedRecs)\n    previousCachedRecs.each() p => do\n      diff(p, {trash}, {remove}).commit\n    end\n  end\n  //Write to the cache\n  //if(specialTagName.contains(\"portfolio\")) records = records.renameCol(\"site\", \"siteR\")\n  records.each() r => do\n    newTags: {cache:marker(), cacheYear:dateYear}.set(specialTagName.toStr, marker())\n    r = r.merge(newTags)\n    diff(null, r, {add}).commit()\n  end\nend",
      "diff": "@@ -1,15 +1,23 @@ (records, specialTagName, dates) => do\n-  cipherText :\n-    \"AxD:T1BkkICIb4SOTMNJwbx-wA::PFhoj0MDm4_m4f9zzsJ0r_2M-Jv19NOtXHFa-dXnXemkHI9vgIdn\" +\n-    \"KI3TBsEUEto9uneAdwPxA5TYYbg5op3Exs2g0mcAtC2nSFl-g-ZIX6N0H9xJLgsiF5MhjetDhkor5QdG\" +\n-    \"qIzzrBZcQ_lZiZ-NLEvj6038-R0XV8tZFbPv3iIDcAxPX5Zfq-j82GnABIdiUd3v-tMDlLql3-TDf622\" +\n-    \"0wrebG9FJDy7a7hZCwuK26r0UMve0W54qs_ADpr0GLK4EEEd5Ld0KRhYtlsLaWn11LXNx6M9qwLx_FLl\" +\n-    \"JS7Av6E-2QoTCmFzPRiWhpu5m8m01-tpPaDyB--paC2NzvlVfP0NUqqjn0vKyoGsrBeoonpcqhFqYs5x\" +\n-    \"B5bEUL1PWuS4xJLeUNSf_eCymbRzE1zwIJkrk9Cm_Yku3X8gQVbGarqBz7LD92WbxPxC2kaY0EL7fzZI\" +\n-    \"MiPj7rmj9ep3y9sEqUoyfjYK7w3uQrG4Cd5AngMwprhL_lwYLwcTSA5Ci1f9DSIg92LU8w2wK6AyZogB\" +\n-    \"tihcDWo3j7W2T8BS3ptKSZ80UGlcXWNAacH16FbjHC4mEvJ06wFIwwtnnrEhzdmlHpE8LDe0RUbheqo0\" +\n-    \"oLoIiBWsKAO_ctuuEyN729NuwPpRS4tENIYqFWtkTD79eJIxnHSUo6N8paPpN1nCJ1PXK5_gBBth4xTx\" +\n-    \"iBXgPJYoB9qutd5LxHIAnHIfyHO2IsjpbkX7VgYmdj3P_U2zije45u4\"\n-  keyFunc : () => \"ddJNIRm2qio2Z7CXpKADiw\"\n-  return afAxonEncryptorRt_callFunc(\"cacheGrid\", cipherText, [records, specialTagName, dates], keyFunc)\n+  if(not dates.isSpan) dates = try dates.toSpan catch \"all\"\n+  dateYear: try dates.start.year() catch \"all\"\n+  if(records.isNull) return \"First parameter needs to be a grid\"\n+  if(records.isEmpty) return \"First parameter needs to be a grid\"\n+  if(not specialTagName.isStr) return \"Second parameter needs to be a string\"\n+  //if(dateYear == \"all\") filter: \"cache and \" + specialTagName + \" and cacheYear==everything\"\n+  filter: \"cache and \" + specialTagName\n+  previousCachedRecs: readAll(parseFilter(filter)).findAll(r => r.get(\"cacheYear\") == dateYear)\n+  if(previousCachedRecs.size > 0) do\n+    //recDelete(previousCachedRecs)\n+    previousCachedRecs.each() p => do\n+      diff(p, {trash}, {remove}).commit\n+    end\n+  end\n+  //Write to the cache\n+  //if(specialTagName.contains(\"portfolio\")) records = records.renameCol(\"site\", \"siteR\")\n+  records.each() r => do\n+    newTags: {cache:marker(), cacheYear:dateYear}.set(specialTagName.toStr, marker())\n+    r = r.merge(newTags)\n+    diff(null, r, {add}).commit()\n+  end\n end"
    },
    {
      "name": "calcTMRASavings",
      "pod": "kwLinkVirtualExt",
      "pod_src": "(siteId, savingsHis, modelPt,  meterPt, performancePeriodStartDate, performancePeriodEndDate, mildTempRangeLower: \"40\", mildTempRangeUpper: \"65\") => do\n  mildTempRange: (mildTempRangeLower.parseNumber.as(\"\u00b0F\")..mildTempRangeUpper.parseNumber.as(\"\u00b0F\"))\n  performancePeriod : (performancePeriodStartDate..performancePeriodEndDate).toDateSpan\n  siteRec: siteId.toRec\n  schedule: siteRec.get(\"scheduleRef\")\n  scheduleRec: schedule.toRec\n  tempPt: read(temp and weatherStationRef == siteRec.get(\"weatherStationRef\"))\n  baselinePeriod: modelPt.get(\"modeledDataSpan\")\n  modelBaselineHis: modelPt.hisRead(baselinePeriod, {-limit}).hisRollup(avg, 1hr)\n  meterBaselineHis: meterPt.hisRead(baselinePeriod, {-limit}).hisRollup(avg, 1hr)\n  tempBaselineHis: tempPt.hisRead(baselinePeriod, {-limit}).hisRollup(avg, 1hr)\n\n  modelPerformanceHis: modelPt.hisRead(performancePeriod, {-limit}).hisRollup(avg, 1hr)\n  meterPerformanceHis: meterPt.hisRead(performancePeriod, {-limit}).hisRollup(avg, 1hr)\n  tempPerformanceHis: tempPt.hisRead(performancePeriod, {-limit}).hisRollup(avg, 1hr)\n  baselineHis: hisJoin([modelBaselineHis, meterBaselineHis, tempBaselineHis])\n  baselineHis = baselineHis.map(r => if(mildTempRange.contains(r.get(\"v2\"))) return r else return r = null).removeCols([\"v2\"])\n  performanceHis: hisJoin([modelPerformanceHis, meterPerformanceHis, tempPerformanceHis])\n  performanceHis = performanceHis.map(r => if(mildTempRange.contains(r.get(\"v2\"))) return r else return r = null).removeCols([\"v2\"])\n\n  baselineResiduals: addColTwoPointDelta(baselineHis, \"delta\",  \"v1\", \"v0\").keepCols([\"ts\", \"delta\"]).renameCol(\"delta\", \"baselineResiduals\")\n  performanceResiduals: addColTwoPointDelta(performanceHis, \"delta\",  \"v1\", \"v0\").keepCols([\"ts\", \"delta\"]).renameCol(\"delta\", \"performanceResiduals\")\n  baselineResidualTrend: baselineResiduals.checkResidualTrend(tempBaselineHis)\n\n  performanceResidualTrend: performanceResiduals.checkResidualTrend(tempPerformanceHis)\n  if(baselineResidualTrend.get(\"r2\") > 0.2 or performanceResidualTrend.get(\"r2\") > 0.2) return {\"msg\": \"TMRA not applicable\",\n                                                                                                \"reason\": \"Residuals in mildBand have trend\",\n                                                                                                \"baselineR_two\":baselineResidualTrend.get(\"r2\"),\n                                                                                                \"performanceR_two\":performanceResidualTrend.get(\"r2\")}\n  onPeakBaselineResiduals: baselineResiduals.map(r => if(schedule.scheduleValAt(r.get(\"ts\"))==true) return r else return null)\n  offPeakBaselineResiduals: baselineResiduals.map(r => if(schedule.scheduleValAt(r.get(\"ts\")).isNull) return r else return null)\n  presentBaselineResiduals: hisJoin([onPeakBaselineResiduals.addColMeta(\"baselineResiduals\", {dis: \"On Peak Residuals\" + \", n= \" + onPeakBaselineResiduals.size}).renameCol(\"baselineResiduals\", \"onPeakResiduals\"),\n                                     offPeakBaselineResiduals.addColMeta(\"baselineResiduals\", {dis: \"Off Peak Residuals\" + \", n= \" + offPeakBaselineResiduals.size}).renameCol(\"baselineResiduals\", \"offPeakResiduals\")])\n  offPeak: 0\n  onPeak: 0\n  cusumBaselineResiduals: presentBaselineResiduals.map r => do\n    ts: r.get(\"ts\")\n    offPeak = if(r.get(\"offPeakResiduals\").isNull) offPeak else offPeak + r.get(\"offPeakResiduals\")\n    onPeak = if(r.get(\"onPeakResiduals\").isNull) onPeak else onPeak + r.get(\"onPeakResiduals\")\n    return {\"ts\": ts, \"offPeakCuSum\": offPeak.as(\"kWh\"), \"onPeakCuSum\": onPeak.as(\"kWh\")}\n  end\n  cusumBaselineResiduals = cusumBaselineResiduals.addColMeta(\"offPeakCuSum\", {\"chartGroup\": \"one\", \"unit\": \"kWh\"}).addColMeta(\"onPeakCuSum\", {\"chartGroup\": \"one\", \"unit\": \"kWh\"})\n\n\n  onPeakPerformanceResiduals: performanceResiduals.map(r => if(schedule.scheduleValAt(r.get(\"ts\"))==true) return r else return null)\n  offPeakPerformanceResiduals: performanceResiduals.map(r => if(schedule.scheduleValAt(r.get(\"ts\")).isNull) return r else return null)\n  presentPerformanceResiduals: hisJoin([onPeakPerformanceResiduals.addColMeta(\"performanceResiduals\", {dis: \"On Peak Residuals\" + \", n= \" + onPeakPerformanceResiduals.size}).renameCol(\"performanceResiduals\", \"onPeakResiduals\"),\n                                        offPeakPerformanceResiduals.addColMeta(\"performanceResiduals\", {dis: \"Off Peak Residuals\" + \", n= \" + offPeakPerformanceResiduals.size}).renameCol(\"performanceResiduals\", \"offPeakResiduals\")])\n  offPeak= 0\n  onPeak= 0\n  cusumPerformanceResiduals: presentPerformanceResiduals.map r => do\n    ts: r.get(\"ts\")\n    offPeak = if(r.get(\"offPeakResiduals\").isNull) offPeak else offPeak + r.get(\"offPeakResiduals\")\n    onPeak = if(r.get(\"onPeakResiduals\").isNull) onPeak else onPeak + r.get(\"onPeakResiduals\")\n    return {\"ts\": ts, \"offPeakCuSum\": offPeak.as(\"kWh\"), \"onPeakCuSum\": onPeak.as(\"kWh\")}\n  end\n  cusumPerformanceResiduals = cusumPerformanceResiduals.addColMeta(\"offPeakCuSum\", {\"chartGroup\": \"two\", \"unit\": \"kWh\"}).addColMeta(\"onPeakCuSum\", {\"chartGroup\": \"two\", \"unit\": \"kWh\"})\n  onPeakBaselineMedian: onPeakBaselineResiduals.hisRollup(median, \"*\").first.get(\"baselineResiduals\")\n  offPeakBaselineMedian: offPeakBaselineResiduals.hisRollup(median, \"*\").first.get(\"baselineResiduals\")\n  onPeakPerformanceMedian: onPeakPerformanceResiduals.hisRollup(median, \"*\").first.get(\"performanceResiduals\")\n  offPeakPerformanceMedian: offPeakPerformanceResiduals.hisRollup(median, \"*\").first.get(\"performanceResiduals\")\n  adderDict: {\"onPeakAdder\": (onPeakPerformanceMedian - onPeakBaselineMedian),\n              \"offPeakAdder\": (offPeakPerformanceMedian - offPeakBaselineMedian)}\n  savingsAfterTMRA: savingsHis.map r => do\n    if(performancePeriod.contains(r.get(\"ts\").date)) do\n      ts: r.get(\"ts\")\n      offPeakAdder: if(adderDict.get(\"offPeakAdder\")<3) 0 else adderDict.get(\"offPeakAdder\")\n      onPeakAdder: if(adderDict.get(\"onPeakAdder\")<3) 0 else adderDict.get(\"onPeakAdder\")\n      adder: if(scheduleRec.scheduleValAt(ts).isNull) offPeakAdder else onPeakAdder\n      return r = r.set(\"delta\", r.get(\"delta\") + adder)\n    end else return r\n  end\n  return savingsAfterTMRA\nend",
      "local_src": "(siteId, savingsHis, modelPt,  meterPt, performancePeriodStartDate, performancePeriodEndDate, mildTempRangeLower: \"35\", mildTempRangeUpper: \"50\") => do\n  mildTempRange: (mildTempRangeLower.parseNumber.as(\"\u00b0F\")..mildTempRangeUpper.parseNumber.as(\"\u00b0F\"))\n  performancePeriod : (performancePeriodStartDate..performancePeriodEndDate).toDateSpan\n  siteRec: siteId.toRec\n  schedule: siteRec.get(\"scheduleRef\")\n  scheduleRec: schedule.toRec\n  tempPt: read(temp and weatherStationRef == siteRec.get(\"weatherStationRef\"))\n  baselinePeriod: modelPt.get(\"modeledDataSpan\")\n  modelBaselineHis: modelPt.hisRead(baselinePeriod, {-limit}).hisRollup(avg, 1hr)\n  meterBaselineHis: meterPt.hisRead(baselinePeriod, {-limit}).hisRollup(avg, 1hr)\n  tempBaselineHis: tempPt.hisRead(baselinePeriod, {-limit}).hisRollup(avg, 1hr)\n \n  modelPerformanceHis: modelPt.hisRead(performancePeriod, {-limit}).hisRollup(avg, 1hr)\n  meterPerformanceHis: meterPt.hisRead(performancePeriod, {-limit}).hisRollup(avg, 1hr)\n  tempPerformanceHis: tempPt.hisRead(performancePeriod, {-limit}).hisRollup(avg, 1hr)\n  baselineHis: hisJoin([modelBaselineHis, meterBaselineHis, tempBaselineHis])\n  baselineHis = baselineHis.map(r => if(mildTempRange.contains(r.get(\"v2\"))) return r else return r = null).removeCols([\"v2\"])\n  performanceHis: hisJoin([modelPerformanceHis, meterPerformanceHis, tempPerformanceHis])\n  performanceHis = performanceHis.map(r => if(mildTempRange.contains(r.get(\"v2\"))) return r else return r = null).removeCols([\"v2\"])\n\n  baselineResiduals: addColTwoPointDelta(baselineHis, \"delta\",  \"v1\", \"v0\").keepCols([\"ts\", \"delta\"]).renameCol(\"delta\", \"baselineResiduals\")\n  performanceResiduals: addColTwoPointDelta(performanceHis, \"delta\",  \"v1\", \"v0\").keepCols([\"ts\", \"delta\"]).renameCol(\"delta\", \"performanceResiduals\")\n  baselineResidualTrend: baselineResiduals.checkResidualTrend(tempBaselineHis)\n \n  performanceResidualTrend: performanceResiduals.checkResidualTrend(tempPerformanceHis)\n  if(baselineResidualTrend.get(\"r2\") > 0.2 or performanceResidualTrend.get(\"r2\") > 0.2) return {\"msg\": \"TMRA not applicable\",\n                                                                                                \"reason\": \"Residuals in mildBand have trend\",\n                                                                                                \"baselineR_two\":baselineResidualTrend.get(\"r2\"),\n                                                                                                \"performanceR_two\":performanceResidualTrend.get(\"r2\")}\n  onPeakBaselineResiduals: baselineResiduals.map(r => if(schedule.scheduleValAt(r.get(\"ts\"))==true) return r else return null)\n  offPeakBaselineResiduals: baselineResiduals.map(r => if(schedule.scheduleValAt(r.get(\"ts\")).isNull) return r else return null)\n  presentBaselineResiduals: hisJoin([onPeakBaselineResiduals.addColMeta(\"baselineResiduals\", {dis: \"On Peak Residuals\" + \", n= \" + onPeakBaselineResiduals.size}).renameCol(\"baselineResiduals\", \"onPeakResiduals\"), \n                                     offPeakBaselineResiduals.addColMeta(\"baselineResiduals\", {dis: \"Off Peak Residuals\" + \", n= \" + offPeakBaselineResiduals.size}).renameCol(\"baselineResiduals\", \"offPeakResiduals\")])\n  offPeak: 0\n  onPeak: 0\n  cusumBaselineResiduals: presentBaselineResiduals.map r => do\n    ts: r.get(\"ts\")\n    offPeak = if(r.get(\"offPeakResiduals\").isNull) offPeak else offPeak + r.get(\"offPeakResiduals\")\n    onPeak = if(r.get(\"onPeakResiduals\").isNull) onPeak else onPeak + r.get(\"onPeakResiduals\")\n    return {\"ts\": ts, \"offPeakCuSum\": offPeak.as(\"kWh\"), \"onPeakCuSum\": onPeak.as(\"kWh\")}\n  end\n  cusumBaselineResiduals = cusumBaselineResiduals.addColMeta(\"offPeakCuSum\", {\"chartGroup\": \"one\", \"unit\": \"kWh\"}).addColMeta(\"onPeakCuSum\", {\"chartGroup\": \"one\", \"unit\": \"kWh\"})\n \n                                                                              \n  onPeakPerformanceResiduals: performanceResiduals.map(r => if(schedule.scheduleValAt(r.get(\"ts\"))==true) return r else return null)\n  offPeakPerformanceResiduals: performanceResiduals.map(r => if(schedule.scheduleValAt(r.get(\"ts\")).isNull) return r else return null)\n  presentPerformanceResiduals: hisJoin([onPeakPerformanceResiduals.addColMeta(\"performanceResiduals\", {dis: \"On Peak Residuals\" + \", n= \" + onPeakPerformanceResiduals.size}).renameCol(\"performanceResiduals\", \"onPeakResiduals\"), \n                                        offPeakPerformanceResiduals.addColMeta(\"performanceResiduals\", {dis: \"Off Peak Residuals\" + \", n= \" + offPeakPerformanceResiduals.size}).renameCol(\"performanceResiduals\", \"offPeakResiduals\")])\n  offPeak= 0\n  onPeak= 0\n  cusumPerformanceResiduals: presentPerformanceResiduals.map r => do\n    ts: r.get(\"ts\")\n    offPeak = if(r.get(\"offPeakResiduals\").isNull) offPeak else offPeak + r.get(\"offPeakResiduals\")\n    onPeak = if(r.get(\"onPeakResiduals\").isNull) onPeak else onPeak + r.get(\"onPeakResiduals\")\n    return {\"ts\": ts, \"offPeakCuSum\": offPeak.as(\"kWh\"), \"onPeakCuSum\": onPeak.as(\"kWh\")}\n  end\n  cusumPerformanceResiduals = cusumPerformanceResiduals.addColMeta(\"offPeakCuSum\", {\"chartGroup\": \"two\", \"unit\": \"kWh\"}).addColMeta(\"onPeakCuSum\", {\"chartGroup\": \"two\", \"unit\": \"kWh\"})\n  onPeakBaselineMedian: onPeakBaselineResiduals.hisRollup(median, \"*\").first.get(\"baselineResiduals\")\n  offPeakBaselineMedian: offPeakBaselineResiduals.hisRollup(median, \"*\").first.get(\"baselineResiduals\")\n  onPeakPerformanceMedian: onPeakPerformanceResiduals.hisRollup(median, \"*\").first.get(\"performanceResiduals\")\n  offPeakPerformanceMedian: offPeakPerformanceResiduals.hisRollup(median, \"*\").first.get(\"performanceResiduals\")\n  adderDict: {\"onPeakAdder\": (onPeakPerformanceMedian - onPeakBaselineMedian),\n              \"offPeakAdder\": (offPeakPerformanceMedian - offPeakBaselineMedian)}\n  savingsAfterTMRA: savingsHis.map r => do\n    if(performancePeriod.contains(r.get(\"ts\").date)) do \n      ts: r.get(\"ts\")\n      offPeakAdder: if(adderDict.get(\"offPeakAdder\")<3) 0 else adderDict.get(\"offPeakAdder\")\n      onPeakAdder: if(adderDict.get(\"onPeakAdder\")<3) 0 else adderDict.get(\"onPeakAdder\")\n      adder: if(scheduleRec.scheduleValAt(ts).isNull) offPeakAdder else onPeakAdder\n      return r = r.set(\"delta\", r.get(\"delta\") + adder)\n    end else return r\n  end\n  return savingsAfterTMRA\nend",
      "diff": "@@ -1,4 +1,4 @@-(siteId, savingsHis, modelPt,  meterPt, performancePeriodStartDate, performancePeriodEndDate, mildTempRangeLower: \"40\", mildTempRangeUpper: \"65\") => do\n+(siteId, savingsHis, modelPt,  meterPt, performancePeriodStartDate, performancePeriodEndDate, mildTempRangeLower: \"35\", mildTempRangeUpper: \"50\") => do\n   mildTempRange: (mildTempRangeLower.parseNumber.as(\"\u00b0F\")..mildTempRangeUpper.parseNumber.as(\"\u00b0F\"))\n   performancePeriod : (performancePeriodStartDate..performancePeriodEndDate).toDateSpan\n   siteRec: siteId.toRec\n"
    },
    {
      "name": "func_euiLibrary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(primaryFunction, tag) => do\n\n  lib: [{primaryFunction:\"Bank Branch\", eui_source:209.9, eui_site:88.3},\n        {primaryFunction:\"Aquarium\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Zoo\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Other - Stadium\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Food Sales\", eui_source:592.6, eui_site:231.4},\n        {primaryFunction:\"Food Service\", eui_source:592.6, eui_site:231.4},\n        {primaryFunction:\"Data Center\", eui_source:1.82, eui_site:1.82},\n        {primaryFunction:\"Indoor Arena\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Stadium (Closed)\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Stadium (Open)\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Race Track\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Museum\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Performing Arts\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Movie Theater\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Casino\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Financial Office\", eui_source:116.4, eui_site:52.9},\n        {primaryFunction:\"College/University\", eui_source:180.6, eui_site:84.3},\n        {primaryFunction:\"K-12 School\", eui_source:104.4, eui_site:48.5},\n        {primaryFunction:\"Pre-school/Daycare\", eui_source:131.5, eui_site:64.8},\n        {primaryFunction:\"Adult Education\", eui_source:110.4, eui_site:52.4},\n        {primaryFunction:\"Vocational School\", eui_source:110.4, eui_site:52.4},\n        {primaryFunction:\"Other - Education\", eui_source:110.4, eui_site:52.4},\n        {primaryFunction:\"Convention Center\", eui_source:109.6, eui_site:56.1},\n        {primaryFunction:\"Other - Recreation\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Other - Entertainment/Public Assembly\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Roller Rink\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Swimming Pool\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Bowling Alley\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Ice/Curling Rink\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Fitness Center/Health Club/Gym\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Worship Facility\", eui_source:58.4, eui_site:30.5},\n        {primaryFunction:\"Convenience Store with Gas Station\", eui_source:592.6, eui_site:231.4},\n        {primaryFunction:\"Convenience Store without Gas Station\", eui_source:592.6, eui_site:231.4},\n        {primaryFunction:\"Bar/Nightclub\", eui_source:297, eui_site:130.7},\n        {primaryFunction:\"Fast Food Restaurant\", eui_source:886.4, eui_site:402.7},\n        {primaryFunction:\"Restaurant\", eui_source:573.7, eui_site:325.6},\n        {primaryFunction:\"Wastewater Treatment Plant\", eui_source:7.51, eui_site:2.89},\n        {primaryFunction:\"Other - Restaurant/Bar\", eui_source:573.7, eui_site:325.6},\n        {primaryFunction:\"Supermarket/Grocery Store\", eui_source:444, eui_site:196},\n        {primaryFunction:\"Wholesale Club/Supercenter\", eui_source:120, eui_site:51.4},\n        {primaryFunction:\"Ambulatory Surgical Center\", eui_source:138.3, eui_site:62},\n        {primaryFunction:\"Hospital (General Medical & Surgical)\", eui_source:426.9, eui_site:234.3},\n        {primaryFunction:\"Other/Specialty Hospital\", eui_source:433.9, eui_site:206.7},\n        {primaryFunction:\"Medical Office\", eui_source:121.7, eui_site:51.2},\n        {primaryFunction:\"Outpatient Rehabilitation/Physical Therapy\", eui_source:138.3, eui_site:62},\n        {primaryFunction:\"Urgent Care/Clinic/Other Outpatient\", eui_source:145.8, eui_site:64.5},\n        {primaryFunction:\"Barracks\", eui_source:107.5, eui_site:57.9},\n        {primaryFunction:\"Hotel\", eui_source:146.7, eui_site:63},\n        {primaryFunction:\"Multifamily Housing\", eui_source:118.1, eui_site:59.6},\n        {primaryFunction:\"Prison/Incarceration\", eui_source:156.4, eui_site:69.9},\n        {primaryFunction:\"Residence Hall/Dormitory\", eui_source:107.5, eui_site:57.9},\n        {primaryFunction:\"Mixed Use Property\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Other\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Other - Utility\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Other - Public Services\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Police Station\", eui_source:124.9, eui_site:63.5},\n        {primaryFunction:\"Other - Technology/Science\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Office\", eui_source:116.4, eui_site:52.9},\n        {primaryFunction:\"Veterinary Office\", eui_source:145.8, eui_site:64.5},\n        {primaryFunction:\"Courthouse\", eui_source:211.4, eui_site:101.2},\n        {primaryFunction:\"Fire Station\", eui_source:124.9, eui_site:63.5},\n        {primaryFunction:\"Library\", eui_source:143.6, eui_site:71.6},\n        {primaryFunction:\"Other - Lodging/Residential\", eui_source:143.6, eui_site:63.6},\n        {primaryFunction:\"Mailing Center/Post Office\", eui_source:96.9, eui_site:47.9},\n        {primaryFunction:\"Transportation Terminal/Station\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Automobile Dealership\", eui_source:124.1, eui_site:55},\n        {primaryFunction:\"Enclosed Mall\", eui_source:170.7, eui_site:65.7},\n        {primaryFunction:\"Manufacturing/Industrial Plant\", eui_source:null, eui_site:null},\n        {primaryFunction:\"Single Family Home\", eui_source:null, eui_site:null},\n        {primaryFunction:\"Parking\", eui_source:null, eui_site:null},\n        {primaryFunction:\"Social/Meeting Hall\", eui_source:109.6, eui_site:56.1},\n        {primaryFunction:\"Strip Mall\", eui_source:228.8, eui_site:103.5},\n        {primaryFunction:\"Lifestyle Center\", eui_source:228.8, eui_site:103.5},\n        {primaryFunction:\"Retail Store\", eui_source:120, eui_site:103.5},\n        {primaryFunction:\"Laboratory\", eui_source:318.2, eui_site:115.3},\n        {primaryFunction:\"Drinking Water Treatment & Distribution\", eui_source:5.9, eui_site:2.3},\n        {primaryFunction:\"Repair Services (Vehicle, Shoe, Locksmith, etc)\", eui_source:96.9, eui_site:47.9},\n        {primaryFunction:\"Personal Services (Health/Beauty, Dry Cleaning, etc)\", eui_source:96.9, eui_site:47.9},\n        {primaryFunction:\"Other - Services\", eui_source:96.9, eui_site:47.9},\n        {primaryFunction:\"Energy/Power Station\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Self-Storage Facility\", eui_source:47.8, eui_site:20.2},\n        {primaryFunction:\"Distribution Center\", eui_source:52.9, eui_site:22.7},\n        {primaryFunction:\"Non-Refrigerated Warehouse\", eui_source:52.9, eui_site:22.7},\n        {primaryFunction:\"Senior Care Community\", eui_source:213.2, eui_site:99},\n        {primaryFunction:\"Refrigerated Warehouse\", eui_source:235.6, eui_site:84.1},\n        {primaryFunction:\"Other - Mall\", eui_source:225.3, eui_site:101.6}\n   ].toGrid\n   out: lib.find(r => r.get(\"primaryFunction\")==primaryFunction)\n   if (out==null) return \"\"\n   else return out.get(tag)\nend",
      "local_src": "(primaryFunction, tag) => do\n\n  lib: [{primaryFunction:\"Bank Branch\", eui_source:209.9, eui_site:88.3},\n        {primaryFunction:\"Aquarium\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Zoo\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Other - Stadium\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Food Sales\", eui_source:592.6, eui_site:231.4},\n        {primaryFunction:\"Food Service\", eui_source:592.6, eui_site:231.4},\n        {primaryFunction:\"Data Center\", eui_source:1.82, eui_site:1.82},\n        {primaryFunction:\"Indoor Arena\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Stadium (Closed)\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Stadium (Open)\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Race Track\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Museum\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Performing Arts\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Movie Theater\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Casino\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Financial Office\", eui_source:116.4, eui_site:52.9},\n        {primaryFunction:\"College/University\", eui_source:180.6, eui_site:84.3},\n        {primaryFunction:\"K-12 School\", eui_source:104.4, eui_site:48.5},\n        {primaryFunction:\"Pre-school/Daycare\", eui_source:131.5, eui_site:64.8},\n        {primaryFunction:\"Adult Education\", eui_source:110.4, eui_site:52.4},\n        {primaryFunction:\"Vocational School\", eui_source:110.4, eui_site:52.4},\n        {primaryFunction:\"Other - Education\", eui_source:110.4, eui_site:52.4},\n        {primaryFunction:\"Convention Center\", eui_source:109.6, eui_site:56.1},\n        {primaryFunction:\"Other - Recreation\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Other - Entertainment/Public Assembly\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Roller Rink\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Swimming Pool\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Bowling Alley\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Ice/Curling Rink\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Fitness Center/Health Club/Gym\", eui_source:112, eui_site:50.8},\n        {primaryFunction:\"Worship Facility\", eui_source:58.4, eui_site:30.5},\n        {primaryFunction:\"Convenience Store with Gas Station\", eui_source:592.6, eui_site:231.4},\n        {primaryFunction:\"Convenience Store without Gas Station\", eui_source:592.6, eui_site:231.4},\n        {primaryFunction:\"Bar/Nightclub\", eui_source:297, eui_site:130.7},\n        {primaryFunction:\"Fast Food Restaurant\", eui_source:886.4, eui_site:402.7},\n        {primaryFunction:\"Restaurant\", eui_source:573.7, eui_site:325.6},\n        {primaryFunction:\"Wastewater Treatment Plant\", eui_source:7.51, eui_site:2.89},\n        {primaryFunction:\"Other - Restaurant/Bar\", eui_source:573.7, eui_site:325.6},\n        {primaryFunction:\"Supermarket/Grocery Store\", eui_source:444, eui_site:196},\n        {primaryFunction:\"Wholesale Club/Supercenter\", eui_source:120, eui_site:51.4},\n        {primaryFunction:\"Ambulatory Surgical Center\", eui_source:138.3, eui_site:62},\n        {primaryFunction:\"Hospital (General Medical & Surgical)\", eui_source:426.9, eui_site:234.3},\n        {primaryFunction:\"Other/Specialty Hospital\", eui_source:433.9, eui_site:206.7},\n        {primaryFunction:\"Medical Office\", eui_source:121.7, eui_site:51.2},\n        {primaryFunction:\"Outpatient Rehabilitation/Physical Therapy\", eui_source:138.3, eui_site:62},\n        {primaryFunction:\"Urgent Care/Clinic/Other Outpatient\", eui_source:145.8, eui_site:64.5},\n        {primaryFunction:\"Barracks\", eui_source:107.5, eui_site:57.9},\n        {primaryFunction:\"Hotel\", eui_source:146.7, eui_site:63},\n        {primaryFunction:\"Multifamily Housing\", eui_source:118.1, eui_site:59.6},\n        {primaryFunction:\"Prison/Incarceration\", eui_source:156.4, eui_site:69.9},\n        {primaryFunction:\"Residence Hall/Dormitory\", eui_source:107.5, eui_site:57.9},\n        {primaryFunction:\"Mixed Use Property\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Other\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Other - Utility\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Other - Public Services\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Police Station\", eui_source:124.9, eui_site:63.5},\n        {primaryFunction:\"Other - Technology/Science\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Office\", eui_source:116.4, eui_site:52.9},\n        {primaryFunction:\"Veterinary Office\", eui_source:145.8, eui_site:64.5},\n        {primaryFunction:\"Courthouse\", eui_source:211.4, eui_site:101.2},\n        {primaryFunction:\"Fire Station\", eui_source:124.9, eui_site:63.5},\n        {primaryFunction:\"Library\", eui_source:143.6, eui_site:71.6},\n        {primaryFunction:\"Other - Lodging/Residential\", eui_source:143.6, eui_site:63.6},\n        {primaryFunction:\"Mailing Center/Post Office\", eui_source:96.9, eui_site:47.9},\n        {primaryFunction:\"Transportation Terminal/Station\", eui_source:112, eui_site:56.2},\n        {primaryFunction:\"Automobile Dealership\", eui_source:124.1, eui_site:55},\n        {primaryFunction:\"Enclosed Mall\", eui_source:170.7, eui_site:65.7},\n        {primaryFunction:\"Manufacturing/Industrial Plant\", eui_source:null, eui_site:null},\n        {primaryFunction:\"Single Family Home\", eui_source:null, eui_site:null},\n        {primaryFunction:\"Parking\", eui_source:null, eui_site:null},\n        {primaryFunction:\"Social/Meeting Hall\", eui_source:109.6, eui_site:56.1},\n        {primaryFunction:\"Strip Mall\", eui_source:228.8, eui_site:103.5},\n        {primaryFunction:\"Lifestyle Center\", eui_source:228.8, eui_site:103.5},\n        {primaryFunction:\"Retail Store\", eui_source:120, eui_site:103.5},\n        {primaryFunction:\"Laboratory\", eui_source:318.2, eui_site:256},\n        {primaryFunction:\"Drinking Water Treatment & Distribution\", eui_source:5.9, eui_site:2.3},\n        {primaryFunction:\"Repair Services (Vehicle, Shoe, Locksmith, etc)\", eui_source:96.9, eui_site:47.9},\n        {primaryFunction:\"Personal Services (Health/Beauty, Dry Cleaning, etc)\", eui_source:96.9, eui_site:47.9},\n        {primaryFunction:\"Other - Services\", eui_source:96.9, eui_site:47.9},\n        {primaryFunction:\"Energy/Power Station\", eui_source:89.3, eui_site:40.1},\n        {primaryFunction:\"Self-Storage Facility\", eui_source:47.8, eui_site:20.2},\n        {primaryFunction:\"Distribution Center\", eui_source:52.9, eui_site:22.7},\n        {primaryFunction:\"Non-Refrigerated Warehouse\", eui_source:52.9, eui_site:22.7},\n        {primaryFunction:\"Senior Care Community\", eui_source:213.2, eui_site:99},\n        {primaryFunction:\"Refrigerated Warehouse\", eui_source:235.6, eui_site:84.1},\n        {primaryFunction:\"Other - Mall\", eui_source:225.3, eui_site:101.6}\n   ].toGrid\n   out: lib.find(r => r.get(\"primaryFunction\")==primaryFunction)\n   if (out==null) return \"\"\n   else return out.get(tag)\nend",
      "diff": "@@ -74,7 +74,7 @@         {primaryFunction:\"Strip Mall\", eui_source:228.8, eui_site:103.5},\n         {primaryFunction:\"Lifestyle Center\", eui_source:228.8, eui_site:103.5},\n         {primaryFunction:\"Retail Store\", eui_source:120, eui_site:103.5},\n-        {primaryFunction:\"Laboratory\", eui_source:318.2, eui_site:115.3},\n+        {primaryFunction:\"Laboratory\", eui_source:318.2, eui_site:256},\n         {primaryFunction:\"Drinking Water Treatment & Distribution\", eui_source:5.9, eui_site:2.3},\n         {primaryFunction:\"Repair Services (Vehicle, Shoe, Locksmith, etc)\", eui_source:96.9, eui_site:47.9},\n         {primaryFunction:\"Personal Services (Health/Beauty, Dry Cleaning, etc)\", eui_source:96.9, eui_site:47.9},\n"
    },
    {
      "name": "getCHWMeterSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "//modes:\n// effective will return only the effective points we care about, with specially named columns\n// full will return all points, with more verbose and less general names\n\n(site, dates, opts:{mode:\"effective\"}) => do\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: site->id\n  dates = dates.toSpan\n\n  //get opts\n  debug: if(opts.has(\"debug\")) opts.get(\"debug\")\n  rollupInterval: if (opts.has(\"rollupInterval\")) opts.get(\"rollupInterval\") else 1hr\n  mode: if (opts.has(\"mode\")) opts.get(\"mode\") else \"effective\"\n\n  //set defaults\n  missingPointValue: \"missing point\"\n  missingHisValue: \"missing his\"\n  missingValue: \"missing\"\n\n  //lookup site meter\n  meter: read(siteMeter and chilled and water and siteRef==siteId and equip, false)\n  if (meter.isNull) return {dis:\"Missing site meter\", action:\"Add a site meter and the required points\"}\n\n  //points\n  pointQueryOpts: {equipRef:meter.get(\"id\"), read:true, checked:false}\n\n  chw_intervalCons: pointQuery(\"chilledWater_intervalCons\", pointQueryOpts)\n  chw_intervalModelCons:  pointQuery(\"chilledWater_intervalModelCons\", pointQueryOpts)\n  chw_intervalSavingsCons:  pointQuery(\"chilledWater_intervalSavingsCons\", pointQueryOpts)\n  chw_intervalSavingsCost: pointQuery(\"chilledWater_intervalSavingsCost\", pointQueryOpts)\n  chw_intervalCost: pointQuery(\"chilledWater_intervalCost\", pointQueryOpts)\n\n  chw_monthlyCons: pointQuery(\"chilledWater_monthlyCons\", pointQueryOpts)\n  chw_monthlyModelCons: pointQuery(\"chilledWater_monthlyModelCons\", pointQueryOpts)\n  chw_monthlySavingsCons: pointQuery(\"chilledWater_monthlySavingsCons\", pointQueryOpts)\n  chw_monthlySavingsCost: pointQuery(\"chilledWater_monthlySavingsCost\", pointQueryOpts)\n  chw_monthlyCost: pointQuery(\"chilledWater_monthlyCost\", pointQueryOpts)\n\n  chw_monthlyRateCost:  pointQuery(\"chilledWater_monthlyRateCost\", pointQueryOpts)\n\n\n  points: {chw_intervalCons        : try chw_intervalCons.get(\"id\") catch missingPointValue,\n           chw_intervalModelCons   : try chw_intervalModelCons.get(\"id\") catch missingPointValue,\n           chw_intervalSavingsCons : try chw_intervalSavingsCons.get(\"id\") catch missingPointValue,\n           chw_intervalSavingsCost : try chw_intervalSavingsCost.get(\"id\") catch missingPointValue,\n           chw_intervalCost        : try chw_intervalCost.get(\"id\") catch missingPointValue,\n           chw_monthlyCons         : try chw_monthlyCons.get(\"id\") catch missingPointValue,\n           chw_monthlyModelCons    : try chw_monthlyModelCons.get(\"id\") catch missingPointValue,\n           chw_monthlySavingsCons  : try chw_monthlySavingsCons.get(\"id\") catch missingPointValue,\n           chw_monthlySavingsCost  : try chw_monthlySavingsCost.get(\"id\") catch missingPointValue,\n           chw_monthlyRateCost     : try chw_monthlyRateCost.get(\"id\") catch missingPointValue,\n           chw_monthlyCost         : try chw_monthlyCost.get(\"id\") catch missingPointValue\n          }\n  if(debug==\"points\") return points\n  if (points.vals.all(v=>v==missingPointValue)) return {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n\n  //define col meta options on points\n  if (chw_intervalCons!=null and chw_intervalCons!=missingValue) chw_intervalCons= chw_intervalCons                               .merge({dis:\"Chilled Water Consumption\", chartGroup:\"chw\", color:\"skyBlue\"})\n  if (chw_intervalModelCons!=null and chw_intervalModelCons!=missingValue) chw_intervalModelCons=    chw_intervalModelCons        .merge({dis:\"Chilled Water Model\", chartGroup:\"chw\", color:\"skyBlue\", strokeDasharray:\"1,1\"})\n  if (chw_intervalSavingsCons!=null and chw_intervalSavingsCons!=missingValue) chw_intervalSavingsCons=  chw_intervalSavingsCons  .merge({})\n  if (chw_intervalSavingsCost!=null and chw_intervalSavingsCost!=missingValue) chw_intervalSavingsCost= chw_intervalSavingsCost   .merge({})\n  if (chw_intervalCost!=null and chw_intervalCost!=missingValue) chw_intervalCost= chw_intervalCost                               .merge({})\n\n  if (chw_monthlyCons!=null and chw_monthlyCons!=missingValue) chw_monthlyCons= chw_monthlyCons                                   .merge({dis:\"Chilled Water Consumption\", chartGroup:\"chw\", color:\"skyBlue\"})\n  if (chw_monthlyModelCons!=null and chw_monthlyModelCons!=missingValue) chw_monthlyModelCons= chw_monthlyModelCons               .merge({dis:\"Chilled Water Model\", chartGroup:\"chw\", color:\"skyBlue\", strokeDasharray:\"1,1\"})\n  if (chw_monthlySavingsCons!=null and chw_monthlySavingsCons!=missingValue) chw_monthlySavingsCons= chw_monthlySavingsCons       .merge({})\n  if (chw_monthlySavingsCost!=null and chw_monthlySavingsCost!=missingValue) chw_monthlySavingsCost= chw_monthlySavingsCost       .merge({})\n  if (chw_monthlyCost!=null and chw_monthlyCost!=missingValue) chw_monthlyCost= chw_monthlyCost                                   .merge({})\n\n  if (chw_monthlyRateCost!=null and chw_monthlyRateCost!=missingValue) chw_monthlyRateCost=  chw_monthlyRateCost                  .merge({})\n\n\n  //get his\n  his_chw_intervalCons: try chw_intervalCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_intervalCons\") catch null\n  his_chw_intervalModelCons: try chw_intervalModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_intervalModelCons\") catch null\n  his_chw_intervalSavingsCons: try chw_intervalSavingsCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).renameCol(\"v0\",\"chw_intervalSavingsCons\") catch null\n  his_chw_intervalSavingsCost: try chw_intervalSavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_intervalSavingsCost\") catch null\n  his_chw_intervalCost: try chw_intervalCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_intervalCost\") catch null\n\n  his_chw_monthlyCons: try chw_monthlyCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlyCons\") catch null\n  his_chw_monthlyModelCons: try chw_monthlyModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlyModelCons\") catch null\n  his_chw_monthlySavingsCons: try chw_monthlySavingsCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlySavingsCons\") catch null\n  his_chw_monthlySavingsCost: try chw_monthlySavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlySavingsCost\") catch null\n  his_chw_monthlyCost: try chw_monthlyCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlyCost\") catch null\n\n  his_chw_monthlyRateCost: try chw_monthlyRateCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlyRateCost\") catch null\n\n  //pull together his for output\n  his: [his_chw_intervalCons,\n        his_chw_intervalModelCons,\n        his_chw_intervalSavingsCons,\n        his_chw_intervalSavingsCost,\n        his_chw_intervalCost,\n\n        his_chw_monthlyCons,\n        his_chw_monthlyModelCons,\n        his_chw_monthlySavingsCons,\n        his_chw_monthlySavingsCost,\n        his_chw_monthlyCost,\n\n        his_chw_monthlyRateCost\n\n        ].findAll(v=>v!=null).hisJoin.hisRollup(avg, 1hr)\n\n  //this his summary is helpful when debugging, it shows how many data points exist within this span\n  hisSummary: {  his_chw_intervalCons        :  try his_chw_intervalCons.size catch missingHisValue,\n                 his_chw_intervalModelCons   :  try his_chw_intervalModelCons.size catch missingHisValue,\n                 his_chw_intervalSavingsCons :  try his_chw_intervalSavingsCons.size catch missingHisValue,\n                 his_chw_intervalSavingsCost :  try his_chw_intervalSavingsCost.size catch missingHisValue,\n                 his_chw_intervalCost        :  try his_chw_intervalCost.size catch missingHisValue,\n                 his_chw_monthlyCons         :  try his_chw_monthlyCons.size catch missingHisValue,\n                 his_chw_monthlyModelCons    :  try his_chw_monthlyModelCons.size catch missingHisValue,\n                 his_chw_monthlySavingsCons  :  try his_chw_monthlySavingsCons.size catch missingHisValue,\n                 his_chw_monthlySavingsCost  :  try his_chw_monthlySavingsCost.size catch missingHisValue,\n                 his_chw_monthlyCost         :  try his_chw_monthlyCost.size catch missingHisValue,\n                 his_chw_monthlyRateCost     :  try his_chw_monthlyRateCost.size catch missingHisValue,\n          }\n  if (debug==\"his\") return hisSummary\n\n  //rollup data into summary values we can use quickly in widgets\n  rollup_chw_intervalCons: try his_chw_intervalCons.hisRollup(avg,1hr).hisMap(v=>v.as(\"tonref\")).hisRollup(sum,\"*\").first.get(\"chw_intervalCons\") catch missingValue\n  rollup_chw_intervalModelCons:    try his_chw_intervalModelCons.hisRollup(avg,1hr).hisMap(v=>v.as(\"tonref\")).hisRollup(sum,\"*\").first.get(\"chw_intervalModelCons\") catch missingValue\n  rollup_chw_intervalSavingsCons:  try his_chw_intervalSavingsCons.hisRollup(avg,1hr).hisMap(v=>v.as(\"tonref\")).hisRollup(sum,\"*\").first.get(\"chw_intervalSavingsCons\") catch missingValue\n  rollup_chw_intervalSavingsCost:   try his_chw_intervalSavingsCost.hisRollup(sum,\"*\").first.get(\"chw_intervalSavingsCost\") catch missingValue\n  rollup_chw_intervalCost:   try his_chw_intervalCost.hisRollup(sum,\"*\").first.get(\"chw_intervalCost\") catch missingValue\n\n  rollup_chw_monthlyCons :   try his_chw_monthlyCons.hisRollup(sum,\"*\").first.get(\"chw_monthlyCons\") catch missingValue\n  rollup_chw_monthlyModelCons:   try his_chw_monthlyModelCons.hisRollup(sum,\"*\").first.get(\"chw_monthlyModelCons\") catch missingValue\n  rollup_chw_monthlySavingsCons:   try his_chw_monthlySavingsCons.hisRollup(sum,\"*\").first.get(\"chw_monthlySavingsCons\") catch missingValue\n  rollup_chw_monthlySavingsCost:   try his_chw_monthlySavingsCost.hisRollup(sum,\"*\").first.get(\"chw_monthlySavingsCost\") catch missingValue\n  rollup_chw_monthlyCost:   try his_chw_monthlyCost.hisRollup(sum,\"*\").first.get(\"chw_monthlyCost\") catch missingValue\n\n  rollup_chw_monthlyRateCost:   try his_chw_monthlyRateCost.hisRollup(avg,\"*\").first.get(\"chw_monthlyRateCost\") catch missingValue\n\n  rollup:  {rollup_chw_intervalCons: rollup_chw_intervalCons,\n            rollup_chw_intervalModelCons: rollup_chw_intervalModelCons,\n            rollup_chw_intervalSavingsCons: rollup_chw_intervalSavingsCons,\n            rollup_chw_intervalSavingsCost: rollup_chw_intervalSavingsCost,\n            rollup_chw_intervalCost: rollup_chw_intervalCost,\n\n            rollup_chw_monthlyCons: rollup_chw_monthlyCons,\n            rollup_chw_monthlyModelCons: rollup_chw_monthlyModelCons,\n            rollup_chw_monthlySavingsCons: rollup_chw_monthlySavingsCons,\n            rollup_chw_monthlySavingsCost: rollup_chw_monthlySavingsCost,\n            rollup_chw_monthlyCost: rollup_chw_monthlyCost,\n\n            rollup_chw_monthlyRateCost: rollup_chw_monthlyRateCost\n            }\n\n  //get effective points - this will help when we want to ignore points that don't matter and just get the effective points we're actually using\n  //helper func check for effective points\n  passesChecks: (point) => do\n    startTime: dates.toSpan.start\n    if (startTime.isDate) startTime=startTime.dateTime(0:00)\n    if (point.isDict and point!=missingPointValue and point.get(\"hisSize\")>0 and point.get(\"hisEnd\")>startTime) return true\n    else false\n  end\n\n  chw_usage:       if (chw_intervalCons.passesChecks) chw_intervalCons.get(\"id\")\n               else if (chw_monthlyCons.passesChecks) chw_monthlyCons.get(\"id\")\n               else missingValue\n  chw_model:       if (chw_intervalModelCons.passesChecks) chw_intervalModelCons.get(\"id\")\n               else if (chw_monthlyModelCons.passesChecks) chw_monthlyModelCons.get(\"id\")\n               else missingValue\n  chw_savings:     if (chw_intervalSavingsCons.passesChecks) chw_intervalSavingsCons.get(\"id\")\n               else if (chw_monthlySavingsCons.passesChecks) chw_monthlySavingsCons.get(\"id\")\n               else missingValue\n  chw_cost :       if (chw_intervalCost.passesChecks) chw_intervalCost.get(\"id\")\n               else if (chw_monthlyCost.passesChecks) chw_monthlyCost.get(\"id\")\n               else missingValue\n  chw_savingsCost: if (chw_intervalSavingsCost.passesChecks) chw_intervalSavingsCost.get(\"id\")\n               else if (chw_monthlySavingsCost.passesChecks) chw_monthlySavingsCost.get(\"id\")\n               else missingValue\n\n  effectivePoints: {chw_usage:chw_usage,\n                    chw_model:chw_model,\n                    chw_savings:chw_savings,\n                    chw_cost:chw_cost,\n                    chw_savingsCost:chw_savingsCost,\n                    }\n\n  //this is just to get the \"effective\" his columns for widgets, ignoring the points that don't matter\n  hisCols: his.cols\n  effectiveHisCols: {ts: \"ts\",\n                     chw_usage:       try hisCols.find(r=>r.meta.get(\"id\")==chw_usage).name catch missingValue,\n                     chw_model:       try hisCols.find(r=>r.meta.get(\"id\")==chw_model).name catch missingValue,\n                     chw_savings:     try hisCols.find(r=>r.meta.get(\"id\")==chw_savings).name catch missingValue,\n                     chw_cost:        try hisCols.find(r=>r.meta.get(\"id\")==chw_cost).name catch missingValue,\n                     chw_savingsCost: try hisCols.find(r=>r.meta.get(\"id\")==chw_savingsCost).name catch missingValue,\n                    }\n\n  //get effective rollups - only for the points that are relevant\n  rollup_chw_usage: if (rollup_chw_intervalCons!=null and rollup_chw_intervalCons!=missingValue) rollup_chw_intervalCons\n                else if (rollup_chw_monthlyCons!=null and rollup_chw_monthlyCons!=missingValue) rollup_chw_monthlyCons\n                else null\n  rollup_chw_model: if (rollup_chw_intervalModelCons!=null and rollup_chw_intervalModelCons!=missingValue) rollup_chw_intervalModelCons\n                else if (rollup_chw_monthlyModelCons!=null and rollup_chw_monthlyModelCons!=missingValue) rollup_chw_monthlyModelCons\n                else null\n  rollup_chw_savings: if (rollup_chw_intervalSavingsCons!=null and rollup_chw_intervalSavingsCons!=missingValue) rollup_chw_intervalSavingsCons\n                else if (rollup_chw_monthlySavingsCons!=null and rollup_chw_monthlySavingsCons!=missingValue) rollup_chw_monthlySavingsCons\n                else null\n  rollup_chw_cost: if (rollup_chw_intervalCost!=null and rollup_chw_intervalCost!=missingValue) rollup_chw_intervalCost\n                else if (rollup_chw_monthlyCost!=null and rollup_chw_monthlyCost!=missingValue) rollup_chw_monthlyCost\n                else null\n  rollup_chw_savingsCost: if (rollup_chw_intervalSavingsCost!=null and rollup_chw_intervalSavingsCost!=missingValue) rollup_chw_intervalSavingsCost\n                else if (rollup_chw_monthlySavingsCost!=null and rollup_chw_monthlySavingsCost!=missingValue) rollup_chw_monthlySavingsCost\n                else null\n  rollup_chw_rate: if (siteRec.has(\"chwRate\")) siteRec.get(\"chwRate\")\n                else if (rollup_chw_monthlyRateCost!=null and rollup_chw_monthlyRateCost!=missingValue) rollup_chw_monthlyRateCost\n                else null\n\n  effectiveRollup: {chw_usage: rollup_chw_usage,\n                    chw_model: rollup_chw_model,\n                    chw_savings: rollup_chw_savings,\n                    chw_cost: rollup_chw_cost,\n                    chw_savingsCost: rollup_chw_savingsCost,\n                    chw_rate:rollup_chw_rate,\n                    }\n  //output mode\n  out: his\n\n  //FULL MODE\n  if (mode==\"full\") do //returns everything\n    meta:  {points:points,\n            hisSummary: hisSummary,\n            rollupSummary:rollup,\n            effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup,\n            effectiveHisCols:effectiveHisCols}\n    return out.addMeta(meta)\n  end\n\n  //EFFECTIVE MODE\n  else do //returns only effective points we care about\n\n    meta:  {effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup}\n\n    //which columns to keep\n    effectiveCols: effectiveHisCols.vals.findAll(v=>v!=missingValue)\n\n    //what to rename the columns to in order to standardize them\n    renameDict: {}\n    if (effectiveCols.contains(\"chw_intervalCons\")) renameDict=renameDict.set(\"chw_intervalCons\",\"chw_usage\")\n    if (effectiveCols.contains(\"chw_monthlyCons\")) renameDict=renameDict.set(\"chw_monthlyCons\",\"chw_usage\")\n    if (effectiveCols.contains(\"chw_intervalModelCons\")) renameDict=renameDict.set(\"chw_intervalModelCons\",\"chw_model\")\n    if (effectiveCols.contains(\"chw_monthlyModelCons\")) renameDict=renameDict.set(\"chw_monthlyModelCons\",\"chw_model\")\n    if (effectiveCols.contains(\"chw_intervalSavingsCons\")) renameDict=renameDict.set(\"chw_intervalSavingsCons\",\"chw_savings\")\n    if (effectiveCols.contains(\"chw_monthlySavingsCons\")) renameDict=renameDict.set(\"chw_monthlySavingsCons\",\"chw_savings\")\n    if (effectiveCols.contains(\"chw_intervalCost\")) renameDict=renameDict.set(\"chw_intervalCost\",\"chw_cost\")\n    if (effectiveCols.contains(\"chw_monthlyCost\")) renameDict=renameDict.set(\"chw_monthlyCost\",\"chw_cost\")\n    if (effectiveCols.contains(\"chw_intervalSavingsCost\")) renameDict=renameDict.set(\"chw_intervalSavingsCost\",\"chw_savingsCost\")\n    if (effectiveCols.contains(\"chw_monthlySavingsCost\")) renameDict=renameDict.set(\"chw_monthlySavingsCost\",\"chw_savingsCost\")\n    if (effectiveCols.contains(\"chw_monthlyRateCost\")) renameDict=renameDict.set(\"chw_monthlyRateCost\",\"chw_rate\")\n\n    return out.keepCols(effectiveCols)\n             .renameCols(renameDict)\n             .addMeta(meta)\n  end\nend",
      "local_src": "//modes:\n// effective will return only the effective points we care about, with specially named columns\n// full will return all points, with more verbose and less general names\n\n(site, dates, opts:{mode:\"effective\"}) => do\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: site->id\n\n  //get opts\n  debug: if(opts.has(\"debug\")) opts.get(\"debug\")\n  rollupInterval: if (opts.has(\"rollupInterval\")) opts.get(\"rollupInterval\") else 1hr\n  mode: if (opts.has(\"mode\")) opts.get(\"mode\") else \"effective\"\n\n  //set defaults\n  missingPointValue: \"missing point\"\n  missingHisValue: \"missing his\"\n  missingValue: \"missing\"\n\n  //lookup site meter\n  meter: read(siteMeter and chilled and water and siteRef==siteId and equip, false)\n  if (meter.isNull) return {dis:\"Missing site meter\", action:\"Add a site meter and the required points\"}\n\n  //points\n  pointQueryOpts: {equipRef:meter.get(\"id\"), read:true, checked:false}\n\n  chw_intervalCons: pointQuery(\"chilledWater_intervalCons\", pointQueryOpts)\n  chw_intervalModelCons:  pointQuery(\"chilledWater_intervalModelCons\", pointQueryOpts)\n  chw_intervalSavingsCons:  pointQuery(\"chilledWater_intervalSavingsCons\", pointQueryOpts)\n  chw_intervalSavingsCost: pointQuery(\"chilledWater_intervalSavingsCost\", pointQueryOpts)\n  chw_intervalCost: pointQuery(\"chilledWater_intervalCost\", pointQueryOpts)\n\n  chw_monthlyCons: pointQuery(\"chilledWater_monthlyCons\", pointQueryOpts)\n  chw_monthlyModelCons: pointQuery(\"chilledWater_monthlyModelCons\", pointQueryOpts)\n  chw_monthlySavingsCons: pointQuery(\"chilledWater_monthlySavingsCons\", pointQueryOpts)\n  chw_monthlySavingsCost: pointQuery(\"chilledWater_monthlySavingsCost\", pointQueryOpts)\n  chw_monthlyCost: pointQuery(\"chilledWater_monthlyCost\", pointQueryOpts)\n\n  chw_monthlyRateCost:  pointQuery(\"chilledWater_monthlyRateCost\", pointQueryOpts)\n\n\n  points: {chw_intervalCons        : try chw_intervalCons.get(\"id\") catch missingPointValue,\n           chw_intervalModelCons   : try chw_intervalModelCons.get(\"id\") catch missingPointValue,\n           chw_intervalSavingsCons : try chw_intervalSavingsCons.get(\"id\") catch missingPointValue,\n           chw_intervalSavingsCost : try chw_intervalSavingsCost.get(\"id\") catch missingPointValue,\n           chw_intervalCost        : try chw_intervalCost.get(\"id\") catch missingPointValue,\n           chw_monthlyCons         : try chw_monthlyCons.get(\"id\") catch missingPointValue,\n           chw_monthlyModelCons    : try chw_monthlyModelCons.get(\"id\") catch missingPointValue,\n           chw_monthlySavingsCons  : try chw_monthlySavingsCons.get(\"id\") catch missingPointValue,\n           chw_monthlySavingsCost  : try chw_monthlySavingsCost.get(\"id\") catch missingPointValue,\n           chw_monthlyRateCost     : try chw_monthlyRateCost.get(\"id\") catch missingPointValue,\n           chw_monthlyCost         : try chw_monthlyCost.get(\"id\") catch missingPointValue\n          }\n  if(debug==\"points\") return points\n  if (points.vals.all(v=>v==missingPointValue)) return {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n\n  //define col meta options on points\n  if (chw_intervalCons!=null and chw_intervalCons!=missingValue) chw_intervalCons= chw_intervalCons                               .merge({dis:\"Chilled Water Consumption\", chartGroup:\"chw\", color:\"purple\"})\n  if (chw_intervalModelCons!=null and chw_intervalModelCons!=missingValue) chw_intervalModelCons=    chw_intervalModelCons        .merge({dis:\"Chilled Water Model\", chartGroup:\"chw\", color:\"purple\", strokeDasharray:\"1,1\"})\n  if (chw_intervalSavingsCons!=null and chw_intervalSavingsCons!=missingValue) chw_intervalSavingsCons=  chw_intervalSavingsCons  .merge({})\n  if (chw_intervalSavingsCost!=null and chw_intervalSavingsCost!=missingValue) chw_intervalSavingsCost= chw_intervalSavingsCost   .merge({})\n  if (chw_intervalCost!=null and chw_intervalCost!=missingValue) chw_intervalCost= chw_intervalCost                               .merge({})\n\n  if (chw_monthlyCons!=null and chw_monthlyCons!=missingValue) chw_monthlyCons= chw_monthlyCons                                   .merge({dis:\"Chilled Water Consumption\", chartGroup:\"chw\", color:\"purple\"})\n  if (chw_monthlyModelCons!=null and chw_monthlyModelCons!=missingValue) chw_monthlyModelCons= chw_monthlyModelCons               .merge({dis:\"Chilled Water Model\", chartGroup:\"chw\", color:\"purple\", strokeDasharray:\"1,1\"})\n  if (chw_monthlySavingsCons!=null and chw_monthlySavingsCons!=missingValue) chw_monthlySavingsCons= chw_monthlySavingsCons       .merge({})\n  if (chw_monthlySavingsCost!=null and chw_monthlySavingsCost!=missingValue) chw_monthlySavingsCost= chw_monthlySavingsCost       .merge({})\n  if (chw_monthlyCost!=null and chw_monthlyCost!=missingValue) chw_monthlyCost= chw_monthlyCost                                   .merge({})\n\n  if (chw_monthlyRateCost!=null and chw_monthlyRateCost!=missingValue) chw_monthlyRateCost=  chw_monthlyRateCost                  .merge({})\n\n\n  //get his\n  his_chw_intervalCons: try chw_intervalCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"chw_intervalCons\") catch null\n  his_chw_intervalModelCons: try chw_intervalModelCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"chw_intervalModelCons\") catch null\n  his_chw_intervalSavingsCons: try chw_intervalSavingsCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"chw_intervalSavingsCons\") catch null\n  his_chw_intervalSavingsCost: try chw_intervalSavingsCost.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"chw_intervalSavingsCost\") catch null\n  his_chw_intervalCost: try chw_intervalCost.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"chw_intervalCost\") catch null\n\n  his_chw_monthlyCons: try chw_monthlyCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlyCons\") catch null\n  his_chw_monthlyModelCons: try chw_monthlyModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlyModelCons\") catch null\n  his_chw_monthlySavingsCons: try chw_monthlySavingsCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlySavingsCons\") catch null\n  his_chw_monthlySavingsCost: try chw_monthlySavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlySavingsCost\") catch null\n  his_chw_monthlyCost: try chw_monthlyCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlyCost\") catch null\n\n  his_chw_monthlyRateCost: try chw_monthlyRateCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlyRateCost\") catch null\n\n  //pull together his for output\n  his: [his_chw_intervalCons,\n        his_chw_intervalModelCons,\n        his_chw_intervalSavingsCons,\n        his_chw_intervalSavingsCost,\n        his_chw_intervalCost,\n\n        his_chw_monthlyCons,\n        his_chw_monthlyModelCons,\n        his_chw_monthlySavingsCons,\n        his_chw_monthlySavingsCost,\n        his_chw_monthlyCost,\n\n        his_chw_monthlyRateCost\n\n        ].findAll(v=>v!=null).hisJoin.hisRollup(avg, 1hr)\n\n  //this his summary is helpful when debugging, it shows how many data points exist within this span\n  hisSummary: {  his_chw_intervalCons        :  try his_chw_intervalCons.size catch missingHisValue,\n                 his_chw_intervalModelCons   :  try his_chw_intervalModelCons.size catch missingHisValue,\n                 his_chw_intervalSavingsCons :  try his_chw_intervalSavingsCons.size catch missingHisValue,\n                 his_chw_intervalSavingsCost :  try his_chw_intervalSavingsCost.size catch missingHisValue,\n                 his_chw_intervalCost        :  try his_chw_intervalCost.size catch missingHisValue,\n                 his_chw_monthlyCons         :  try his_chw_monthlyCons.size catch missingHisValue,\n                 his_chw_monthlyModelCons    :  try his_chw_monthlyModelCons.size catch missingHisValue,\n                 his_chw_monthlySavingsCons  :  try his_chw_monthlySavingsCons.size catch missingHisValue,\n                 his_chw_monthlySavingsCost  :  try his_chw_monthlySavingsCost.size catch missingHisValue,\n                 his_chw_monthlyCost         :  try his_chw_monthlyCost.size catch missingHisValue,\n                 his_chw_monthlyRateCost     :  try his_chw_monthlyRateCost.size catch missingHisValue,\n          }\n  if (debug==\"his\") return hisSummary\n\n  //rollup data into summary values we can use quickly in widgets\n  rollup_chw_intervalCons: try his_chw_intervalCons.hisRollup(avg,1hr).hisMap(v=>v.as(\"tonref\")).hisRollup(sum,\"*\").first.get(\"chw_intervalCons\") catch missingValue\n  rollup_chw_intervalModelCons:    try his_chw_intervalModelCons.hisRollup(avg,1hr).hisMap(v=>v.as(\"tonref\")).hisRollup(sum,\"*\").first.get(\"chw_intervalModelCons\") catch missingValue\n  rollup_chw_intervalSavingsCons:  try his_chw_intervalSavingsCons.hisRollup(avg,1hr).hisMap(v=>v.as(\"tonref\")).hisRollup(sum,\"*\").first.get(\"chw_intervalSavingsCons\") catch missingValue\n  rollup_chw_intervalSavingsCost:   try his_chw_intervalSavingsCost.hisRollup(sum,\"*\").first.get(\"chw_intervalSavingsCost\") catch missingValue\n  rollup_chw_intervalCost:   try his_chw_intervalCost.hisRollup(sum,\"*\").first.get(\"chw_intervalCost\") catch missingValue\n\n  rollup_chw_monthlyCons :   try his_chw_monthlyCons.hisRollup(sum,\"*\").first.get(\"chw_monthlyCons\") catch missingValue\n  rollup_chw_monthlyModelCons:   try his_chw_monthlyModelCons.hisRollup(sum,\"*\").first.get(\"chw_monthlyModelCons\") catch missingValue\n  rollup_chw_monthlySavingsCons:   try his_chw_monthlySavingsCons.hisRollup(sum,\"*\").first.get(\"chw_monthlySavingsCons\") catch missingValue\n  rollup_chw_monthlySavingsCost:   try his_chw_monthlySavingsCost.hisRollup(sum,\"*\").first.get(\"chw_monthlySavingsCost\") catch missingValue\n  rollup_chw_monthlyCost:   try his_chw_monthlyCost.hisRollup(sum,\"*\").first.get(\"chw_monthlyCost\") catch missingValue\n\n  rollup_chw_monthlyRateCost:   try his_chw_monthlyRateCost.hisRollup(avg,\"*\").first.get(\"chw_monthlyRateCost\") catch missingValue\n\n  rollup:  {rollup_chw_intervalCons: rollup_chw_intervalCons,\n            rollup_chw_intervalModelCons: rollup_chw_intervalModelCons,\n            rollup_chw_intervalSavingsCons: rollup_chw_intervalSavingsCons,\n            rollup_chw_intervalSavingsCost: rollup_chw_intervalSavingsCost,\n            rollup_chw_intervalCost: rollup_chw_intervalCost,\n\n            rollup_chw_monthlyCons: rollup_chw_monthlyCons,\n            rollup_chw_monthlyModelCons: rollup_chw_monthlyModelCons,\n            rollup_chw_monthlySavingsCons: rollup_chw_monthlySavingsCons,\n            rollup_chw_monthlySavingsCost: rollup_chw_monthlySavingsCost,\n            rollup_chw_monthlyCost: rollup_chw_monthlyCost,\n\n            rollup_chw_monthlyRateCost: rollup_chw_monthlyRateCost\n            }\n\n  //get effective points - this will help when we want to ignore points that don't matter and just get the effective points we're actually using\n  //helper func check for effective points\n  passesChecks: (point) => do\n    startTime: dates.toSpan.start\n    if (startTime.isDate) startTime=startTime.dateTime(0:00)\n    if (point.isDict and point!=missingPointValue and point.get(\"hisSize\")>0 and point.get(\"hisEnd\")>startTime) return true\n    else false\n  end\n\n  chw_usage:       if (chw_intervalCons.passesChecks) chw_intervalCons.get(\"id\")\n               else if (chw_monthlyCons.passesChecks) chw_monthlyCons.get(\"id\")\n               else missingValue\n  chw_model:       if (chw_intervalModelCons.passesChecks) chw_intervalModelCons.get(\"id\")\n               else if (chw_monthlyModelCons.passesChecks) chw_monthlyModelCons.get(\"id\")\n               else missingValue\n  chw_savings:     if (chw_intervalSavingsCons.passesChecks) chw_intervalSavingsCons.get(\"id\")\n               else if (chw_monthlySavingsCons.passesChecks) chw_monthlySavingsCons.get(\"id\")\n               else missingValue\n  chw_cost :       if (chw_intervalCost.passesChecks) chw_intervalCost.get(\"id\")\n               else if (chw_monthlyCost.passesChecks) chw_monthlyCost.get(\"id\")\n               else missingValue\n  chw_savingsCost: if (chw_intervalSavingsCost.passesChecks) chw_intervalSavingsCost.get(\"id\")\n               else if (chw_monthlySavingsCost.passesChecks) chw_monthlySavingsCost.get(\"id\")\n               else missingValue\n\n  effectivePoints: {chw_usage:chw_usage,\n                    chw_model:chw_model,\n                    chw_savings:chw_savings,\n                    chw_cost:chw_cost,\n                    chw_savingsCost:chw_savingsCost,\n                    }\n\n  //this is just to get the \"effective\" his columns for widgets, ignoring the points that don't matter\n  hisCols: his.cols\n  effectiveHisCols: {ts: \"ts\",\n                     chw_usage:       try hisCols.find(r=>r.meta.get(\"id\")==chw_usage).name catch missingValue,\n                     chw_model:       try hisCols.find(r=>r.meta.get(\"id\")==chw_model).name catch missingValue,\n                     chw_savings:     try hisCols.find(r=>r.meta.get(\"id\")==chw_savings).name catch missingValue,\n                     chw_cost:        try hisCols.find(r=>r.meta.get(\"id\")==chw_cost).name catch missingValue,\n                     chw_savingsCost: try hisCols.find(r=>r.meta.get(\"id\")==chw_savingsCost).name catch missingValue,\n                    }\n\n  //get effective rollups - only for the points that are relevant\n  rollup_chw_usage: if (rollup_chw_intervalCons!=null and rollup_chw_intervalCons!=missingValue) rollup_chw_intervalCons\n                else if (rollup_chw_monthlyCons!=null and rollup_chw_monthlyCons!=missingValue) rollup_chw_monthlyCons\n                else null\n  rollup_chw_model: if (rollup_chw_intervalModelCons!=null and rollup_chw_intervalModelCons!=missingValue) rollup_chw_intervalModelCons\n                else if (rollup_chw_monthlyModelCons!=null and rollup_chw_monthlyModelCons!=missingValue) rollup_chw_monthlyModelCons\n                else null\n  rollup_chw_savings: if (rollup_chw_intervalSavingsCons!=null and rollup_chw_intervalSavingsCons!=missingValue) rollup_chw_intervalSavingsCons\n                else if (rollup_chw_monthlySavingsCons!=null and rollup_chw_monthlySavingsCons!=missingValue) rollup_chw_monthlySavingsCons\n                else null\n  rollup_chw_cost: if (rollup_chw_intervalCost!=null and rollup_chw_intervalCost!=missingValue) rollup_chw_intervalCost\n                else if (rollup_chw_monthlyCost!=null and rollup_chw_monthlyCost!=missingValue) rollup_chw_monthlyCost\n                else null\n  rollup_chw_savingsCost: if (rollup_chw_intervalSavingsCost!=null and rollup_chw_intervalSavingsCost!=missingValue) rollup_chw_intervalSavingsCost\n                else if (rollup_chw_monthlySavingsCost!=null and rollup_chw_monthlySavingsCost!=missingValue) rollup_chw_monthlySavingsCost\n                else null\n  rollup_chw_rate: if (siteRec.has(\"chwRate\")) siteRec.get(\"chwRate\")\n                else if (rollup_chw_monthlyRateCost!=null and rollup_chw_monthlyRateCost!=missingValue) rollup_chw_monthlyRateCost\n                else null\n\n  effectiveRollup: {chw_usage: rollup_chw_usage,\n                    chw_model: rollup_chw_model,\n                    chw_savings: rollup_chw_savings,\n                    chw_cost: rollup_chw_cost,\n                    chw_savingsCost: rollup_chw_savingsCost,\n                    chw_rate:rollup_chw_rate,\n                    }\n  //output mode\n  out: his\n\n  //FULL MODE\n  if (mode==\"full\") do //returns everything\n    meta:  {points:points,\n            hisSummary: hisSummary,\n            rollupSummary:rollup,\n            effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup,\n            effectiveHisCols:effectiveHisCols}\n    return out.addMeta(meta)\n  end\n\n  //EFFECTIVE MODE\n  else do //returns only effective points we care about\n\n    meta:  {effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup}\n\n    //which columns to keep\n    effectiveCols: effectiveHisCols.vals.findAll(v=>v!=missingValue)\n\n    //what to rename the columns to in order to standardize them\n    renameDict: {}\n    if (effectiveCols.contains(\"chw_intervalCons\")) renameDict=renameDict.set(\"chw_intervalCons\",\"chw_usage\")\n    if (effectiveCols.contains(\"chw_monthlyCons\")) renameDict=renameDict.set(\"chw_monthlyCons\",\"chw_usage\")\n    if (effectiveCols.contains(\"chw_intervalModelCons\")) renameDict=renameDict.set(\"chw_intervalModelCons\",\"chw_model\")\n    if (effectiveCols.contains(\"chw_monthlyModelCons\")) renameDict=renameDict.set(\"chw_monthlyModelCons\",\"chw_model\")\n    if (effectiveCols.contains(\"chw_intervalSavingsCons\")) renameDict=renameDict.set(\"chw_intervalSavingsCons\",\"chw_savings\")\n    if (effectiveCols.contains(\"chw_monthlySavingsCons\")) renameDict=renameDict.set(\"chw_monthlySavingsCons\",\"chw_savings\")\n    if (effectiveCols.contains(\"chw_intervalCost\")) renameDict=renameDict.set(\"chw_intervalCost\",\"chw_cost\")\n    if (effectiveCols.contains(\"chw_monthlyCost\")) renameDict=renameDict.set(\"chw_monthlyCost\",\"chw_cost\")\n    if (effectiveCols.contains(\"chw_intervalSavingsCost\")) renameDict=renameDict.set(\"chw_intervalSavingsCost\",\"chw_savingsCost\")\n    if (effectiveCols.contains(\"chw_monthlySavingsCost\")) renameDict=renameDict.set(\"chw_monthlySavingsCost\",\"chw_savingsCost\")\n    if (effectiveCols.contains(\"chw_monthlyRateCost\")) renameDict=renameDict.set(\"chw_monthlyRateCost\",\"chw_rate\")\n\n    return out.keepCols(effectiveCols)\n             .renameCols(renameDict)\n             .addMeta(meta)\n  end\nend",
      "diff": "@@ -7,7 +7,6 @@   //normalize inputs\n   siteRec: site.toRec\n   siteId: site->id\n-  dates = dates.toSpan\n \n   //get opts\n   debug: if(opts.has(\"debug\")) opts.get(\"debug\")\n@@ -57,14 +56,14 @@   if (points.vals.all(v=>v==missingPointValue)) return {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n \n   //define col meta options on points\n-  if (chw_intervalCons!=null and chw_intervalCons!=missingValue) chw_intervalCons= chw_intervalCons                               .merge({dis:\"Chilled Water Consumption\", chartGroup:\"chw\", color:\"skyBlue\"})\n-  if (chw_intervalModelCons!=null and chw_intervalModelCons!=missingValue) chw_intervalModelCons=    chw_intervalModelCons        .merge({dis:\"Chilled Water Model\", chartGroup:\"chw\", color:\"skyBlue\", strokeDasharray:\"1,1\"})\n+  if (chw_intervalCons!=null and chw_intervalCons!=missingValue) chw_intervalCons= chw_intervalCons                               .merge({dis:\"Chilled Water Consumption\", chartGroup:\"chw\", color:\"purple\"})\n+  if (chw_intervalModelCons!=null and chw_intervalModelCons!=missingValue) chw_intervalModelCons=    chw_intervalModelCons        .merge({dis:\"Chilled Water Model\", chartGroup:\"chw\", color:\"purple\", strokeDasharray:\"1,1\"})\n   if (chw_intervalSavingsCons!=null and chw_intervalSavingsCons!=missingValue) chw_intervalSavingsCons=  chw_intervalSavingsCons  .merge({})\n   if (chw_intervalSavingsCost!=null and chw_intervalSavingsCost!=missingValue) chw_intervalSavingsCost= chw_intervalSavingsCost   .merge({})\n   if (chw_intervalCost!=null and chw_intervalCost!=missingValue) chw_intervalCost= chw_intervalCost                               .merge({})\n \n-  if (chw_monthlyCons!=null and chw_monthlyCons!=missingValue) chw_monthlyCons= chw_monthlyCons                                   .merge({dis:\"Chilled Water Consumption\", chartGroup:\"chw\", color:\"skyBlue\"})\n-  if (chw_monthlyModelCons!=null and chw_monthlyModelCons!=missingValue) chw_monthlyModelCons= chw_monthlyModelCons               .merge({dis:\"Chilled Water Model\", chartGroup:\"chw\", color:\"skyBlue\", strokeDasharray:\"1,1\"})\n+  if (chw_monthlyCons!=null and chw_monthlyCons!=missingValue) chw_monthlyCons= chw_monthlyCons                                   .merge({dis:\"Chilled Water Consumption\", chartGroup:\"chw\", color:\"purple\"})\n+  if (chw_monthlyModelCons!=null and chw_monthlyModelCons!=missingValue) chw_monthlyModelCons= chw_monthlyModelCons               .merge({dis:\"Chilled Water Model\", chartGroup:\"chw\", color:\"purple\", strokeDasharray:\"1,1\"})\n   if (chw_monthlySavingsCons!=null and chw_monthlySavingsCons!=missingValue) chw_monthlySavingsCons= chw_monthlySavingsCons       .merge({})\n   if (chw_monthlySavingsCost!=null and chw_monthlySavingsCost!=missingValue) chw_monthlySavingsCost= chw_monthlySavingsCost       .merge({})\n   if (chw_monthlyCost!=null and chw_monthlyCost!=missingValue) chw_monthlyCost= chw_monthlyCost                                   .merge({})\n@@ -73,11 +72,11 @@ \n \n   //get his\n-  his_chw_intervalCons: try chw_intervalCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_intervalCons\") catch null\n-  his_chw_intervalModelCons: try chw_intervalModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_intervalModelCons\") catch null\n-  his_chw_intervalSavingsCons: try chw_intervalSavingsCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).renameCol(\"v0\",\"chw_intervalSavingsCons\") catch null\n-  his_chw_intervalSavingsCost: try chw_intervalSavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_intervalSavingsCost\") catch null\n-  his_chw_intervalCost: try chw_intervalCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_intervalCost\") catch null\n+  his_chw_intervalCons: try chw_intervalCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"chw_intervalCons\") catch null\n+  his_chw_intervalModelCons: try chw_intervalModelCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"chw_intervalModelCons\") catch null\n+  his_chw_intervalSavingsCons: try chw_intervalSavingsCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"chw_intervalSavingsCons\") catch null\n+  his_chw_intervalSavingsCost: try chw_intervalSavingsCost.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"chw_intervalSavingsCost\") catch null\n+  his_chw_intervalCost: try chw_intervalCost.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"chw_intervalCost\") catch null\n \n   his_chw_monthlyCons: try chw_monthlyCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlyCons\") catch null\n   his_chw_monthlyModelCons: try chw_monthlyModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"chw_monthlyModelCons\") catch null\n"
    },
    {
      "name": "getElecMeterSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "//modes:\n// effective will return only the effective points we care about, with specially named columns\n// full will return all points, with more verbose and less general names\n\n(site, dates, opts:{mode:\"effective\"}) => do\n\n  //normalize inputs\n  siteRec: site.getRecord\n  siteId: site->id\n  dates = dates.toSpan\n\n  //get opts\n  debug: if (opts.has(\"debug\")) opts.get(\"debug\")\n  rollupInterval: if (opts.has(\"rollupInterval\")) opts.get(\"rollupInterval\") else 1hr\n  mode: if (opts.has(\"mode\")) opts.get(\"mode\") else \"effective\"\n\n  //set defaults\n  missingPointValue: \"missing point\"\n  missingHisValue: \"missing his\"\n  missingValue: \"missing\"\n\n  //cleanup func\n  hisClean: (grid) => do\n    out: grid.addMeta({hisLimit:10000000}).hisMap(v=>if (v.isNaN) null else v)\n  end\n\n  //lookup site meter\n  meter: read(siteMeter and elec and siteRef==siteId and equip, false)\n  if (meter.isNull) throw {dis:\"Missing site meter\", action:\"Add a site meter and the required points\"}\n\n  //get points (queried through pointQuery)\n  pointQueryOpts: {equipRef:meter.get(\"id\"), read:true, checked:false}\n\n  elec_intervalPwr: pointQuery(\"elec_intervalPwr\", pointQueryOpts)\n  elec_modelPwr:    pointQuery(\"elec_modelPwr\", pointQueryOpts)\n  elec_savingsPwr:  pointQuery(\"elec_savingsPwr\", pointQueryOpts)\n  elec_intervalSavingsCost: pointQuery(\"elec_intervalSavingsCost\", pointQueryOpts)\n  elec_intervalCost: pointQuery(\"elec_intervalCost\", pointQueryOpts)\n\n  elec_monthlyEnergy: pointQuery(\"elec_monthlyEnergy\", pointQueryOpts)\n  elec_modelEnergy: pointQuery(\"elec_modelEnergy\", pointQueryOpts)\n  elec_savingsEnergy: pointQuery(\"elec_savingsEnergy\", pointQueryOpts)\n  elec_monthlySavingsCost: pointQuery(\"elec_monthlySavingsCost\", pointQueryOpts)\n  elec_monthlyCost: pointQuery(\"elec_monthlyCost\", pointQueryOpts)\n\n  elec_monthlyRateCost:  pointQuery(\"elec_monthlyRateCost\", pointQueryOpts)\n\n  //pull together points and return them as part of the grid meta\n  points: {elec_intervalPwr        :  try elec_intervalPwr.get(\"id\") catch missingPointValue,\n           elec_modelPwr           :  try elec_modelPwr.get(\"id\") catch missingPointValue,\n           elec_savingsPwr         :  try elec_savingsPwr.get(\"id\") catch missingPointValue,\n           elec_intervalSavingsCost:  try elec_intervalSavingsCost.get(\"id\") catch missingPointValue,\n           elec_intervalCost       :  try elec_intervalCost.get(\"id\") catch missingPointValue,\n           elec_monthlyEnergy      :  try elec_monthlyEnergy.get(\"id\") catch missingPointValue,\n           elec_modelEnergy        :  try elec_modelEnergy.get(\"id\") catch missingPointValue,\n           elec_savingsEnergy      :  try elec_savingsEnergy.get(\"id\") catch missingPointValue,\n           elec_monthlySavingsCost :  try elec_monthlySavingsCost.get(\"id\") catch missingPointValue,\n           elec_monthlyCost        :  try elec_monthlyCost.get(\"id\") catch missingPointValue,\n           elec_monthlyRateCost    :  try elec_monthlyRateCost.get(\"id\") catch missingPointValue,\n          }\n  if (debug==\"points\") return points\n  if (points.vals.all(v=>v==missingPointValue)) throw {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n\n  //define col meta options on points\n  if (elec_intervalPwr!=null and elec_intervalPwr!=missingValue) elec_intervalPwr= elec_intervalPwr                                .merge({dis:\"Electric Usage\", chartGroup:\"elec\", color:\"orange\"})\n  if (elec_modelPwr!=null and elec_modelPwr!=missingValue) elec_modelPwr=    elec_modelPwr                                         .merge({dis:\"Electric Model\", chartGroup:\"elec\", color:\"orange\", strokeDasharray:\"1,1\"})\n  if (elec_savingsPwr!=null and elec_savingsPwr!=missingValue) elec_savingsPwr=  elec_savingsPwr                                   .merge({})\n  if (elec_intervalSavingsCost!=null and elec_intervalSavingsCost!=missingValue) elec_intervalSavingsCost= elec_intervalSavingsCost.merge({})\n  if (elec_intervalCost!=null and elec_intervalCost!=missingValue) elec_intervalCost= elec_intervalCost                            .merge({})\n\n  if (elec_monthlyEnergy!=null and elec_monthlyEnergy!=missingValue) elec_monthlyEnergy= elec_monthlyEnergy                        .merge({dis:\"Electric Usage\", chartGroup:\"elec\", color:\"orange\"})\n  if (elec_modelEnergy!=null and elec_modelEnergy!=missingValue) elec_modelEnergy= elec_modelEnergy                                .merge({dis:\"Electric Model\", chartGroup:\"elec\", color:\"orange\", strokeDasharray:\"1,1\"})\n  if (elec_savingsEnergy!=null and elec_savingsEnergy!=missingValue) elec_savingsEnergy= elec_savingsEnergy                        .merge({})\n  if (elec_monthlySavingsCost!=null and elec_monthlySavingsCost!=missingValue) elec_monthlySavingsCost= elec_monthlySavingsCost    .merge({})\n  if (elec_monthlyCost!=null and elec_monthlyCost!=missingValue) elec_monthlyCost= elec_monthlyCost                                .merge({})\n\n  if (elec_monthlyRateCost!=null and elec_monthlyRateCost!=missingValue) elec_monthlyRateCost=  elec_monthlyRateCost               .merge({})\n\n\n  //get his and rename columns\n  his_elec_intervalPwr: try elec_intervalPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).renameCol(\"v0\",\"elec_intervalPwr\").hisClean catch null\n  his_elec_modelPwr:    try elec_modelPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).renameCol(\"v0\",\"elec_modelPwr\").hisClean catch null\n  his_elec_savingsPwr:  try elec_savingsPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).renameCol(\"v0\",\"elec_savingsPwr\").hisClean catch null\n  his_elec_intervalSavingsCost: try elec_intervalSavingsCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).renameCol(\"v0\",\"elec_intervalSavingsCost\").hisClean catch null\n  his_elec_intervalCost: try elec_intervalCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).renameCol(\"v0\",\"elec_intervalCost\").hisClean catch null\n\n  his_elec_monthlyEnergy: try elec_monthlyEnergy.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_monthlyEnergy\").hisClean catch null\n  his_elec_modelEnergy:   try elec_modelEnergy.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_modelEnergy\").hisClean catch null\n  his_elec_savingsEnergy: try elec_savingsEnergy.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_savingsEnergy\").hisClean catch null\n  his_elec_monthlySavingsCost: try elec_monthlySavingsCost.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_monthlySavingsCost\").hisClean catch null\n  his_elec_monthlyCost: try elec_monthlyCost.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_monthlyCost\").hisClean catch null\n\n  his_elec_monthlyRateCost: try elec_monthlyRateCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"elec_monthlyRateCost\").hisClean catch null\n\n  //pull together his for output\n  his: [his_elec_intervalPwr,\n        his_elec_modelPwr,\n        his_elec_savingsPwr,\n        his_elec_intervalSavingsCost,\n        his_elec_intervalCost,\n\n        his_elec_monthlyEnergy,\n        his_elec_modelEnergy,\n        his_elec_savingsEnergy,\n        his_elec_monthlySavingsCost,\n        his_elec_monthlyCost,\n\n        his_elec_monthlyRateCost\n\n        ].findAll(v=>v!=null).hisJoin.hisRollupAuto(rollupInterval)\n\n  //this his summary is helpful when debugging, it shows how many data points exist within this span\n  hisSummary: {  his_elec_intervalPwr        :  try his_elec_intervalPwr.size catch missingHisValue,\n                 his_elec_modelPwr           :  try his_elec_modelPwr.size catch missingHisValue,\n                 his_elec_savingsPwr         :  try his_elec_savingsPwr.size catch missingHisValue,\n                 his_elec_intervalSavingsCost:  try his_elec_intervalSavingsCost.size catch missingHisValue,\n                 his_elec_intervalCost       :  try his_elec_intervalCost.size catch missingHisValue,\n                 his_elec_monthlyEnergy      :  try his_elec_monthlyEnergy.size catch missingHisValue,\n                 his_elec_modelEnergy        :  try his_elec_modelEnergy.size catch missingHisValue,\n                 his_elec_savingsEnergy      :  try his_elec_savingsEnergy.size catch missingHisValue,\n                 his_elec_monthlySavingsCost :  try his_elec_monthlySavingsCost.size catch missingHisValue,\n                 his_elec_monthlyCost        :  try his_elec_monthlyCost.size catch missingHisValue,\n                 his_elec_monthlyRateCost    :  try his_elec_monthlyRateCost.size catch missingHisValue,\n          }\n  if (debug==\"his\") return hisSummary\n\n  //rollup data into summary values we can use quickly in widgets\n  rollup_elec_intervalPwr: try his_elec_intervalPwr.hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_intervalPwr\") catch missingValue\n  rollup_elec_modelPwr:    try his_elec_modelPwr.hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_modelPwr\") catch missingValue\n  rollup_elec_savingsPwr:  try his_elec_savingsPwr.hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_savingsPwr\") catch missingValue\n  rollup_elec_intervalSavingsCost:   try his_elec_intervalSavingsCost.hisRollup(sum,\"*\").first.get(\"elec_intervalSavingsCost\") catch missingValue\n  rollup_elec_intervalCost:   try his_elec_intervalCost.hisRollup(sum,\"*\").first.get(\"elec_intervalCost\") catch missingValue\n\n  rollup_elec_monthlyEnergy:   try his_elec_monthlyEnergy.hisRollup(sum,\"*\").first.get(\"elec_monthlyEnergy\") catch missingValue\n  rollup_elec_modelEnergy:   try his_elec_modelEnergy.hisRollup(sum,\"*\").first.get(\"elec_modelEnergy\") catch missingValue\n  rollup_elec_savingsEnergy:   try his_elec_savingsEnergy.hisRollup(sum,\"*\").first.get(\"elec_savingsEnergy\") catch missingValue\n  rollup_elec_monthlySavingsCost:   try his_elec_monthlySavingsCost.hisRollup(sum,\"*\").first.get(\"elec_monthlySavingsCost\") catch missingValue\n  rollup_elec_monthlyCost:   try his_elec_monthlyCost.hisRollup(sum,\"*\").first.get(\"elec_monthlyCost\") catch missingValue\n\n  rollup_elec_monthlyRateCost:   try his_elec_monthlyRateCost.hisRollup(avg,\"*\").first.get(\"elec_monthlyRateCost\") catch missingValue\n\n  rollup:  {rollup_elec_intervalPwr: rollup_elec_intervalPwr,\n            rollup_elec_modelPwr:    rollup_elec_modelPwr,\n            rollup_elec_savingsPwr:   rollup_elec_savingsPwr,\n            rollup_elec_intervalSavingsCost:   rollup_elec_intervalSavingsCost,\n            rollup_elec_intervalCost:   rollup_elec_intervalCost,\n\n            rollup_elec_monthlyEnergy:   rollup_elec_monthlyEnergy,\n            rollup_elec_modelEnergy:   rollup_elec_modelEnergy,\n            rollup_elec_savingsEnergy:   rollup_elec_savingsEnergy,\n            rollup_elec_monthlySavingsCost:   rollup_elec_monthlySavingsCost,\n            rollup_elec_monthlyCost:   rollup_elec_monthlyCost,\n\n            rollup_elec_monthlyRateCost:   rollup_elec_monthlyRateCost\n            }\n\n  //get effective points - this will help when we want to ignore points that don't matter and just get the effective points we're actually using\n  //helper func check for effective points\n  passesChecks: (point) => do\n    startTime: dates.toSpan.start\n    if (startTime.isDate) startTime=startTime.dateTime(0:00)\n    if (point.isDict and point!=missingPointValue and point.get(\"hisSize\")>0 and point.get(\"hisEnd\")>startTime) return true\n    else false\n  end\n\n  elec_usage:       if (elec_intervalPwr.passesChecks) elec_intervalPwr.get(\"id\")\n               else if (elec_monthlyEnergy.passesChecks) elec_monthlyEnergy.get(\"id\")\n               else missingValue\n  elec_model:       if (elec_modelPwr.passesChecks) elec_modelPwr.get(\"id\")\n               else if (elec_modelEnergy.passesChecks) elec_modelEnergy.get(\"id\")\n               else missingValue\n  elec_savings:     if (elec_savingsPwr.passesChecks) elec_savingsPwr.get(\"id\")\n               else if (elec_savingsEnergy.passesChecks) elec_savingsEnergy.get(\"id\")\n               else missingValue\n  elec_cost :       if (elec_intervalCost.passesChecks) elec_intervalCost.get(\"id\")\n               else if (elec_monthlyCost.passesChecks) elec_monthlyCost.get(\"id\")\n               else missingValue\n  elec_savingsCost: if (elec_intervalSavingsCost.passesChecks) elec_intervalSavingsCost.get(\"id\")\n               else if (elec_monthlySavingsCost.passesChecks) elec_monthlySavingsCost.get(\"id\")\n               else missingValue\n\n  effectivePoints: {elec_usage:elec_usage,\n                    elec_model:elec_model,\n                    elec_savings:elec_savings,\n                    elec_cost:elec_cost,\n                    elec_savingsCost:elec_savingsCost,\n                    }\n\n  //this is just to get the \"effective\" his columns for widgets, ignoring the points that don't matter\n  hisCols: his.cols\n  effectiveHisCols: {ts: \"ts\",\n                     elec_usage:       try hisCols.find(r=>r.meta.get(\"id\")==elec_usage).name catch missingValue,\n                     elec_model:       try hisCols.find(r=>r.meta.get(\"id\")==elec_model).name catch missingValue,\n                     elec_savings:     try hisCols.find(r=>r.meta.get(\"id\")==elec_savings).name catch missingValue,\n                     elec_cost:        try hisCols.find(r=>r.meta.get(\"id\")==elec_cost).name catch missingValue,\n                     elec_savingsCost: try hisCols.find(r=>r.meta.get(\"id\")==elec_savingsCost).name catch missingValue,\n                    }\n\n  //get effective rollups - only for the points that are relevant\n  rollup_elec_usage: if (rollup_elec_intervalPwr!=null and rollup_elec_intervalPwr!=missingValue) rollup_elec_intervalPwr\n                else if (rollup_elec_monthlyEnergy!=null and rollup_elec_monthlyEnergy!=missingValue) rollup_elec_monthlyEnergy\n                else missingValue\n  rollup_elec_model: if (rollup_elec_modelPwr!=null and rollup_elec_modelPwr!=missingValue) rollup_elec_modelPwr\n                else if (rollup_elec_modelEnergy!=null and rollup_elec_modelEnergy!=missingValue) rollup_elec_modelEnergy\n                else missingValue\n  rollup_elec_savings: if (rollup_elec_savingsPwr!=null and rollup_elec_savingsPwr!=missingValue) rollup_elec_savingsPwr\n                else if (rollup_elec_savingsEnergy!=null and rollup_elec_savingsEnergy!=missingValue) rollup_elec_savingsEnergy\n                else missingValue\n  rollup_elec_cost: if (rollup_elec_intervalCost!=null and rollup_elec_intervalCost!=missingValue) rollup_elec_intervalCost\n                else if (rollup_elec_monthlyCost!=null and rollup_elec_monthlyCost!=missingValue) rollup_elec_monthlyCost\n                else missingValue\n  rollup_elec_savingsCost: if (rollup_elec_intervalSavingsCost!=null and rollup_elec_intervalSavingsCost!=missingValue) rollup_elec_intervalSavingsCost\n                else if (rollup_elec_monthlySavingsCost!=null and rollup_elec_monthlySavingsCost!=missingValue) rollup_elec_monthlySavingsCost\n                else missingValue\n  rollup_elec_rate: if (siteRec.has(\"elecRate\")) siteRec.get(\"elecRate\")\n                else if (rollup_elec_monthlyRateCost!=null and rollup_elec_monthlyRateCost!=missingValue) rollup_elec_monthlyRateCost\n                else missingValue\n\n  effectiveRollup: {elec_usage: rollup_elec_usage,\n                    elec_model: rollup_elec_model,\n                    elec_savings: rollup_elec_savings,\n                    elec_cost: rollup_elec_cost,\n                    elec_savingsCost: rollup_elec_savingsCost,\n                    elec_rate:rollup_elec_rate,\n                    }\n\n  //output mode\n  out: his\n\n  //FULL MODE\n  if (mode==\"full\") do //returns everything\n    meta:  {points:points,\n            hisSummary: hisSummary,\n            rollupSummary:rollup,\n            effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup,\n            effectiveHisCols:effectiveHisCols}\n    return out.addMeta(meta)\n  end\n\n  //EFFECTIVE MODE\n  else do //returns only effective points we care about\n\n    meta:  {effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup}\n\n    //which columns to keep\n    effectiveCols: effectiveHisCols.vals.findAll(v=>v!=missingValue)\n\n    //what to rename the columns to in order to standardize them\n    renameDict: {}\n    if (effectiveCols.contains(\"elec_intervalPwr\")) renameDict=renameDict.set(\"elec_intervalPwr\",\"elec_usage\")\n    if (effectiveCols.contains(\"elec_monthlyEnergy\")) renameDict=renameDict.set(\"elec_monthlyEnergy\",\"elec_usage\")\n    if (effectiveCols.contains(\"elec_modelPwr\")) renameDict=renameDict.set(\"elec_modelPwr\",\"elec_model\")\n    if (effectiveCols.contains(\"elec_modelEnergy\")) renameDict=renameDict.set(\"elec_modelEnergy\",\"elec_model\")\n    if (effectiveCols.contains(\"elec_savingsPwr\")) renameDict=renameDict.set(\"elec_savingsPwr\",\"elec_savings\")\n    if (effectiveCols.contains(\"elec_savingsEnergy\")) renameDict=renameDict.set(\"elec_savingsEnergy\",\"elec_savings\")\n    if (effectiveCols.contains(\"elec_intervalCost\")) renameDict=renameDict.set(\"elec_intervalCost\",\"elec_cost\")\n    if (effectiveCols.contains(\"elec_monthlyCost\")) renameDict=renameDict.set(\"elec_monthlyCost\",\"elec_cost\")\n    if (effectiveCols.contains(\"elec_intervalSavingsCost\")) renameDict=renameDict.set(\"elec_intervalSavingsCost\",\"elec_savingsCost\")\n    if (effectiveCols.contains(\"elec_monthlySavingsCost\")) renameDict=renameDict.set(\"elec_monthlySavingsCost\",\"elec_savingsCost\")\n    if (effectiveCols.contains(\"elec_monthlyRateCost\")) renameDict=renameDict.set(\"elec_monthlyRateCost\",\"elec_rate\")\n\n    return out.reorderKeepCols(effectiveCols)\n              .renameCols(renameDict)\n              .addMeta(meta)\n  end\nend",
      "local_src": "//modes:\n// effective will return only the effective points we care about, with specially named columns\n// full will return all points, with more verbose and less general names\n\n(site, dates, opts:{mode:\"effective\"}) => do\n\n  //normalize inputs\n  siteRec: site.getRecord\n  siteId: site->id\n\n  //get opts\n  debug: if (opts.has(\"debug\")) opts.get(\"debug\")\n  rollupInterval: if (opts.has(\"rollupInterval\")) opts.get(\"rollupInterval\") else 1hr\n  mode: if (opts.has(\"mode\")) opts.get(\"mode\") else \"effective\"\n\n  //set defaults\n  missingPointValue: \"missing point\"\n  missingHisValue: \"missing his\"\n  missingValue: \"missing\"\n\n  //cleanup func\n  hisClean: (grid) => do\n    out: grid.addMeta({hisLimit:10000000}).hisMap(v=>if (v.isNaN) null else v)\n  end\n\n  //lookup site meter\n  meter: read(siteMeter and elec and siteRef==siteId and equip, false)\n  if (meter.isNull) throw {dis:\"Missing site meter\", action:\"Add a site meter and the required points\"}\n\n  //get points (queried through pointQuery)\n  pointQueryOpts: {equipRef:meter.get(\"id\"), read:true, checked:false}\n\n  elec_intervalPwr: pointQuery(\"elec_intervalPwr\", pointQueryOpts)\n  elec_modelPwr:    pointQuery(\"elec_modelPwr\", pointQueryOpts)\n  elec_savingsPwr:  pointQuery(\"elec_savingsPwr\", pointQueryOpts)\n  elec_intervalSavingsCost: pointQuery(\"elec_intervalSavingsCost\", pointQueryOpts)\n  elec_intervalCost: pointQuery(\"elec_intervalCost\", pointQueryOpts)\n\n  elec_monthlyEnergy: pointQuery(\"elec_monthlyEnergy\", pointQueryOpts)\n  elec_modelEnergy: pointQuery(\"elec_modelEnergy\", pointQueryOpts)\n  elec_savingsEnergy: pointQuery(\"elec_savingsEnergy\", pointQueryOpts)\n  elec_monthlySavingsCost: pointQuery(\"elec_monthlySavingsCost\", pointQueryOpts)\n  elec_monthlyCost: pointQuery(\"elec_monthlyCost\", pointQueryOpts)\n\n  elec_monthlyRateCost:  pointQuery(\"elec_monthlyRateCost\", pointQueryOpts)\n\n  //pull together points and return them as part of the grid meta\n  points: {elec_intervalPwr        :  try elec_intervalPwr.get(\"id\") catch missingPointValue,\n           elec_modelPwr           :  try elec_modelPwr.get(\"id\") catch missingPointValue,\n           elec_savingsPwr         :  try elec_savingsPwr.get(\"id\") catch missingPointValue,\n           elec_intervalSavingsCost:  try elec_intervalSavingsCost.get(\"id\") catch missingPointValue,\n           elec_intervalCost       :  try elec_intervalCost.get(\"id\") catch missingPointValue,\n           elec_monthlyEnergy      :  try elec_monthlyEnergy.get(\"id\") catch missingPointValue,\n           elec_modelEnergy        :  try elec_modelEnergy.get(\"id\") catch missingPointValue,\n           elec_savingsEnergy      :  try elec_savingsEnergy.get(\"id\") catch missingPointValue,\n           elec_monthlySavingsCost :  try elec_monthlySavingsCost.get(\"id\") catch missingPointValue,\n           elec_monthlyCost        :  try elec_monthlyCost.get(\"id\") catch missingPointValue,\n           elec_monthlyRateCost    :  try elec_monthlyRateCost.get(\"id\") catch missingPointValue,\n          }\n  if (debug==\"points\") return points\n  if (points.vals.all(v=>v==missingPointValue)) throw {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n\n  //define col meta options on points\n  if (elec_intervalPwr!=null and elec_intervalPwr!=missingValue) elec_intervalPwr= elec_intervalPwr                                .merge({dis:\"Electric Usage\", chartGroup:\"elec\", color:\"orange\"})\n  if (elec_modelPwr!=null and elec_modelPwr!=missingValue) elec_modelPwr=    elec_modelPwr                                         .merge({dis:\"Electric Model\", chartGroup:\"elec\", color:\"orange\", strokeDasharray:\"1,1\"})\n  if (elec_savingsPwr!=null and elec_savingsPwr!=missingValue) elec_savingsPwr=  elec_savingsPwr                                   .merge({})\n  if (elec_intervalSavingsCost!=null and elec_intervalSavingsCost!=missingValue) elec_intervalSavingsCost= elec_intervalSavingsCost.merge({})\n  if (elec_intervalCost!=null and elec_intervalCost!=missingValue) elec_intervalCost= elec_intervalCost                            .merge({})\n\n  if (elec_monthlyEnergy!=null and elec_monthlyEnergy!=missingValue) elec_monthlyEnergy= elec_monthlyEnergy                        .merge({dis:\"Electric Usage\", chartGroup:\"elec\", color:\"orange\"})\n  if (elec_modelEnergy!=null and elec_modelEnergy!=missingValue) elec_modelEnergy= elec_modelEnergy                                .merge({dis:\"Electric Model\", chartGroup:\"elec\", color:\"orange\", strokeDasharray:\"1,1\"})\n  if (elec_savingsEnergy!=null and elec_savingsEnergy!=missingValue) elec_savingsEnergy= elec_savingsEnergy                        .merge({})\n  if (elec_monthlySavingsCost!=null and elec_monthlySavingsCost!=missingValue) elec_monthlySavingsCost= elec_monthlySavingsCost    .merge({})\n  if (elec_monthlyCost!=null and elec_monthlyCost!=missingValue) elec_monthlyCost= elec_monthlyCost                                .merge({})\n\n  if (elec_monthlyRateCost!=null and elec_monthlyRateCost!=missingValue) elec_monthlyRateCost=  elec_monthlyRateCost               .merge({})\n\n\n  //get his and rename columns\n  his_elec_intervalPwr: try elec_intervalPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"elec_intervalPwr\").hisClean catch null\n  his_elec_modelPwr:    try elec_modelPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"elec_modelPwr\").hisClean catch null\n  his_elec_savingsPwr:  try elec_savingsPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"elec_savingsPwr\").hisClean catch null\n  his_elec_intervalSavingsCost: try elec_intervalSavingsCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"elec_intervalSavingsCost\").hisClean catch null\n  his_elec_intervalCost: try elec_intervalCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"elec_intervalCost\").hisClean catch null\n\n  his_elec_monthlyEnergy: try elec_monthlyEnergy.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_monthlyEnergy\").hisClean catch null\n  his_elec_modelEnergy:   try elec_modelEnergy.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_modelEnergy\").hisClean catch null\n  his_elec_savingsEnergy: try elec_savingsEnergy.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_savingsEnergy\").hisClean catch null\n  his_elec_monthlySavingsCost: try elec_monthlySavingsCost.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_monthlySavingsCost\").hisClean catch null\n  his_elec_monthlyCost: try elec_monthlyCost.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_monthlyCost\").hisClean catch null\n\n  his_elec_monthlyRateCost: try elec_monthlyRateCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"elec_monthlyRateCost\").hisClean catch null\n\n  //pull together his for output\n  his: [his_elec_intervalPwr,\n        his_elec_modelPwr,\n        his_elec_savingsPwr,\n        his_elec_intervalSavingsCost,\n        his_elec_intervalCost,\n\n        his_elec_monthlyEnergy,\n        his_elec_modelEnergy,\n        his_elec_savingsEnergy,\n        his_elec_monthlySavingsCost,\n        his_elec_monthlyCost,\n\n        his_elec_monthlyRateCost\n\n        ].findAll(v=>v!=null).hisJoin.hisRollupAuto(rollupInterval)\n\n  //this his summary is helpful when debugging, it shows how many data points exist within this span\n  hisSummary: {  his_elec_intervalPwr        :  try his_elec_intervalPwr.size catch missingHisValue,\n                 his_elec_modelPwr           :  try his_elec_modelPwr.size catch missingHisValue,\n                 his_elec_savingsPwr         :  try his_elec_savingsPwr.size catch missingHisValue,\n                 his_elec_intervalSavingsCost:  try his_elec_intervalSavingsCost.size catch missingHisValue,\n                 his_elec_intervalCost       :  try his_elec_intervalCost.size catch missingHisValue,\n                 his_elec_monthlyEnergy      :  try his_elec_monthlyEnergy.size catch missingHisValue,\n                 his_elec_modelEnergy        :  try his_elec_modelEnergy.size catch missingHisValue,\n                 his_elec_savingsEnergy      :  try his_elec_savingsEnergy.size catch missingHisValue,\n                 his_elec_monthlySavingsCost :  try his_elec_monthlySavingsCost.size catch missingHisValue,\n                 his_elec_monthlyCost        :  try his_elec_monthlyCost.size catch missingHisValue,\n                 his_elec_monthlyRateCost    :  try his_elec_monthlyRateCost.size catch missingHisValue,\n          }\n  if (debug==\"his\") return hisSummary\n\n  //rollup data into summary values we can use quickly in widgets\n  rollup_elec_intervalPwr: try his_elec_intervalPwr.hisRollup(avg,1hr).hisMap(v=>v.to(\"kW\")).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_intervalPwr\") catch missingValue\n  rollup_elec_modelPwr:    try his_elec_modelPwr.hisRollup(avg,1hr).hisMap(v=>v.to(\"kW\")).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_modelPwr\") catch missingValue\n  rollup_elec_savingsPwr:  try his_elec_savingsPwr.hisRollup(avg,1hr).hisMap(v=>v.to(\"kW\")).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_savingsPwr\") catch missingValue\n  rollup_elec_intervalSavingsCost:   try his_elec_intervalSavingsCost.hisRollup(sum,\"*\").first.get(\"elec_intervalSavingsCost\") catch missingValue\n  rollup_elec_intervalCost:   try his_elec_intervalCost.hisRollup(sum,\"*\").first.get(\"elec_intervalCost\") catch missingValue\n\n  rollup_elec_monthlyEnergy:   try his_elec_monthlyEnergy.hisRollup(sum,\"*\").first.get(\"elec_monthlyEnergy\") catch missingValue\n  rollup_elec_modelEnergy:   try his_elec_modelEnergy.hisRollup(sum,\"*\").first.get(\"elec_modelEnergy\") catch missingValue\n  rollup_elec_savingsEnergy:   try his_elec_savingsEnergy.hisRollup(sum,\"*\").first.get(\"elec_savingsEnergy\") catch missingValue\n  rollup_elec_monthlySavingsCost:   try his_elec_monthlySavingsCost.hisRollup(sum,\"*\").first.get(\"elec_monthlySavingsCost\") catch missingValue\n  rollup_elec_monthlyCost:   try his_elec_monthlyCost.hisRollup(sum,\"*\").first.get(\"elec_monthlyCost\") catch missingValue\n\n  rollup_elec_monthlyRateCost:   try his_elec_monthlyRateCost.hisRollup(avg,\"*\").first.get(\"elec_monthlyRateCost\") catch missingValue\n\n  rollup:  {rollup_elec_intervalPwr: rollup_elec_intervalPwr,\n            rollup_elec_modelPwr:    rollup_elec_modelPwr,\n            rollup_elec_savingsPwr:   rollup_elec_savingsPwr,\n            rollup_elec_intervalSavingsCost:   rollup_elec_intervalSavingsCost,\n            rollup_elec_intervalCost:   rollup_elec_intervalCost,\n\n            rollup_elec_monthlyEnergy:   rollup_elec_monthlyEnergy,\n            rollup_elec_modelEnergy:   rollup_elec_modelEnergy,\n            rollup_elec_savingsEnergy:   rollup_elec_savingsEnergy,\n            rollup_elec_monthlySavingsCost:   rollup_elec_monthlySavingsCost,\n            rollup_elec_monthlyCost:   rollup_elec_monthlyCost,\n\n            rollup_elec_monthlyRateCost:   rollup_elec_monthlyRateCost\n            }\n\n  //get effective points - this will help when we want to ignore points that don't matter and just get the effective points we're actually using\n  //helper func check for effective points\n  passesChecks: (point) => do\n    startTime: dates.toSpan.start\n    if (startTime.isDate) startTime=startTime.dateTime(0:00)\n    if (point.isDict and point!=missingPointValue and point.get(\"hisSize\")>0 and point.get(\"hisEnd\")>startTime) return true\n    else false\n  end\n\n  elec_usage:       if (elec_intervalPwr.passesChecks) elec_intervalPwr.get(\"id\")\n               else if (elec_monthlyEnergy.passesChecks) elec_monthlyEnergy.get(\"id\")\n               else missingValue\n  elec_model:       if (elec_modelPwr.passesChecks) elec_modelPwr.get(\"id\")\n               else if (elec_modelEnergy.passesChecks) elec_modelEnergy.get(\"id\")\n               else missingValue\n  elec_savings:     if (elec_savingsPwr.passesChecks) elec_savingsPwr.get(\"id\")\n               else if (elec_savingsEnergy.passesChecks) elec_savingsEnergy.get(\"id\")\n               else missingValue\n  elec_cost :       if (elec_intervalCost.passesChecks) elec_intervalCost.get(\"id\")\n               else if (elec_monthlyCost.passesChecks) elec_monthlyCost.get(\"id\")\n               else missingValue\n  elec_savingsCost: if (elec_intervalSavingsCost.passesChecks) elec_intervalSavingsCost.get(\"id\")\n               else if (elec_monthlySavingsCost.passesChecks) elec_monthlySavingsCost.get(\"id\")\n               else missingValue\n\n  effectivePoints: {elec_usage:elec_usage,\n                    elec_model:elec_model,\n                    elec_savings:elec_savings,\n                    elec_cost:elec_cost,\n                    elec_savingsCost:elec_savingsCost,\n                    }\n\n  //this is just to get the \"effective\" his columns for widgets, ignoring the points that don't matter\n  hisCols: his.cols\n  effectiveHisCols: {ts: \"ts\",\n                     elec_usage:       try hisCols.find(r=>r.meta.get(\"id\")==elec_usage).name catch missingValue,\n                     elec_model:       try hisCols.find(r=>r.meta.get(\"id\")==elec_model).name catch missingValue,\n                     elec_savings:     try hisCols.find(r=>r.meta.get(\"id\")==elec_savings).name catch missingValue,\n                     elec_cost:        try hisCols.find(r=>r.meta.get(\"id\")==elec_cost).name catch missingValue,\n                     elec_savingsCost: try hisCols.find(r=>r.meta.get(\"id\")==elec_savingsCost).name catch missingValue,\n                    }\n\n  //get effective rollups - only for the points that are relevant\n  rollup_elec_usage: if (rollup_elec_intervalPwr!=null and rollup_elec_intervalPwr!=missingValue) rollup_elec_intervalPwr\n                else if (rollup_elec_monthlyEnergy!=null and rollup_elec_monthlyEnergy!=missingValue) rollup_elec_monthlyEnergy\n                else null\n  rollup_elec_model: if (rollup_elec_modelPwr!=null and rollup_elec_modelPwr!=missingValue) rollup_elec_modelPwr\n                else if (rollup_elec_modelEnergy!=null and rollup_elec_modelEnergy!=missingValue) rollup_elec_modelEnergy\n                else null\n  rollup_elec_savings: if (rollup_elec_savingsPwr!=null and rollup_elec_savingsPwr!=missingValue) rollup_elec_savingsPwr\n                else if (rollup_elec_savingsEnergy!=null and rollup_elec_savingsEnergy!=missingValue) rollup_elec_savingsEnergy\n                else null\n  rollup_elec_cost: if (rollup_elec_intervalCost!=null and rollup_elec_intervalCost!=missingValue) rollup_elec_intervalCost\n                else if (rollup_elec_monthlyCost!=null and rollup_elec_monthlyCost!=missingValue) rollup_elec_monthlyCost\n                else null\n  rollup_elec_savingsCost: if (rollup_elec_intervalSavingsCost!=null and rollup_elec_intervalSavingsCost!=missingValue) rollup_elec_intervalSavingsCost\n                else if (rollup_elec_monthlySavingsCost!=null and rollup_elec_monthlySavingsCost!=missingValue) rollup_elec_monthlySavingsCost\n                else null\n  rollup_elec_rate: if (siteRec.has(\"elecRate\")) siteRec.get(\"elecRate\")\n                else if (rollup_elec_monthlyRateCost!=null and rollup_elec_monthlyRateCost!=missingValue) rollup_elec_monthlyRateCost\n                else null\n\n  effectiveRollup: {elec_usage: rollup_elec_usage,\n                    elec_model: rollup_elec_model,\n                    elec_savings: rollup_elec_savings,\n                    elec_cost: rollup_elec_cost,\n                    elec_savingsCost: rollup_elec_savingsCost,\n                    elec_rate:rollup_elec_rate,\n                    }\n\n  //output mode\n  out: his\n\n  //FULL MODE\n  if (mode==\"full\") do //returns everything\n    meta:  {points:points,\n            hisSummary: hisSummary,\n            rollupSummary:rollup,\n            effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup,\n            effectiveHisCols:effectiveHisCols}\n    return out.addMeta(meta)\n  end\n\n  //EFFECTIVE MODE\n  else do //returns only effective points we care about\n\n    meta:  {effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup}\n\n    //which columns to keep\n    effectiveCols: effectiveHisCols.vals.findAll(v=>v!=missingValue)\n\n    //what to rename the columns to in order to standardize them\n    renameDict: {}\n    if (effectiveCols.contains(\"elec_intervalPwr\")) renameDict=renameDict.set(\"elec_intervalPwr\",\"elec_usage\")\n    if (effectiveCols.contains(\"elec_monthlyEnergy\")) renameDict=renameDict.set(\"elec_monthlyEnergy\",\"elec_usage\")\n    if (effectiveCols.contains(\"elec_modelPwr\")) renameDict=renameDict.set(\"elec_modelPwr\",\"elec_model\")\n    if (effectiveCols.contains(\"elec_modelEnergy\")) renameDict=renameDict.set(\"elec_modelEnergy\",\"elec_model\")\n    if (effectiveCols.contains(\"elec_savingsPwr\")) renameDict=renameDict.set(\"elec_savingsPwr\",\"elec_savings\")\n    if (effectiveCols.contains(\"elec_savingsEnergy\")) renameDict=renameDict.set(\"elec_savingsEnergy\",\"elec_savings\")\n    if (effectiveCols.contains(\"elec_intervalCost\")) renameDict=renameDict.set(\"elec_intervalCost\",\"elec_cost\")\n    if (effectiveCols.contains(\"elec_monthlyCost\")) renameDict=renameDict.set(\"elec_monthlyCost\",\"elec_cost\")\n    if (effectiveCols.contains(\"elec_intervalSavingsCost\")) renameDict=renameDict.set(\"elec_intervalSavingsCost\",\"elec_savingsCost\")\n    if (effectiveCols.contains(\"elec_monthlySavingsCost\")) renameDict=renameDict.set(\"elec_monthlySavingsCost\",\"elec_savingsCost\")\n    if (effectiveCols.contains(\"elec_monthlyRateCost\")) renameDict=renameDict.set(\"elec_monthlyRateCost\",\"elec_rate\")\n\n    return out.reorderKeepCols(effectiveCols)\n              .renameCols(renameDict)\n              .addMeta(meta)\n  end\nend",
      "diff": "@@ -7,7 +7,6 @@   //normalize inputs\n   siteRec: site.getRecord\n   siteId: site->id\n-  dates = dates.toSpan\n \n   //get opts\n   debug: if (opts.has(\"debug\")) opts.get(\"debug\")\n@@ -78,11 +77,11 @@ \n \n   //get his and rename columns\n-  his_elec_intervalPwr: try elec_intervalPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).renameCol(\"v0\",\"elec_intervalPwr\").hisClean catch null\n-  his_elec_modelPwr:    try elec_modelPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).renameCol(\"v0\",\"elec_modelPwr\").hisClean catch null\n-  his_elec_savingsPwr:  try elec_savingsPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).renameCol(\"v0\",\"elec_savingsPwr\").hisClean catch null\n-  his_elec_intervalSavingsCost: try elec_intervalSavingsCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).renameCol(\"v0\",\"elec_intervalSavingsCost\").hisClean catch null\n-  his_elec_intervalCost: try elec_intervalCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).renameCol(\"v0\",\"elec_intervalCost\").hisClean catch null\n+  his_elec_intervalPwr: try elec_intervalPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"elec_intervalPwr\").hisClean catch null\n+  his_elec_modelPwr:    try elec_modelPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"elec_modelPwr\").hisClean catch null\n+  his_elec_savingsPwr:  try elec_savingsPwr.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"elec_savingsPwr\").hisClean catch null\n+  his_elec_intervalSavingsCost: try elec_intervalSavingsCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"elec_intervalSavingsCost\").hisClean catch null\n+  his_elec_intervalCost: try elec_intervalCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"elec_intervalCost\").hisClean catch null\n \n   his_elec_monthlyEnergy: try elec_monthlyEnergy.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_monthlyEnergy\").hisClean catch null\n   his_elec_modelEnergy:   try elec_modelEnergy.hisRead(dates, {-limit}).hisRollup(sum,1mo).renameCol(\"v0\",\"elec_modelEnergy\").hisClean catch null\n@@ -125,9 +124,9 @@   if (debug==\"his\") return hisSummary\n \n   //rollup data into summary values we can use quickly in widgets\n-  rollup_elec_intervalPwr: try his_elec_intervalPwr.hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_intervalPwr\") catch missingValue\n-  rollup_elec_modelPwr:    try his_elec_modelPwr.hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_modelPwr\") catch missingValue\n-  rollup_elec_savingsPwr:  try his_elec_savingsPwr.hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_savingsPwr\") catch missingValue\n+  rollup_elec_intervalPwr: try his_elec_intervalPwr.hisRollup(avg,1hr).hisMap(v=>v.to(\"kW\")).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_intervalPwr\") catch missingValue\n+  rollup_elec_modelPwr:    try his_elec_modelPwr.hisRollup(avg,1hr).hisMap(v=>v.to(\"kW\")).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_modelPwr\") catch missingValue\n+  rollup_elec_savingsPwr:  try his_elec_savingsPwr.hisRollup(avg,1hr).hisMap(v=>v.to(\"kW\")).hisMap(v=>v.as(\"kWh\")).hisRollup(sum,\"*\").first.get(\"elec_savingsPwr\") catch missingValue\n   rollup_elec_intervalSavingsCost:   try his_elec_intervalSavingsCost.hisRollup(sum,\"*\").first.get(\"elec_intervalSavingsCost\") catch missingValue\n   rollup_elec_intervalCost:   try his_elec_intervalCost.hisRollup(sum,\"*\").first.get(\"elec_intervalCost\") catch missingValue\n \n@@ -199,22 +198,22 @@   //get effective rollups - only for the points that are relevant\n   rollup_elec_usage: if (rollup_elec_intervalPwr!=null and rollup_elec_intervalPwr!=missingValue) rollup_elec_intervalPwr\n                 else if (rollup_elec_monthlyEnergy!=null and rollup_elec_monthlyEnergy!=missingValue) rollup_elec_monthlyEnergy\n-                else missingValue\n+                else null\n   rollup_elec_model: if (rollup_elec_modelPwr!=null and rollup_elec_modelPwr!=missingValue) rollup_elec_modelPwr\n                 else if (rollup_elec_modelEnergy!=null and rollup_elec_modelEnergy!=missingValue) rollup_elec_modelEnergy\n-                else missingValue\n+                else null\n   rollup_elec_savings: if (rollup_elec_savingsPwr!=null and rollup_elec_savingsPwr!=missingValue) rollup_elec_savingsPwr\n                 else if (rollup_elec_savingsEnergy!=null and rollup_elec_savingsEnergy!=missingValue) rollup_elec_savingsEnergy\n-                else missingValue\n+                else null\n   rollup_elec_cost: if (rollup_elec_intervalCost!=null and rollup_elec_intervalCost!=missingValue) rollup_elec_intervalCost\n                 else if (rollup_elec_monthlyCost!=null and rollup_elec_monthlyCost!=missingValue) rollup_elec_monthlyCost\n-                else missingValue\n+                else null\n   rollup_elec_savingsCost: if (rollup_elec_intervalSavingsCost!=null and rollup_elec_intervalSavingsCost!=missingValue) rollup_elec_intervalSavingsCost\n                 else if (rollup_elec_monthlySavingsCost!=null and rollup_elec_monthlySavingsCost!=missingValue) rollup_elec_monthlySavingsCost\n-                else missingValue\n+                else null\n   rollup_elec_rate: if (siteRec.has(\"elecRate\")) siteRec.get(\"elecRate\")\n                 else if (rollup_elec_monthlyRateCost!=null and rollup_elec_monthlyRateCost!=missingValue) rollup_elec_monthlyRateCost\n-                else missingValue\n+                else null\n \n   effectiveRollup: {elec_usage: rollup_elec_usage,\n                     elec_model: rollup_elec_model,\n"
    },
    {
      "name": "getHWMeterSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "//modes:\n// effective will return only the effective points we care about, with specially named columns\n// full will return all points, with more verbose and less general names\n\n(site, dates, opts:{mode:\"effective\"}) => do\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: site->id\n  dates = dates.toSpan\n\n  //get opts\n  debug: if(opts.has(\"debug\")) opts.get(\"debug\")\n  rollupInterval: if (opts.has(\"rollupInterval\")) opts.get(\"rollupInterval\") else 1hr\n  mode: if (opts.has(\"mode\")) opts.get(\"mode\") else \"effective\"\n\n  //set defaults\n  missingPointValue: \"missing point\"\n  missingHisValue: \"missing his\"\n  missingValue: \"missing\"\n\n  //lookup site meter\n  meter: read(siteMeter and hot and water and siteRef==siteId and equip, false)\n  if (meter.isNull) return {dis:\"Missing site meter\", action:\"Add a site meter and the required points\"}\n\n  //points\n  pointQueryOpts: {equipRef:meter.get(\"id\"), read:true, checked:false}\n\n  hw_intervalCons: pointQuery(\"hotWater_intervalCons\", pointQueryOpts)\n  hw_intervalModelCons:  pointQuery(\"hotWater_intervalModelCons\", pointQueryOpts)\n  hw_intervalSavingsCons:  pointQuery(\"hotWater_intervalSavingsCons\", pointQueryOpts)\n  hw_intervalSavingsCost: pointQuery(\"hotWater_intervalSavingsCost\", pointQueryOpts)\n  hw_intervalCost: pointQuery(\"hotWater_intervalCost\", pointQueryOpts)\n\n  hw_monthlyCons: pointQuery(\"hotWater_monthlyCons\", pointQueryOpts)\n  hw_monthlyModelCons: pointQuery(\"hotWater_monthlyModelCons\", pointQueryOpts)\n  hw_monthlySavingsCons: pointQuery(\"hotWater_monthlySavingsCons\", pointQueryOpts)\n  hw_monthlySavingsCost: pointQuery(\"hotWater_monthlySavingsCost\", pointQueryOpts)\n  hw_monthlyCost: pointQuery(\"hotWater_monthlyCost\", pointQueryOpts)\n\n  hw_monthlyRateCost:  pointQuery(\"hotWater_monthlyRateCost\", pointQueryOpts)\n\n\n  points: {hw_intervalCons        : try hw_intervalCons.get(\"id\") catch missingPointValue,\n           hw_intervalModelCons   : try hw_intervalModelCons.get(\"id\") catch missingPointValue,\n           hw_intervalSavingsCons : try hw_intervalSavingsCons.get(\"id\") catch missingPointValue,\n           hw_intervalSavingsCost : try hw_intervalSavingsCost.get(\"id\") catch missingPointValue,\n           hw_intervalCost        : try hw_intervalCost.get(\"id\") catch missingPointValue,\n           hw_monthlyCons         : try hw_monthlyCons.get(\"id\") catch missingPointValue,\n           hw_monthlyModelCons    : try hw_monthlyModelCons.get(\"id\") catch missingPointValue,\n           hw_monthlySavingsCons  : try hw_monthlySavingsCons.get(\"id\") catch missingPointValue,\n           hw_monthlySavingsCost  : try hw_monthlySavingsCost.get(\"id\") catch missingPointValue,\n           hw_monthlyRateCost     : try hw_monthlyRateCost.get(\"id\") catch missingPointValue,\n           hw_monthlyCost         : try hw_monthlyCost.get(\"id\") catch missingPointValue\n          }\n  if(debug==\"points\") return points\n  if (points.vals.all(v=>v==missingPointValue)) return {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n\n  //define col meta options on points\n  if (hw_intervalCons!=null and hw_intervalCons!=missingValue) hw_intervalCons= hw_intervalCons                               .merge({dis:\"Hot Water Consumption\", chartGroup:\"hw\", color:\"tomato\"})\n  if (hw_intervalModelCons!=null and hw_intervalModelCons!=missingValue) hw_intervalModelCons=    hw_intervalModelCons        .merge({dis:\"Hot Water Model\", chartGroup:\"hw\", color:\"tomato\", strokeDasharray:\"1,1\"})\n  if (hw_intervalSavingsCons!=null and hw_intervalSavingsCons!=missingValue) hw_intervalSavingsCons=  hw_intervalSavingsCons  .merge({})\n  if (hw_intervalSavingsCost!=null and hw_intervalSavingsCost!=missingValue) hw_intervalSavingsCost= hw_intervalSavingsCost   .merge({})\n  if (hw_intervalCost!=null and hw_intervalCost!=missingValue) hw_intervalCost= hw_intervalCost                               .merge({})\n\n  if (hw_monthlyCons!=null and hw_monthlyCons!=missingValue) hw_monthlyCons= hw_monthlyCons                                   .merge({dis:\"Hot Water Consumption\", chartGroup:\"hw\", color:\"tomato\"})\n  if (hw_monthlyModelCons!=null and hw_monthlyModelCons!=missingValue) hw_monthlyModelCons= hw_monthlyModelCons               .merge({dis:\"Hot Water Model\", chartGroup:\"hw\", color:\"tomato\", strokeDasharray:\"1,1\"})\n  if (hw_monthlySavingsCons!=null and hw_monthlySavingsCons!=missingValue) hw_monthlySavingsCons= hw_monthlySavingsCons       .merge({})\n  if (hw_monthlySavingsCost!=null and hw_monthlySavingsCost!=missingValue) hw_monthlySavingsCost= hw_monthlySavingsCost       .merge({})\n  if (hw_monthlyCost!=null and hw_monthlyCost!=missingValue) hw_monthlyCost= hw_monthlyCost                                   .merge({})\n\n  if (hw_monthlyRateCost!=null and hw_monthlyRateCost!=missingValue) hw_monthlyRateCost=  hw_monthlyRateCost                  .merge({})\n\n\n  //get his\n  his_hw_intervalCons: try hw_intervalCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_intervalCons\") catch null\n  his_hw_intervalModelCons: try hw_intervalModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_intervalModelCons\") catch null\n  his_hw_intervalSavingsCons: try hw_intervalSavingsCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).renameCol(\"v0\",\"hw_intervalSavingsCons\") catch null\n  his_hw_intervalSavingsCost: try hw_intervalSavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_intervalSavingsCost\") catch null\n  his_hw_intervalCost: try hw_intervalCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_intervalCost\") catch null\n\n  his_hw_monthlyCons: try hw_monthlyCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlyCons\") catch null\n  his_hw_monthlyModelCons: try hw_monthlyModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlyModelCons\") catch null\n  his_hw_monthlySavingsCons: try hw_monthlySavingsCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlySavingsCons\") catch null\n  his_hw_monthlySavingsCost: try hw_monthlySavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlySavingsCost\") catch null\n  his_hw_monthlyCost: try hw_monthlyCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlyCost\") catch null\n\n  his_hw_monthlyRateCost: try hw_monthlyRateCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlyRateCost\") catch null\n\n  //pull together his for output\n  his: [his_hw_intervalCons,\n        his_hw_intervalModelCons,\n        his_hw_intervalSavingsCons,\n        his_hw_intervalSavingsCost,\n        his_hw_intervalCost,\n\n        his_hw_monthlyCons,\n        his_hw_monthlyModelCons,\n        his_hw_monthlySavingsCons,\n        his_hw_monthlySavingsCost,\n        his_hw_monthlyCost,\n\n        his_hw_monthlyRateCost\n\n        ].findAll(v=>v!=null).hisJoin.hisRollup(avg, 1hr)\n\n  //this his summary is helpful when debugging, it shows how many data points exist within this span\n  hisSummary: {  his_hw_intervalCons        :  try his_hw_intervalCons.size catch missingHisValue,\n                 his_hw_intervalModelCons   :  try his_hw_intervalModelCons.size catch missingHisValue,\n                 his_hw_intervalSavingsCons :  try his_hw_intervalSavingsCons.size catch missingHisValue,\n                 his_hw_intervalSavingsCost :  try his_hw_intervalSavingsCost.size catch missingHisValue,\n                 his_hw_intervalCost        :  try his_hw_intervalCost.size catch missingHisValue,\n                 his_hw_monthlyCons         :  try his_hw_monthlyCons.size catch missingHisValue,\n                 his_hw_monthlyModelCons    :  try his_hw_monthlyModelCons.size catch missingHisValue,\n                 his_hw_monthlySavingsCons  :  try his_hw_monthlySavingsCons.size catch missingHisValue,\n                 his_hw_monthlySavingsCost  :  try his_hw_monthlySavingsCost.size catch missingHisValue,\n                 his_hw_monthlyCost         :  try his_hw_monthlyCost.size catch missingHisValue,\n                 his_hw_monthlyRateCost     :  try his_hw_monthlyRateCost.size catch missingHisValue,\n          }\n  if (debug==\"his\") return hisSummary\n\n  //rollup data into summary values we can use quickly in widgets\n  rollup_hw_intervalCons: try his_hw_intervalCons.hisRollup(avg,1hr).hisMap(v=>v.to(\"kBTU/h\")).hisRollup(sum,\"*\").first.get(\"hw_intervalCons\") catch missingValue\n  rollup_hw_intervalModelCons:    try his_hw_intervalModelCons.hisRollup(avg,1hr).hisMap(v=>v.to(\"kBTU/h\")).hisRollup(sum,\"*\").first.get(\"hw_intervalModelCons\") catch missingValue\n  rollup_hw_intervalSavingsCons:  try his_hw_intervalSavingsCons.hisRollup(avg,1hr).hisMap(v=>v.to(\"kBTU/h\")).hisRollup(sum,\"*\").first.get(\"hw_intervalSavingsCons\") catch missingValue\n  rollup_hw_intervalSavingsCost:   try his_hw_intervalSavingsCost.hisRollup(sum,\"*\").first.get(\"hw_intervalSavingsCost\") catch missingValue\n  rollup_hw_intervalCost:   try his_hw_intervalCost.hisRollup(sum,\"*\").first.get(\"hw_intervalCost\") catch missingValue\n\n  rollup_hw_monthlyCons :   try his_hw_monthlyCons.hisRollup(sum,\"*\").first.get(\"hw_monthlyCons\") catch missingValue\n  rollup_hw_monthlyModelCons:   try his_hw_monthlyModelCons.hisRollup(sum,\"*\").first.get(\"hw_monthlyModelCons\") catch missingValue\n  rollup_hw_monthlySavingsCons:   try his_hw_monthlySavingsCons.hisRollup(sum,\"*\").first.get(\"hw_monthlySavingsCons\") catch missingValue\n  rollup_hw_monthlySavingsCost:   try his_hw_monthlySavingsCost.hisRollup(sum,\"*\").first.get(\"hw_monthlySavingsCost\") catch missingValue\n  rollup_hw_monthlyCost:   try his_hw_monthlyCost.hisRollup(sum,\"*\").first.get(\"hw_monthlyCost\") catch missingValue\n\n  rollup_hw_monthlyRateCost:   try his_hw_monthlyRateCost.hisRollup(avg,\"*\").first.get(\"hw_monthlyRateCost\") catch missingValue\n\n  rollup:  {rollup_hw_intervalCons: rollup_hw_intervalCons,\n            rollup_hw_intervalModelCons: rollup_hw_intervalModelCons,\n            rollup_hw_intervalSavingsCons: rollup_hw_intervalSavingsCons,\n            rollup_hw_intervalSavingsCost: rollup_hw_intervalSavingsCost,\n            rollup_hw_intervalCost: rollup_hw_intervalCost,\n\n            rollup_hw_monthlyCons: rollup_hw_monthlyCons,\n            rollup_hw_monthlyModelCons: rollup_hw_monthlyModelCons,\n            rollup_hw_monthlySavingsCons: rollup_hw_monthlySavingsCons,\n            rollup_hw_monthlySavingsCost: rollup_hw_monthlySavingsCost,\n            rollup_hw_monthlyCost: rollup_hw_monthlyCost,\n\n            rollup_hw_monthlyRateCost: rollup_hw_monthlyRateCost\n            }\n\n  //get effective points - this will help when we want to ignore points that don't matter and just get the effective points we're actually using\n  //helper func check for effective points\n  passesChecks: (point) => do\n    startTime: dates.toSpan.start\n    if (startTime.isDate) startTime=startTime.dateTime(0:00)\n    if (point.isDict and point!=missingPointValue and point.get(\"hisSize\")>0 and point.get(\"hisEnd\")>startTime) return true\n    else false\n  end\n\n  hw_usage:       if (hw_intervalCons.passesChecks) hw_intervalCons.get(\"id\")\n               else if (hw_monthlyCons.passesChecks) hw_monthlyCons.get(\"id\")\n               else missingValue\n  hw_model:       if (hw_intervalModelCons.passesChecks) hw_intervalModelCons.get(\"id\")\n               else if (hw_monthlyModelCons.passesChecks) hw_monthlyModelCons.get(\"id\")\n               else missingValue\n  hw_savings:     if (hw_intervalSavingsCons.passesChecks) hw_intervalSavingsCons.get(\"id\")\n               else if (hw_monthlySavingsCons.passesChecks) hw_monthlySavingsCons.get(\"id\")\n               else missingValue\n  hw_cost :       if (hw_intervalCost.passesChecks) hw_intervalCost.get(\"id\")\n               else if (hw_monthlyCost.passesChecks) hw_monthlyCost.get(\"id\")\n               else missingValue\n  hw_savingsCost: if (hw_intervalSavingsCost.passesChecks) hw_intervalSavingsCost.get(\"id\")\n               else if (hw_monthlySavingsCost.passesChecks) hw_monthlySavingsCost.get(\"id\")\n               else missingValue\n\n  effectivePoints: {hw_usage:hw_usage,\n                    hw_model:hw_model,\n                    hw_savings:hw_savings,\n                    hw_cost:hw_cost,\n                    hw_savingsCost:hw_savingsCost,\n                    }\n\n  //this is just to get the \"effective\" his columns for widgets, ignoring the points that don't matter\n  hisCols: his.cols\n  effectiveHisCols: {ts: \"ts\",\n                     hw_usage:       try hisCols.find(r=>r.meta.get(\"id\")==hw_usage).name catch missingValue,\n                     hw_model:       try hisCols.find(r=>r.meta.get(\"id\")==hw_model).name catch missingValue,\n                     hw_savings:     try hisCols.find(r=>r.meta.get(\"id\")==hw_savings).name catch missingValue,\n                     hw_cost:        try hisCols.find(r=>r.meta.get(\"id\")==hw_cost).name catch missingValue,\n                     hw_savingsCost: try hisCols.find(r=>r.meta.get(\"id\")==hw_savingsCost).name catch missingValue,\n                    }\n\n  //get effective rollups - only for the points that are relevant\n  rollup_hw_usage: if (rollup_hw_intervalCons!=null and rollup_hw_intervalCons!=missingValue) rollup_hw_intervalCons\n                else if (rollup_hw_monthlyCons!=null and rollup_hw_monthlyCons!=missingValue) rollup_hw_monthlyCons\n                else null\n  rollup_hw_model: if (rollup_hw_intervalModelCons!=null and rollup_hw_intervalModelCons!=missingValue) rollup_hw_intervalModelCons\n                else if (rollup_hw_monthlyModelCons!=null and rollup_hw_monthlyModelCons!=missingValue) rollup_hw_monthlyModelCons\n                else null\n  rollup_hw_savings: if (rollup_hw_intervalSavingsCons!=null and rollup_hw_intervalSavingsCons!=missingValue) rollup_hw_intervalSavingsCons\n                else if (rollup_hw_monthlySavingsCons!=null and rollup_hw_monthlySavingsCons!=missingValue) rollup_hw_monthlySavingsCons\n                else null\n  rollup_hw_cost: if (rollup_hw_intervalCost!=null and rollup_hw_intervalCost!=missingValue) rollup_hw_intervalCost\n                else if (rollup_hw_monthlyCost!=null and rollup_hw_monthlyCost!=missingValue) rollup_hw_monthlyCost\n                else null\n  rollup_hw_savingsCost: if (rollup_hw_intervalSavingsCost!=null and rollup_hw_intervalSavingsCost!=missingValue) rollup_hw_intervalSavingsCost\n                else if (rollup_hw_monthlySavingsCost!=null and rollup_hw_monthlySavingsCost!=missingValue) rollup_hw_monthlySavingsCost\n                else null\n  rollup_hw_rate: if (siteRec.has(\"hwRate\")) siteRec.get(\"hwRate\")\n                else if (rollup_hw_monthlyRateCost!=null and rollup_hw_monthlyRateCost!=missingValue) rollup_hw_monthlyRateCost\n                else null\n\n  effectiveRollup: {hw_usage: rollup_hw_usage,\n                    hw_model: rollup_hw_model,\n                    hw_savings: rollup_hw_savings,\n                    hw_cost: rollup_hw_cost,\n                    hw_savingsCost: rollup_hw_savingsCost,\n                    hw_rate:rollup_hw_rate,\n                    }\n  //output mode\n  out: his\n\n  //FULL MODE\n  if (mode==\"full\") do //returns everything\n    meta:  {points:points,\n            hisSummary: hisSummary,\n            rollupSummary:rollup,\n            effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup,\n            effectiveHisCols:effectiveHisCols}\n    return out.addMeta(meta)\n  end\n\n  //EFFECTIVE MODE\n  else do //returns only effective points we care about\n\n    meta:  {effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup}\n\n    //which columns to keep\n    effectiveCols: effectiveHisCols.vals.findAll(v=>v!=missingValue)\n\n    //what to rename the columns to in order to standardize them\n    renameDict: {}\n    if (effectiveCols.contains(\"hw_intervalCons\")) renameDict=renameDict.set(\"hw_intervalCons\",\"hw_usage\")\n    if (effectiveCols.contains(\"hw_monthlyCons\")) renameDict=renameDict.set(\"hw_monthlyCons\",\"hw_usage\")\n    if (effectiveCols.contains(\"hw_intervalModelCons\")) renameDict=renameDict.set(\"hw_intervalModelCons\",\"hw_model\")\n    if (effectiveCols.contains(\"hw_monthlyModelCons\")) renameDict=renameDict.set(\"hw_monthlyModelCons\",\"hw_model\")\n    if (effectiveCols.contains(\"hw_intervalSavingsCons\")) renameDict=renameDict.set(\"hw_intervalSavingsCons\",\"hw_savings\")\n    if (effectiveCols.contains(\"hw_monthlySavingsCons\")) renameDict=renameDict.set(\"hw_monthlySavingsCons\",\"hw_savings\")\n    if (effectiveCols.contains(\"hw_intervalCost\")) renameDict=renameDict.set(\"hw_intervalCost\",\"hw_cost\")\n    if (effectiveCols.contains(\"hw_monthlyCost\")) renameDict=renameDict.set(\"hw_monthlyCost\",\"hw_cost\")\n    if (effectiveCols.contains(\"hw_intervalSavingsCost\")) renameDict=renameDict.set(\"hw_intervalSavingsCost\",\"hw_savingsCost\")\n    if (effectiveCols.contains(\"hw_monthlySavingsCost\")) renameDict=renameDict.set(\"hw_monthlySavingsCost\",\"hw_savingsCost\")\n    if (effectiveCols.contains(\"hw_monthlyRateCost\")) renameDict=renameDict.set(\"hw_monthlyRateCost\",\"hw_rate\")\n\n    return out.keepCols(effectiveCols)\n             .renameCols(renameDict)\n             .addMeta(meta)\n  end\nend",
      "local_src": "//modes:\n// effective will return only the effective points we care about, with specially named columns\n// full will return all points, with more verbose and less general names\n\n(site, dates, opts:{mode:\"effective\"}) => do\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: site->id\n\n  //get opts\n  debug: if(opts.has(\"debug\")) opts.get(\"debug\")\n  rollupInterval: if (opts.has(\"rollupInterval\")) opts.get(\"rollupInterval\") else 1hr\n  mode: if (opts.has(\"mode\")) opts.get(\"mode\") else \"effective\"\n\n  //set defaults\n  missingPointValue: \"missing point\"\n  missingHisValue: \"missing his\"\n  missingValue: \"missing\"\n\n  //lookup site meter\n  meter: read(siteMeter and hot and water and siteRef==siteId and equip, false)\n  if (meter.isNull) return {dis:\"Missing site meter\", action:\"Add a site meter and the required points\"}\n\n  //points\n  pointQueryOpts: {equipRef:meter.get(\"id\"), read:true, checked:false}\n\n  hw_intervalCons: pointQuery(\"hotWater_intervalCons\", pointQueryOpts)\n  hw_intervalModelCons:  pointQuery(\"hotWater_intervalModelCons\", pointQueryOpts)\n  hw_intervalSavingsCons:  pointQuery(\"hotWater_intervalSavingsCons\", pointQueryOpts)\n  hw_intervalSavingsCost: pointQuery(\"hotWater_intervalSavingsCost\", pointQueryOpts)\n  hw_intervalCost: pointQuery(\"hotWater_intervalCost\", pointQueryOpts)\n\n  hw_monthlyCons: pointQuery(\"hotWater_monthlyCons\", pointQueryOpts)\n  hw_monthlyModelCons: pointQuery(\"hotWater_monthlyModelCons\", pointQueryOpts)\n  hw_monthlySavingsCons: pointQuery(\"hotWater_monthlySavingsCons\", pointQueryOpts)\n  hw_monthlySavingsCost: pointQuery(\"hotWater_monthlySavingsCost\", pointQueryOpts)\n  hw_monthlyCost: pointQuery(\"hotWater_monthlyCost\", pointQueryOpts)\n\n  hw_monthlyRateCost:  pointQuery(\"hotWater_monthlyRateCost\", pointQueryOpts)\n\n\n  points: {hw_intervalCons        : try hw_intervalCons.get(\"id\") catch missingPointValue,\n           hw_intervalModelCons   : try hw_intervalModelCons.get(\"id\") catch missingPointValue,\n           hw_intervalSavingsCons : try hw_intervalSavingsCons.get(\"id\") catch missingPointValue,\n           hw_intervalSavingsCost : try hw_intervalSavingsCost.get(\"id\") catch missingPointValue,\n           hw_intervalCost        : try hw_intervalCost.get(\"id\") catch missingPointValue,\n           hw_monthlyCons         : try hw_monthlyCons.get(\"id\") catch missingPointValue,\n           hw_monthlyModelCons    : try hw_monthlyModelCons.get(\"id\") catch missingPointValue,\n           hw_monthlySavingsCons  : try hw_monthlySavingsCons.get(\"id\") catch missingPointValue,\n           hw_monthlySavingsCost  : try hw_monthlySavingsCost.get(\"id\") catch missingPointValue,\n           hw_monthlyRateCost     : try hw_monthlyRateCost.get(\"id\") catch missingPointValue,\n           hw_monthlyCost         : try hw_monthlyCost.get(\"id\") catch missingPointValue\n          }\n  if(debug==\"points\") return points\n  if (points.vals.all(v=>v==missingPointValue)) return {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n\n  //define col meta options on points\n  if (hw_intervalCons!=null and hw_intervalCons!=missingValue) hw_intervalCons= hw_intervalCons                               .merge({dis:\"Hot Water Consumption\", chartGroup:\"hw\", color:\"purple\"})\n  if (hw_intervalModelCons!=null and hw_intervalModelCons!=missingValue) hw_intervalModelCons=    hw_intervalModelCons        .merge({dis:\"Hot Water Model\", chartGroup:\"hw\", color:\"purple\", strokeDasharray:\"1,1\"})\n  if (hw_intervalSavingsCons!=null and hw_intervalSavingsCons!=missingValue) hw_intervalSavingsCons=  hw_intervalSavingsCons  .merge({})\n  if (hw_intervalSavingsCost!=null and hw_intervalSavingsCost!=missingValue) hw_intervalSavingsCost= hw_intervalSavingsCost   .merge({})\n  if (hw_intervalCost!=null and hw_intervalCost!=missingValue) hw_intervalCost= hw_intervalCost                               .merge({})\n\n  if (hw_monthlyCons!=null and hw_monthlyCons!=missingValue) hw_monthlyCons= hw_monthlyCons                                   .merge({dis:\"Hot Water Consumption\", chartGroup:\"hw\", color:\"purple\"})\n  if (hw_monthlyModelCons!=null and hw_monthlyModelCons!=missingValue) hw_monthlyModelCons= hw_monthlyModelCons               .merge({dis:\"Hot Water Model\", chartGroup:\"hw\", color:\"purple\", strokeDasharray:\"1,1\"})\n  if (hw_monthlySavingsCons!=null and hw_monthlySavingsCons!=missingValue) hw_monthlySavingsCons= hw_monthlySavingsCons       .merge({})\n  if (hw_monthlySavingsCost!=null and hw_monthlySavingsCost!=missingValue) hw_monthlySavingsCost= hw_monthlySavingsCost       .merge({})\n  if (hw_monthlyCost!=null and hw_monthlyCost!=missingValue) hw_monthlyCost= hw_monthlyCost                                   .merge({})\n\n  if (hw_monthlyRateCost!=null and hw_monthlyRateCost!=missingValue) hw_monthlyRateCost=  hw_monthlyRateCost                  .merge({})\n\n\n  //get his\n  his_hw_intervalCons: try hw_intervalCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_intervalCons\") catch null\n  his_hw_intervalModelCons: try hw_intervalModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_intervalModelCons\") catch null\n  his_hw_intervalSavingsCons: try hw_intervalSavingsCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).renameCol(\"v0\",\"hw_intervalSavingsCons\") catch null\n  his_hw_intervalSavingsCost: try hw_intervalSavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_intervalSavingsCost\") catch null\n  his_hw_intervalCost: try hw_intervalCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_intervalCost\") catch null\n\n  his_hw_monthlyCons: try hw_monthlyCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlyCons\") catch null\n  his_hw_monthlyModelCons: try hw_monthlyModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlyModelCons\") catch null\n  his_hw_monthlySavingsCons: try hw_monthlySavingsCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlySavingsCons\") catch null\n  his_hw_monthlySavingsCost: try hw_monthlySavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlySavingsCost\") catch null\n  his_hw_monthlyCost: try hw_monthlyCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlyCost\") catch null\n\n  his_hw_monthlyRateCost: try hw_monthlyRateCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"hw_monthlyRateCost\") catch null\n\n  //pull together his for output\n  his: [his_hw_intervalCons,\n        his_hw_intervalModelCons,\n        his_hw_intervalSavingsCons,\n        his_hw_intervalSavingsCost,\n        his_hw_intervalCost,\n\n        his_hw_monthlyCons,\n        his_hw_monthlyModelCons,\n        his_hw_monthlySavingsCons,\n        his_hw_monthlySavingsCost,\n        his_hw_monthlyCost,\n\n        his_hw_monthlyRateCost\n\n        ].findAll(v=>v!=null).hisJoin.hisRollup(avg, 1hr)\n\n  //this his summary is helpful when debugging, it shows how many data points exist within this span\n  hisSummary: {  his_hw_intervalCons        :  try his_hw_intervalCons.size catch missingHisValue,\n                 his_hw_intervalModelCons   :  try his_hw_intervalModelCons.size catch missingHisValue,\n                 his_hw_intervalSavingsCons :  try his_hw_intervalSavingsCons.size catch missingHisValue,\n                 his_hw_intervalSavingsCost :  try his_hw_intervalSavingsCost.size catch missingHisValue,\n                 his_hw_intervalCost        :  try his_hw_intervalCost.size catch missingHisValue,\n                 his_hw_monthlyCons         :  try his_hw_monthlyCons.size catch missingHisValue,\n                 his_hw_monthlyModelCons    :  try his_hw_monthlyModelCons.size catch missingHisValue,\n                 his_hw_monthlySavingsCons  :  try his_hw_monthlySavingsCons.size catch missingHisValue,\n                 his_hw_monthlySavingsCost  :  try his_hw_monthlySavingsCost.size catch missingHisValue,\n                 his_hw_monthlyCost         :  try his_hw_monthlyCost.size catch missingHisValue,\n                 his_hw_monthlyRateCost     :  try his_hw_monthlyRateCost.size catch missingHisValue,\n          }\n  if (debug==\"his\") return hisSummary\n\n  //rollup data into summary values we can use quickly in widgets\n  rollup_hw_intervalCons: try his_hw_intervalCons.hisRollup(avg,1hr).hisMap(v=>v.to(\"kBTU/h\")).hisRollup(sum,\"*\").first.get(\"hw_intervalCons\") catch missingValue\n  rollup_hw_intervalModelCons:    try his_hw_intervalModelCons.hisRollup(avg,1hr).hisMap(v=>v.to(\"kBTU/h\")).hisRollup(sum,\"*\").first.get(\"hw_intervalModelCons\") catch missingValue\n  rollup_hw_intervalSavingsCons:  try his_hw_intervalSavingsCons.hisRollup(avg,1hr).hisMap(v=>v.to(\"kBTU/h\")).hisRollup(sum,\"*\").first.get(\"hw_intervalSavingsCons\") catch missingValue\n  rollup_hw_intervalSavingsCost:   try his_hw_intervalSavingsCost.hisRollup(sum,\"*\").first.get(\"hw_intervalSavingsCost\") catch missingValue\n  rollup_hw_intervalCost:   try his_hw_intervalCost.hisRollup(sum,\"*\").first.get(\"hw_intervalCost\") catch missingValue\n\n  rollup_hw_monthlyCons :   try his_hw_monthlyCons.hisRollup(sum,\"*\").first.get(\"hw_monthlyCons\") catch missingValue\n  rollup_hw_monthlyModelCons:   try his_hw_monthlyModelCons.hisRollup(sum,\"*\").first.get(\"hw_monthlyModelCons\") catch missingValue\n  rollup_hw_monthlySavingsCons:   try his_hw_monthlySavingsCons.hisRollup(sum,\"*\").first.get(\"hw_monthlySavingsCons\") catch missingValue\n  rollup_hw_monthlySavingsCost:   try his_hw_monthlySavingsCost.hisRollup(sum,\"*\").first.get(\"hw_monthlySavingsCost\") catch missingValue\n  rollup_hw_monthlyCost:   try his_hw_monthlyCost.hisRollup(sum,\"*\").first.get(\"hw_monthlyCost\") catch missingValue\n\n  rollup_hw_monthlyRateCost:   try his_hw_monthlyRateCost.hisRollup(avg,\"*\").first.get(\"hw_monthlyRateCost\") catch missingValue\n\n  rollup:  {rollup_hw_intervalCons: rollup_hw_intervalCons,\n            rollup_hw_intervalModelCons: rollup_hw_intervalModelCons,\n            rollup_hw_intervalSavingsCons: rollup_hw_intervalSavingsCons,\n            rollup_hw_intervalSavingsCost: rollup_hw_intervalSavingsCost,\n            rollup_hw_intervalCost: rollup_hw_intervalCost,\n\n            rollup_hw_monthlyCons: rollup_hw_monthlyCons,\n            rollup_hw_monthlyModelCons: rollup_hw_monthlyModelCons,\n            rollup_hw_monthlySavingsCons: rollup_hw_monthlySavingsCons,\n            rollup_hw_monthlySavingsCost: rollup_hw_monthlySavingsCost,\n            rollup_hw_monthlyCost: rollup_hw_monthlyCost,\n\n            rollup_hw_monthlyRateCost: rollup_hw_monthlyRateCost\n            }\n\n  //get effective points - this will help when we want to ignore points that don't matter and just get the effective points we're actually using\n  //helper func check for effective points\n  passesChecks: (point) => do\n    startTime: dates.toSpan.start\n    if (startTime.isDate) startTime=startTime.dateTime(0:00)\n    if (point.isDict and point!=missingPointValue and point.get(\"hisSize\")>0 and point.get(\"hisEnd\")>startTime) return true\n    else false\n  end\n\n  hw_usage:       if (hw_intervalCons.passesChecks) hw_intervalCons.get(\"id\")\n               else if (hw_monthlyCons.passesChecks) hw_monthlyCons.get(\"id\")\n               else missingValue\n  hw_model:       if (hw_intervalModelCons.passesChecks) hw_intervalModelCons.get(\"id\")\n               else if (hw_monthlyModelCons.passesChecks) hw_monthlyModelCons.get(\"id\")\n               else missingValue\n  hw_savings:     if (hw_intervalSavingsCons.passesChecks) hw_intervalSavingsCons.get(\"id\")\n               else if (hw_monthlySavingsCons.passesChecks) hw_monthlySavingsCons.get(\"id\")\n               else missingValue\n  hw_cost :       if (hw_intervalCost.passesChecks) hw_intervalCost.get(\"id\")\n               else if (hw_monthlyCost.passesChecks) hw_monthlyCost.get(\"id\")\n               else missingValue\n  hw_savingsCost: if (hw_intervalSavingsCost.passesChecks) hw_intervalSavingsCost.get(\"id\")\n               else if (hw_monthlySavingsCost.passesChecks) hw_monthlySavingsCost.get(\"id\")\n               else missingValue\n\n  effectivePoints: {hw_usage:hw_usage,\n                    hw_model:hw_model,\n                    hw_savings:hw_savings,\n                    hw_cost:hw_cost,\n                    hw_savingsCost:hw_savingsCost,\n                    }\n\n  //this is just to get the \"effective\" his columns for widgets, ignoring the points that don't matter\n  hisCols: his.cols\n  effectiveHisCols: {ts: \"ts\",\n                     hw_usage:       try hisCols.find(r=>r.meta.get(\"id\")==hw_usage).name catch missingValue,\n                     hw_model:       try hisCols.find(r=>r.meta.get(\"id\")==hw_model).name catch missingValue,\n                     hw_savings:     try hisCols.find(r=>r.meta.get(\"id\")==hw_savings).name catch missingValue,\n                     hw_cost:        try hisCols.find(r=>r.meta.get(\"id\")==hw_cost).name catch missingValue,\n                     hw_savingsCost: try hisCols.find(r=>r.meta.get(\"id\")==hw_savingsCost).name catch missingValue,\n                    }\n\n  //get effective rollups - only for the points that are relevant\n  rollup_hw_usage: if (rollup_hw_intervalCons!=null and rollup_hw_intervalCons!=missingValue) rollup_hw_intervalCons\n                else if (rollup_hw_monthlyCons!=null and rollup_hw_monthlyCons!=missingValue) rollup_hw_monthlyCons\n                else null\n  rollup_hw_model: if (rollup_hw_intervalModelCons!=null and rollup_hw_intervalModelCons!=missingValue) rollup_hw_intervalModelCons\n                else if (rollup_hw_monthlyModelCons!=null and rollup_hw_monthlyModelCons!=missingValue) rollup_hw_monthlyModelCons\n                else null\n  rollup_hw_savings: if (rollup_hw_intervalSavingsCons!=null and rollup_hw_intervalSavingsCons!=missingValue) rollup_hw_intervalSavingsCons\n                else if (rollup_hw_monthlySavingsCons!=null and rollup_hw_monthlySavingsCons!=missingValue) rollup_hw_monthlySavingsCons\n                else null\n  rollup_hw_cost: if (rollup_hw_intervalCost!=null and rollup_hw_intervalCost!=missingValue) rollup_hw_intervalCost\n                else if (rollup_hw_monthlyCost!=null and rollup_hw_monthlyCost!=missingValue) rollup_hw_monthlyCost\n                else null\n  rollup_hw_savingsCost: if (rollup_hw_intervalSavingsCost!=null and rollup_hw_intervalSavingsCost!=missingValue) rollup_hw_intervalSavingsCost\n                else if (rollup_hw_monthlySavingsCost!=null and rollup_hw_monthlySavingsCost!=missingValue) rollup_hw_monthlySavingsCost\n                else null\n  rollup_hw_rate: if (siteRec.has(\"hwRate\")) siteRec.get(\"hwRate\")\n                else if (rollup_hw_monthlyRateCost!=null and rollup_hw_monthlyRateCost!=missingValue) rollup_hw_monthlyRateCost\n                else null\n\n  effectiveRollup: {hw_usage: rollup_hw_usage,\n                    hw_model: rollup_hw_model,\n                    hw_savings: rollup_hw_savings,\n                    hw_cost: rollup_hw_cost,\n                    hw_savingsCost: rollup_hw_savingsCost,\n                    hw_rate:rollup_hw_rate,\n                    }\n  //output mode\n  out: his\n\n  //FULL MODE\n  if (mode==\"full\") do //returns everything\n    meta:  {points:points,\n            hisSummary: hisSummary,\n            rollupSummary:rollup,\n            effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup,\n            effectiveHisCols:effectiveHisCols}\n    return out.addMeta(meta)\n  end\n\n  //EFFECTIVE MODE\n  else do //returns only effective points we care about\n\n    meta:  {effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup}\n\n    //which columns to keep\n    effectiveCols: effectiveHisCols.vals.findAll(v=>v!=missingValue)\n\n    //what to rename the columns to in order to standardize them\n    renameDict: {}\n    if (effectiveCols.contains(\"hw_intervalCons\")) renameDict=renameDict.set(\"hw_intervalCons\",\"hw_usage\")\n    if (effectiveCols.contains(\"hw_monthlyCons\")) renameDict=renameDict.set(\"hw_monthlyCons\",\"hw_usage\")\n    if (effectiveCols.contains(\"hw_intervalModelCons\")) renameDict=renameDict.set(\"hw_intervalModelCons\",\"hw_model\")\n    if (effectiveCols.contains(\"hw_monthlyModelCons\")) renameDict=renameDict.set(\"hw_monthlyModelCons\",\"hw_model\")\n    if (effectiveCols.contains(\"hw_intervalSavingsCons\")) renameDict=renameDict.set(\"hw_intervalSavingsCons\",\"hw_savings\")\n    if (effectiveCols.contains(\"hw_monthlySavingsCons\")) renameDict=renameDict.set(\"hw_monthlySavingsCons\",\"hw_savings\")\n    if (effectiveCols.contains(\"hw_intervalCost\")) renameDict=renameDict.set(\"hw_intervalCost\",\"hw_cost\")\n    if (effectiveCols.contains(\"hw_monthlyCost\")) renameDict=renameDict.set(\"hw_monthlyCost\",\"hw_cost\")\n    if (effectiveCols.contains(\"hw_intervalSavingsCost\")) renameDict=renameDict.set(\"hw_intervalSavingsCost\",\"hw_savingsCost\")\n    if (effectiveCols.contains(\"hw_monthlySavingsCost\")) renameDict=renameDict.set(\"hw_monthlySavingsCost\",\"hw_savingsCost\")\n    if (effectiveCols.contains(\"hw_monthlyRateCost\")) renameDict=renameDict.set(\"hw_monthlyRateCost\",\"hw_rate\")\n\n    return out.keepCols(effectiveCols)\n             .renameCols(renameDict)\n             .addMeta(meta)\n  end\nend",
      "diff": "@@ -7,7 +7,6 @@   //normalize inputs\n   siteRec: site.toRec\n   siteId: site->id\n-  dates = dates.toSpan\n \n   //get opts\n   debug: if(opts.has(\"debug\")) opts.get(\"debug\")\n@@ -57,14 +56,14 @@   if (points.vals.all(v=>v==missingPointValue)) return {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n \n   //define col meta options on points\n-  if (hw_intervalCons!=null and hw_intervalCons!=missingValue) hw_intervalCons= hw_intervalCons                               .merge({dis:\"Hot Water Consumption\", chartGroup:\"hw\", color:\"tomato\"})\n-  if (hw_intervalModelCons!=null and hw_intervalModelCons!=missingValue) hw_intervalModelCons=    hw_intervalModelCons        .merge({dis:\"Hot Water Model\", chartGroup:\"hw\", color:\"tomato\", strokeDasharray:\"1,1\"})\n+  if (hw_intervalCons!=null and hw_intervalCons!=missingValue) hw_intervalCons= hw_intervalCons                               .merge({dis:\"Hot Water Consumption\", chartGroup:\"hw\", color:\"purple\"})\n+  if (hw_intervalModelCons!=null and hw_intervalModelCons!=missingValue) hw_intervalModelCons=    hw_intervalModelCons        .merge({dis:\"Hot Water Model\", chartGroup:\"hw\", color:\"purple\", strokeDasharray:\"1,1\"})\n   if (hw_intervalSavingsCons!=null and hw_intervalSavingsCons!=missingValue) hw_intervalSavingsCons=  hw_intervalSavingsCons  .merge({})\n   if (hw_intervalSavingsCost!=null and hw_intervalSavingsCost!=missingValue) hw_intervalSavingsCost= hw_intervalSavingsCost   .merge({})\n   if (hw_intervalCost!=null and hw_intervalCost!=missingValue) hw_intervalCost= hw_intervalCost                               .merge({})\n \n-  if (hw_monthlyCons!=null and hw_monthlyCons!=missingValue) hw_monthlyCons= hw_monthlyCons                                   .merge({dis:\"Hot Water Consumption\", chartGroup:\"hw\", color:\"tomato\"})\n-  if (hw_monthlyModelCons!=null and hw_monthlyModelCons!=missingValue) hw_monthlyModelCons= hw_monthlyModelCons               .merge({dis:\"Hot Water Model\", chartGroup:\"hw\", color:\"tomato\", strokeDasharray:\"1,1\"})\n+  if (hw_monthlyCons!=null and hw_monthlyCons!=missingValue) hw_monthlyCons= hw_monthlyCons                                   .merge({dis:\"Hot Water Consumption\", chartGroup:\"hw\", color:\"purple\"})\n+  if (hw_monthlyModelCons!=null and hw_monthlyModelCons!=missingValue) hw_monthlyModelCons= hw_monthlyModelCons               .merge({dis:\"Hot Water Model\", chartGroup:\"hw\", color:\"purple\", strokeDasharray:\"1,1\"})\n   if (hw_monthlySavingsCons!=null and hw_monthlySavingsCons!=missingValue) hw_monthlySavingsCons= hw_monthlySavingsCons       .merge({})\n   if (hw_monthlySavingsCost!=null and hw_monthlySavingsCost!=missingValue) hw_monthlySavingsCost= hw_monthlySavingsCost       .merge({})\n   if (hw_monthlyCost!=null and hw_monthlyCost!=missingValue) hw_monthlyCost= hw_monthlyCost                                   .merge({})\n"
    },
    {
      "name": "getSiteKickoffDate",
      "pod": "kwLinkMbcxExt",
      "pod_src": "// JG 3/25/2024     If projectKickoffDate is present on site and is date, use that as kickoff date\n\n(site) => do\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n\n  //opts\n  missingVal: today()\n\n  arcs: readAll(arc and projectSite==siteId)\n  if (arcs.size==null) return missingVal\n\n  types: \"active\".split(\",\")\n\n  //timeline where each row is an arc and its transition dates\n  timeline: arcs.map row => do\n\n    arc: arcRead(row->id)\n    audits: arc.findAll(r => r.has(\"audit\"))\n\n    out: {id: row->id}\n    types.each type => do\n      if (type==\"new\") out=out.set(type, row.get(\"created\"))\n      else out=out.set(type, try audits.find(r => r.get(\"audit\").get(\"new\").get(\"projectTrackerState\")==type).get(\"ts\") catch null)\n    end\n    return out\n  end\n\n  resolved: timeline.filter(active)\n  if (resolved.size<1) return missingVal\n\n  return resolved.sort(\"active\").first.get(\"active\").date\n\nend",
      "local_src": "// JG 3/25/2024     If projectKickoffDate is present on site and is date, use that as kickoff date\n\n(site) => do\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n\n  //opts\n  missingVal: today()\n\n  arcs: readAll(arc and projectSite==siteId)\n  if (arcs.size==null) return missingVal\n\n  types: \"active\".split(\",\")\n  //return types\n  //return arcs\n  //timeline where each row is an arc and its transition dates\n  timeline: arcs.map row => do\n\n    arc: arcRead(row->id)\n    audits: arc.findAll(r => r.has(\"audit\"))\n\n    out: {id: row->id}\n    types.each type => do\n      if (type==\"new\") out=out.set(type, row.get(\"estCompleteDate\"))\n      else out=out.set(type, try audits.find(r => r.get(\"audit\").get(\"new\").get(\"projectTrackerState\")==type).get(\"audit\").get(\"new\").get(\"estCompleteDate\") catch null)\n    end\n    return out\n  end\n  //return timeline\n  resolved: timeline.filter(active)\n  //return resolved\n  if (resolved.size<1) return missingVal\n  return resolved.sort(\"active\").first.get(\"active\")//.date\n\nend",
      "diff": "@@ -13,7 +13,8 @@   if (arcs.size==null) return missingVal\n \n   types: \"active\".split(\",\")\n-\n+  //return types\n+  //return arcs\n   //timeline where each row is an arc and its transition dates\n   timeline: arcs.map row => do\n \n@@ -22,15 +23,15 @@ \n     out: {id: row->id}\n     types.each type => do\n-      if (type==\"new\") out=out.set(type, row.get(\"created\"))\n-      else out=out.set(type, try audits.find(r => r.get(\"audit\").get(\"new\").get(\"projectTrackerState\")==type).get(\"ts\") catch null)\n+      if (type==\"new\") out=out.set(type, row.get(\"estCompleteDate\"))\n+      else out=out.set(type, try audits.find(r => r.get(\"audit\").get(\"new\").get(\"projectTrackerState\")==type).get(\"audit\").get(\"new\").get(\"estCompleteDate\") catch null)\n     end\n     return out\n   end\n-\n+  //return timeline\n   resolved: timeline.filter(active)\n+  //return resolved\n   if (resolved.size<1) return missingVal\n-\n-  return resolved.sort(\"active\").first.get(\"active\").date\n+  return resolved.sort(\"active\").first.get(\"active\")//.date\n \n end"
    },
    {
      "name": "getSteamMeterSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "//modes:\n// effective will return only the effective points we care about, with specially named columns\n// full will return all points, with more verbose and less general names\n\n(site, dates, opts:{mode:\"effective\"}) => do\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: site->id\n  dates = dates.toSpan\n\n  //get opts\n  debug: if(opts.has(\"debug\")) opts.get(\"debug\")\n  rollupInterval: if (opts.has(\"rollupInterval\")) opts.get(\"rollupInterval\") else 1hr\n  mode: if (opts.has(\"mode\")) opts.get(\"mode\") else \"effective\"\n\n  //set defaults\n  missingPointValue: \"missing point\"\n  missingHisValue: \"missing his\"\n  missingValue: \"missing\"\n\n  //lookup site meter\n  meter: read(siteMeter and steam and siteRef==siteId and equip, false)\n  if (meter.isNull) return {dis:\"Missing site meter\", action:\"Add a site meter and the required points\"}\n\n  //points\n  pointQueryOpts: {equipRef:meter.get(\"id\"), read:true, checked:false}\n\n  steam_intervalCons: pointQuery(\"steam_intervalCons\", pointQueryOpts)\n  steam_intervalModelCons:  pointQuery(\"steam_intervalModelCons\", pointQueryOpts)\n  steam_intervalSavingsCons:  pointQuery(\"steam_intervalSavingsCons\", pointQueryOpts)\n  steam_intervalSavingsCost: pointQuery(\"steam_intervalSavingsCost\", pointQueryOpts)\n  steam_intervalCost: pointQuery(\"steam_intervalCost\", pointQueryOpts)\n\n  steam_monthlyCons: pointQuery(\"steam_monthlyCons\", pointQueryOpts)\n  steam_monthlyModelCons: pointQuery(\"steam_monthlyModelCons\", pointQueryOpts)\n  steam_monthlySavingsCons: pointQuery(\"steam_monthlySavingsCons\", pointQueryOpts)\n  steam_monthlySavingsCost: pointQuery(\"steam_monthlySavingsCost\", pointQueryOpts)\n  steam_monthlyCost: pointQuery(\"steam_monthlyCost\", pointQueryOpts)\n\n  steam_monthlyRateCost:  pointQuery(\"steam_monthlyRateCost\", pointQueryOpts)\n\n\n  points: {steam_intervalCons        : try steam_intervalCons.get(\"id\") catch missingPointValue,\n           steam_intervalModelCons   : try steam_intervalModelCons.get(\"id\") catch missingPointValue,\n           steam_intervalSavingsCons : try steam_intervalSavingsCons.get(\"id\") catch missingPointValue,\n           steam_intervalSavingsCost : try steam_intervalSavingsCost.get(\"id\") catch missingPointValue,\n           steam_intervalCost        : try steam_intervalCost.get(\"id\") catch missingPointValue,\n           steam_monthlyCons         : try steam_monthlyCons.get(\"id\") catch missingPointValue,\n           steam_monthlyModelCons    : try steam_monthlyModelCons.get(\"id\") catch missingPointValue,\n           steam_monthlySavingsCons  : try steam_monthlySavingsCons.get(\"id\") catch missingPointValue,\n           steam_monthlySavingsCost  : try steam_monthlySavingsCost.get(\"id\") catch missingPointValue,\n           steam_monthlyRateCost     : try steam_monthlyRateCost.get(\"id\") catch missingPointValue,\n           steam_monthlyCost         : try steam_monthlyCost.get(\"id\") catch missingPointValue\n          }\n  if(debug==\"points\") return points\n  if (points.vals.all(v=>v==missingPointValue)) return {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n\n  //define col meta options on points\n  if (steam_intervalCons!=null and steam_intervalCons!=missingValue) steam_intervalCons= steam_intervalCons                               .merge({dis:\"Steam Consumption\", chartGroup:\"steam\", color:\"slateBlue\"})\n  if (steam_intervalModelCons!=null and steam_intervalModelCons!=missingValue) steam_intervalModelCons=    steam_intervalModelCons        .merge({dis:\"Steam Model\", chartGroup:\"steam\", color:\"slateBlue\", strokeDasharray:\"1,1\"})\n  if (steam_intervalSavingsCons!=null and steam_intervalSavingsCons!=missingValue) steam_intervalSavingsCons=  steam_intervalSavingsCons  .merge({})\n  if (steam_intervalSavingsCost!=null and steam_intervalSavingsCost!=missingValue) steam_intervalSavingsCost= steam_intervalSavingsCost   .merge({})\n  if (steam_intervalCost!=null and steam_intervalCost!=missingValue) steam_intervalCost= steam_intervalCost                               .merge({})\n\n  if (steam_monthlyCons!=null and steam_monthlyCons!=missingValue) steam_monthlyCons= steam_monthlyCons                                   .merge({dis:\"Steam Consumption\", chartGroup:\"steam\", color:\"slateBlue\"})\n  if (steam_monthlyModelCons!=null and steam_monthlyModelCons!=missingValue) steam_monthlyModelCons= steam_monthlyModelCons               .merge({dis:\"Steam Model\", chartGroup:\"steam\", color:\"slateBlue\", strokeDasharray:\"1,1\"})\n  if (steam_monthlySavingsCons!=null and steam_monthlySavingsCons!=missingValue) steam_monthlySavingsCons= steam_monthlySavingsCons       .merge({})\n  if (steam_monthlySavingsCost!=null and steam_monthlySavingsCost!=missingValue) steam_monthlySavingsCost= steam_monthlySavingsCost       .merge({})\n  if (steam_monthlyCost!=null and steam_monthlyCost!=missingValue) steam_monthlyCost= steam_monthlyCost                                   .merge({})\n\n  if (steam_monthlyRateCost!=null and steam_monthlyRateCost!=missingValue) steam_monthlyRateCost=  steam_monthlyRateCost                  .merge({})\n\n\n  //get his\n  his_steam_intervalCons: try steam_intervalCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_intervalCons\") catch null\n  his_steam_intervalModelCons: try steam_intervalModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_intervalModelCons\") catch null\n  his_steam_intervalSavingsCons: try steam_intervalSavingsCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).renameCol(\"v0\",\"steam_intervalSavingsCons\") catch null\n  his_steam_intervalSavingsCost: try steam_intervalSavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_intervalSavingsCost\") catch null\n  his_steam_intervalCost: try steam_intervalCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_intervalCost\") catch null\n\n  his_steam_monthlyCons: try steam_monthlyCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlyCons\") catch null\n  his_steam_monthlyModelCons: try steam_monthlyModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlyModelCons\") catch null\n  his_steam_monthlySavingsCons: try steam_monthlySavingsCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlySavingsCons\") catch null\n  his_steam_monthlySavingsCost: try steam_monthlySavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlySavingsCost\") catch null\n  his_steam_monthlyCost: try steam_monthlyCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlyCost\") catch null\n\n  his_steam_monthlyRateCost: try steam_monthlyRateCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlyRateCost\") catch null\n\n  //pull together his for output\n  his: [his_steam_intervalCons,\n        his_steam_intervalModelCons,\n        his_steam_intervalSavingsCons,\n        his_steam_intervalSavingsCost,\n        his_steam_intervalCost,\n\n        his_steam_monthlyCons,\n        his_steam_monthlyModelCons,\n        his_steam_monthlySavingsCons,\n        his_steam_monthlySavingsCost,\n        his_steam_monthlyCost,\n\n        his_steam_monthlyRateCost\n\n        ].findAll(v=>v!=null).hisJoin.hisRollup(avg, 1hr)\n\n  //this his summary is helpful when debugging, it shows how many data points exist within this span\n  hisSummary: {  his_steam_intervalCons        :  try his_steam_intervalCons.size catch missingHisValue,\n                 his_steam_intervalModelCons   :  try his_steam_intervalModelCons.size catch missingHisValue,\n                 his_steam_intervalSavingsCons :  try his_steam_intervalSavingsCons.size catch missingHisValue,\n                 his_steam_intervalSavingsCost :  try his_steam_intervalSavingsCost.size catch missingHisValue,\n                 his_steam_intervalCost        :  try his_steam_intervalCost.size catch missingHisValue,\n                 his_steam_monthlyCons         :  try his_steam_monthlyCons.size catch missingHisValue,\n                 his_steam_monthlyModelCons    :  try his_steam_monthlyModelCons.size catch missingHisValue,\n                 his_steam_monthlySavingsCons  :  try his_steam_monthlySavingsCons.size catch missingHisValue,\n                 his_steam_monthlySavingsCost  :  try his_steam_monthlySavingsCost.size catch missingHisValue,\n                 his_steam_monthlyCost         :  try his_steam_monthlyCost.size catch missingHisValue,\n                 his_steam_monthlyRateCost     :  try his_steam_monthlyRateCost.size catch missingHisValue,\n          }\n  if (debug==\"his\") return hisSummary\n\n  //rollup data into summary values we can use quickly in widgets\n  rollup_steam_intervalCons: try his_steam_intervalCons.hisRollup(avg,1hr).hisRollup(sum,\"*\").first.get(\"steam_intervalCons\") catch missingValue\n  rollup_steam_intervalModelCons:    try his_steam_intervalModelCons.hisRollup(avg,1hr).hisRollup(sum,\"*\").first.get(\"steam_intervalModelCons\") catch missingValue\n  rollup_steam_intervalSavingsCons:  try his_steam_intervalSavingsCons.hisRollup(avg,1hr).hisRollup(sum,\"*\").first.get(\"steam_intervalSavingsCons\") catch missingValue\n  rollup_steam_intervalSavingsCost:   try his_steam_intervalSavingsCost.hisRollup(sum,\"*\").first.get(\"steam_intervalSavingsCost\") catch missingValue\n  rollup_steam_intervalCost:   try his_steam_intervalCost.hisRollup(sum,\"*\").first.get(\"steam_intervalCost\") catch missingValue\n\n  rollup_steam_monthlyCons :   try his_steam_monthlyCons.hisRollup(sum,\"*\").first.get(\"steam_monthlyCons\") catch missingValue\n  rollup_steam_monthlyModelCons:   try his_steam_monthlyModelCons.hisRollup(sum,\"*\").first.get(\"steam_monthlyModelCons\") catch missingValue\n  rollup_steam_monthlySavingsCons:   try his_steam_monthlySavingsCons.hisRollup(sum,\"*\").first.get(\"steam_monthlySavingsCons\") catch missingValue\n  rollup_steam_monthlySavingsCost:   try his_steam_monthlySavingsCost.hisRollup(sum,\"*\").first.get(\"steam_monthlySavingsCost\") catch missingValue\n  rollup_steam_monthlyCost:   try his_steam_monthlyCost.hisRollup(sum,\"*\").first.get(\"steam_monthlyCost\") catch missingValue\n\n  rollup_steam_monthlyRateCost:   try his_steam_monthlyRateCost.hisRollup(avg,\"*\").first.get(\"steam_monthlyRateCost\") catch missingValue\n\n  rollup:  {rollup_steam_intervalCons: rollup_steam_intervalCons,\n            rollup_steam_intervalModelCons: rollup_steam_intervalModelCons,\n            rollup_steam_intervalSavingsCons: rollup_steam_intervalSavingsCons,\n            rollup_steam_intervalSavingsCost: rollup_steam_intervalSavingsCost,\n            rollup_steam_intervalCost: rollup_steam_intervalCost,\n\n            rollup_steam_monthlyCons: rollup_steam_monthlyCons,\n            rollup_steam_monthlyModelCons: rollup_steam_monthlyModelCons,\n            rollup_steam_monthlySavingsCons: rollup_steam_monthlySavingsCons,\n            rollup_steam_monthlySavingsCost: rollup_steam_monthlySavingsCost,\n            rollup_steam_monthlyCost: rollup_steam_monthlyCost,\n\n            rollup_steam_monthlyRateCost: rollup_steam_monthlyRateCost\n            }\n\n  //get effective points - this will help when we want to ignore points that don't matter and just get the effective points we're actually using\n  //helper func check for effective points\n  passesChecks: (point) => do\n    startTime: dates.toSpan.start\n    if (startTime.isDate) startTime=startTime.dateTime(0:00)\n    if (point.isDict and point!=missingPointValue and point.get(\"hisSize\")>0 and point.get(\"hisEnd\")>startTime) return true\n    else false\n  end\n\n  steam_usage:       if (steam_intervalCons.passesChecks) steam_intervalCons.get(\"id\")\n               else if (steam_monthlyCons.passesChecks) steam_monthlyCons.get(\"id\")\n               else missingValue\n  steam_model:       if (steam_intervalModelCons.passesChecks) steam_intervalModelCons.get(\"id\")\n               else if (steam_monthlyModelCons.passesChecks) steam_monthlyModelCons.get(\"id\")\n               else missingValue\n  steam_savings:     if (steam_intervalSavingsCons.passesChecks) steam_intervalSavingsCons.get(\"id\")\n               else if (steam_monthlySavingsCons.passesChecks) steam_monthlySavingsCons.get(\"id\")\n               else missingValue\n  steam_cost :       if (steam_intervalCost.passesChecks) steam_intervalCost.get(\"id\")\n               else if (steam_monthlyCost.passesChecks) steam_monthlyCost.get(\"id\")\n               else missingValue\n  steam_savingsCost: if (steam_intervalSavingsCost.passesChecks) steam_intervalSavingsCost.get(\"id\")\n               else if (steam_monthlySavingsCost.passesChecks) steam_monthlySavingsCost.get(\"id\")\n               else missingValue\n\n  effectivePoints: {steam_usage:steam_usage,\n                    steam_model:steam_model,\n                    steam_savings:steam_savings,\n                    steam_cost:steam_cost,\n                    steam_savingsCost:steam_savingsCost,\n                    }\n\n  //this is just to get the \"effective\" his columns for widgets, ignoring the points that don't matter\n  hisCols: his.cols\n  effectiveHisCols: {ts: \"ts\",\n                     steam_usage:       try hisCols.find(r=>r.meta.get(\"id\")==steam_usage).name catch missingValue,\n                     steam_model:       try hisCols.find(r=>r.meta.get(\"id\")==steam_model).name catch missingValue,\n                     steam_savings:     try hisCols.find(r=>r.meta.get(\"id\")==steam_savings).name catch missingValue,\n                     steam_cost:        try hisCols.find(r=>r.meta.get(\"id\")==steam_cost).name catch missingValue,\n                     steam_savingsCost: try hisCols.find(r=>r.meta.get(\"id\")==steam_savingsCost).name catch missingValue,\n                    }\n\n  //get effective rollups - only for the points that are relevant\n  rollup_steam_usage: if (rollup_steam_intervalCons!=null and rollup_steam_intervalCons!=missingValue) rollup_steam_intervalCons\n                else if (rollup_steam_monthlyCons!=null and rollup_steam_monthlyCons!=missingValue) rollup_steam_monthlyCons\n                else null\n  rollup_steam_model: if (rollup_steam_intervalModelCons!=null and rollup_steam_intervalModelCons!=missingValue) rollup_steam_intervalModelCons\n                else if (rollup_steam_monthlyModelCons!=null and rollup_steam_monthlyModelCons!=missingValue) rollup_steam_monthlyModelCons\n                else null\n  rollup_steam_savings: if (rollup_steam_intervalSavingsCons!=null and rollup_steam_intervalSavingsCons!=missingValue) rollup_steam_intervalSavingsCons\n                else if (rollup_steam_monthlySavingsCons!=null and rollup_steam_monthlySavingsCons!=missingValue) rollup_steam_monthlySavingsCons\n                else null\n  rollup_steam_cost: if (rollup_steam_intervalCost!=null and rollup_steam_intervalCost!=missingValue) rollup_steam_intervalCost\n                else if (rollup_steam_monthlyCost!=null and rollup_steam_monthlyCost!=missingValue) rollup_steam_monthlyCost\n                else null\n  rollup_steam_savingsCost: if (rollup_steam_intervalSavingsCost!=null and rollup_steam_intervalSavingsCost!=missingValue) rollup_steam_intervalSavingsCost\n                else if (rollup_steam_monthlySavingsCost!=null and rollup_steam_monthlySavingsCost!=missingValue) rollup_steam_monthlySavingsCost\n                else null\n  rollup_steam_rate: if (siteRec.has(\"steamRate\")) siteRec.get(\"steamRate\")\n                else if (rollup_steam_monthlyRateCost!=null and rollup_steam_monthlyRateCost!=missingValue) rollup_steam_monthlyRateCost\n                else null\n\n  effectiveRollup: {steam_usage: rollup_steam_usage,\n                    steam_model: rollup_steam_model,\n                    steam_savings: rollup_steam_savings,\n                    steam_cost: rollup_steam_cost,\n                    steam_savingsCost: rollup_steam_savingsCost,\n                    steam_rate:rollup_steam_rate,\n                    }\n  //output mode\n  out: his\n\n  //FULL MODE\n  if (mode==\"full\") do //returns everything\n    meta:  {points:points,\n            hisSummary: hisSummary,\n            rollupSummary:rollup,\n            effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup,\n            effectiveHisCols:effectiveHisCols}\n    return out.addMeta(meta)\n  end\n\n  //EFFECTIVE MODE\n  else do //returns only effective points we care about\n\n    meta:  {effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup}\n\n    //which columns to keep\n    effectiveCols: effectiveHisCols.vals.findAll(v=>v!=missingValue)\n\n    //what to rename the columns to in order to standardize them\n    renameDict: {}\n    if (effectiveCols.contains(\"steam_intervalCons\")) renameDict=renameDict.set(\"steam_intervalCons\",\"steam_usage\")\n    if (effectiveCols.contains(\"steam_monthlyCons\")) renameDict=renameDict.set(\"steam_monthlyCons\",\"steam_usage\")\n    if (effectiveCols.contains(\"steam_intervalModelCons\")) renameDict=renameDict.set(\"steam_intervalModelCons\",\"steam_model\")\n    if (effectiveCols.contains(\"steam_monthlyModelCons\")) renameDict=renameDict.set(\"steam_monthlyModelCons\",\"steam_model\")\n    if (effectiveCols.contains(\"steam_intervalSavingsCons\")) renameDict=renameDict.set(\"steam_intervalSavingsCons\",\"steam_savings\")\n    if (effectiveCols.contains(\"steam_monthlySavingsCons\")) renameDict=renameDict.set(\"steam_monthlySavingsCons\",\"steam_savings\")\n    if (effectiveCols.contains(\"steam_intervalCost\")) renameDict=renameDict.set(\"steam_intervalCost\",\"steam_cost\")\n    if (effectiveCols.contains(\"steam_monthlyCost\")) renameDict=renameDict.set(\"steam_monthlyCost\",\"steam_cost\")\n    if (effectiveCols.contains(\"steam_intervalSavingsCost\")) renameDict=renameDict.set(\"steam_intervalSavingsCost\",\"steam_savingsCost\")\n    if (effectiveCols.contains(\"steam_monthlySavingsCost\")) renameDict=renameDict.set(\"steam_monthlySavingsCost\",\"steam_savingsCost\")\n    if (effectiveCols.contains(\"steam_monthlyRateCost\")) renameDict=renameDict.set(\"steam_monthlyRateCost\",\"steam_rate\")\n\n    return out.keepCols(effectiveCols)\n             .renameCols(renameDict)\n             .addMeta(meta)\n  end\nend",
      "local_src": "//modes:\n// effective will return only the effective points we care about, with specially named columns\n// full will return all points, with more verbose and less general names\n\n(site, dates, opts:{mode:\"effective\"}) => do\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: site->id\n\n  //get opts\n  debug: if(opts.has(\"debug\")) opts.get(\"debug\")\n  rollupInterval: if (opts.has(\"rollupInterval\")) opts.get(\"rollupInterval\") else 1hr\n  mode: if (opts.has(\"mode\")) opts.get(\"mode\") else \"effective\"\n\n  //set defaults\n  missingPointValue: \"missing point\"\n  missingHisValue: \"missing his\"\n  missingValue: \"missing\"\n\n  //lookup site meter\n  meter: read(siteMeter and steam and siteRef==siteId and equip, false)\n  if (meter.isNull) return {dis:\"Missing site meter\", action:\"Add a site meter and the required points\"}\n\n  //points\n  pointQueryOpts: {equipRef:meter.get(\"id\"), read:true, checked:false}\n\n  steam_intervalCons: pointQuery(\"steam_intervalCons\", pointQueryOpts)\n  steam_intervalModelCons:  pointQuery(\"steam_intervalModelCons\", pointQueryOpts)\n  steam_intervalSavingsCons:  pointQuery(\"steam_intervalSavingsCons\", pointQueryOpts)\n  steam_intervalSavingsCost: pointQuery(\"steam_intervalSavingsCost\", pointQueryOpts)\n  steam_intervalCost: pointQuery(\"steam_intervalCost\", pointQueryOpts)\n\n  steam_monthlyCons: pointQuery(\"steam_monthlyCons\", pointQueryOpts)\n  steam_monthlyModelCons: pointQuery(\"steam_monthlyModelCons\", pointQueryOpts)\n  steam_monthlySavingsCons: pointQuery(\"steam_monthlySavingsCons\", pointQueryOpts)\n  steam_monthlySavingsCost: pointQuery(\"steam_monthlySavingsCost\", pointQueryOpts)\n  steam_monthlyCost: pointQuery(\"steam_monthlyCost\", pointQueryOpts)\n\n  steam_monthlyRateCost:  pointQuery(\"steam_monthlyRateCost\", pointQueryOpts)\n\n\n  points: {steam_intervalCons        : try steam_intervalCons.get(\"id\") catch missingPointValue,\n           steam_intervalModelCons   : try steam_intervalModelCons.get(\"id\") catch missingPointValue,\n           steam_intervalSavingsCons : try steam_intervalSavingsCons.get(\"id\") catch missingPointValue,\n           steam_intervalSavingsCost : try steam_intervalSavingsCost.get(\"id\") catch missingPointValue,\n           steam_intervalCost        : try steam_intervalCost.get(\"id\") catch missingPointValue,\n           steam_monthlyCons         : try steam_monthlyCons.get(\"id\") catch missingPointValue,\n           steam_monthlyModelCons    : try steam_monthlyModelCons.get(\"id\") catch missingPointValue,\n           steam_monthlySavingsCons  : try steam_monthlySavingsCons.get(\"id\") catch missingPointValue,\n           steam_monthlySavingsCost  : try steam_monthlySavingsCost.get(\"id\") catch missingPointValue,\n           steam_monthlyRateCost     : try steam_monthlyRateCost.get(\"id\") catch missingPointValue,\n           steam_monthlyCost         : try steam_monthlyCost.get(\"id\") catch missingPointValue\n          }\n  if(debug==\"points\") return points\n  if (points.vals.all(v=>v==missingPointValue)) return {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n\n  //define col meta options on points\n  if (steam_intervalCons!=null and steam_intervalCons!=missingValue) steam_intervalCons= steam_intervalCons                               .merge({dis:\"Steam Consumption\", chartGroup:\"steam\", color:\"purple\"})\n  if (steam_intervalModelCons!=null and steam_intervalModelCons!=missingValue) steam_intervalModelCons=    steam_intervalModelCons        .merge({dis:\"Steam Model\", chartGroup:\"steam\", color:\"purple\", strokeDasharray:\"1,1\"})\n  if (steam_intervalSavingsCons!=null and steam_intervalSavingsCons!=missingValue) steam_intervalSavingsCons=  steam_intervalSavingsCons  .merge({})\n  if (steam_intervalSavingsCost!=null and steam_intervalSavingsCost!=missingValue) steam_intervalSavingsCost= steam_intervalSavingsCost   .merge({})\n  if (steam_intervalCost!=null and steam_intervalCost!=missingValue) steam_intervalCost= steam_intervalCost                               .merge({})\n\n  if (steam_monthlyCons!=null and steam_monthlyCons!=missingValue) steam_monthlyCons= steam_monthlyCons                                   .merge({dis:\"Steam Consumption\", chartGroup:\"steam\", color:\"purple\"})\n  if (steam_monthlyModelCons!=null and steam_monthlyModelCons!=missingValue) steam_monthlyModelCons= steam_monthlyModelCons               .merge({dis:\"Steam Model\", chartGroup:\"steam\", color:\"purple\", strokeDasharray:\"1,1\"})\n  if (steam_monthlySavingsCons!=null and steam_monthlySavingsCons!=missingValue) steam_monthlySavingsCons= steam_monthlySavingsCons       .merge({})\n  if (steam_monthlySavingsCost!=null and steam_monthlySavingsCost!=missingValue) steam_monthlySavingsCost= steam_monthlySavingsCost       .merge({})\n  if (steam_monthlyCost!=null and steam_monthlyCost!=missingValue) steam_monthlyCost= steam_monthlyCost                                   .merge({})\n\n  if (steam_monthlyRateCost!=null and steam_monthlyRateCost!=missingValue) steam_monthlyRateCost=  steam_monthlyRateCost                  .merge({})\n\n\n  //get his\n  his_steam_intervalCons: try steam_intervalCons.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"steam_intervalCons\") catch null\n  his_steam_intervalModelCons: try steam_intervalModelCons.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"steam_intervalModelCons\") catch null\n  his_steam_intervalSavingsCons: try steam_intervalSavingsCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"steam_intervalSavingsCons\") catch null\n  his_steam_intervalSavingsCost: try steam_intervalSavingsCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"steam_intervalSavingsCost\") catch null\n  his_steam_intervalCost: try steam_intervalCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"steam_intervalCost\") catch null\n\n  his_steam_monthlyCons: try steam_monthlyCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlyCons\") catch null\n  his_steam_monthlyModelCons: try steam_monthlyModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlyModelCons\") catch null\n  his_steam_monthlySavingsCons: try steam_monthlySavingsCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlySavingsCons\") catch null\n  his_steam_monthlySavingsCost: try steam_monthlySavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlySavingsCost\") catch null\n  his_steam_monthlyCost: try steam_monthlyCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlyCost\") catch null\n\n  his_steam_monthlyRateCost: try steam_monthlyRateCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlyRateCost\") catch null\n\n  //pull together his for output\n  his: [his_steam_intervalCons,\n        his_steam_intervalModelCons,\n        his_steam_intervalSavingsCons,\n        his_steam_intervalSavingsCost,\n        his_steam_intervalCost,\n\n        his_steam_monthlyCons,\n        his_steam_monthlyModelCons,\n        his_steam_monthlySavingsCons,\n        his_steam_monthlySavingsCost,\n        his_steam_monthlyCost,\n\n        his_steam_monthlyRateCost\n\n        ].findAll(v=>v!=null).hisJoin.hisRollup(avg, 1hr)\n\n  //this his summary is helpful when debugging, it shows how many data points exist within this span\n  hisSummary: {  his_steam_intervalCons        :  try his_steam_intervalCons.size catch missingHisValue,\n                 his_steam_intervalModelCons   :  try his_steam_intervalModelCons.size catch missingHisValue,\n                 his_steam_intervalSavingsCons :  try his_steam_intervalSavingsCons.size catch missingHisValue,\n                 his_steam_intervalSavingsCost :  try his_steam_intervalSavingsCost.size catch missingHisValue,\n                 his_steam_intervalCost        :  try his_steam_intervalCost.size catch missingHisValue,\n                 his_steam_monthlyCons         :  try his_steam_monthlyCons.size catch missingHisValue,\n                 his_steam_monthlyModelCons    :  try his_steam_monthlyModelCons.size catch missingHisValue,\n                 his_steam_monthlySavingsCons  :  try his_steam_monthlySavingsCons.size catch missingHisValue,\n                 his_steam_monthlySavingsCost  :  try his_steam_monthlySavingsCost.size catch missingHisValue,\n                 his_steam_monthlyCost         :  try his_steam_monthlyCost.size catch missingHisValue,\n                 his_steam_monthlyRateCost     :  try his_steam_monthlyRateCost.size catch missingHisValue,\n          }\n  if (debug==\"his\") return hisSummary\n\n  //rollup data into summary values we can use quickly in widgets\n  rollup_steam_intervalCons: try his_steam_intervalCons.hisRollup(avg,1hr)/*.hisRollup(sum,\"*\")*/.first.get(\"steam_intervalCons\") catch missingValue\n  rollup_steam_intervalModelCons:    try his_steam_intervalModelCons.hisRollup(avg,1hr)/*.hisRollup(sum,\"*\")*/.first.get(\"steam_intervalModelCons\") catch missingValue\n  rollup_steam_intervalSavingsCons:  try his_steam_intervalSavingsCons.hisRollup(avg,1hr)/*.hisRollup(sum,\"*\")*/.first.get(\"steam_intervalSavingsCons\") catch missingValue\n  rollup_steam_intervalSavingsCost:   try his_steam_intervalSavingsCost.hisRollup(sum,\"*\").first.get(\"steam_intervalSavingsCost\") catch missingValue\n  rollup_steam_intervalCost:   try his_steam_intervalCost.hisRollup(sum,\"*\").first.get(\"steam_intervalCost\") catch missingValue\n\n  rollup_steam_monthlyCons :   try his_steam_monthlyCons.hisRollup(sum,\"*\").first.get(\"steam_monthlyCons\") catch missingValue\n  rollup_steam_monthlyModelCons:   try his_steam_monthlyModelCons.hisRollup(sum,\"*\").first.get(\"steam_monthlyModelCons\") catch missingValue\n  rollup_steam_monthlySavingsCons:   try his_steam_monthlySavingsCons.hisRollup(sum,\"*\").first.get(\"steam_monthlySavingsCons\") catch missingValue\n  rollup_steam_monthlySavingsCost:   try his_steam_monthlySavingsCost.hisRollup(sum,\"*\").first.get(\"steam_monthlySavingsCost\") catch missingValue\n  rollup_steam_monthlyCost:   try his_steam_monthlyCost.hisRollup(sum,\"*\").first.get(\"steam_monthlyCost\") catch missingValue\n\n  rollup_steam_monthlyRateCost:   try his_steam_monthlyRateCost.hisRollup(avg,\"*\").first.get(\"steam_monthlyRateCost\") catch missingValue\n\n  rollup:  {rollup_steam_intervalCons: rollup_steam_intervalCons,\n            rollup_steam_intervalModelCons: rollup_steam_intervalModelCons,\n            rollup_steam_intervalSavingsCons: rollup_steam_intervalSavingsCons,\n            rollup_steam_intervalSavingsCost: rollup_steam_intervalSavingsCost,\n            rollup_steam_intervalCost: rollup_steam_intervalCost,\n\n            rollup_steam_monthlyCons: rollup_steam_monthlyCons,\n            rollup_steam_monthlyModelCons: rollup_steam_monthlyModelCons,\n            rollup_steam_monthlySavingsCons: rollup_steam_monthlySavingsCons,\n            rollup_steam_monthlySavingsCost: rollup_steam_monthlySavingsCost,\n            rollup_steam_monthlyCost: rollup_steam_monthlyCost,\n\n            rollup_steam_monthlyRateCost: rollup_steam_monthlyRateCost\n            }\n\n  //get effective points - this will help when we want to ignore points that don't matter and just get the effective points we're actually using\n  //helper func check for effective points\n  passesChecks: (point) => do\n    startTime: dates.toSpan.start\n    if (startTime.isDate) startTime=startTime.dateTime(0:00)\n    if (point.isDict and point!=missingPointValue and point.get(\"hisSize\")>0 and point.get(\"hisEnd\")>startTime) return true\n    else false\n  end\n\n  steam_usage:       if (steam_intervalCons.passesChecks) steam_intervalCons.get(\"id\")\n               else if (steam_monthlyCons.passesChecks) steam_monthlyCons.get(\"id\")\n               else missingValue\n  steam_model:       if (steam_intervalModelCons.passesChecks) steam_intervalModelCons.get(\"id\")\n               else if (steam_monthlyModelCons.passesChecks) steam_monthlyModelCons.get(\"id\")\n               else missingValue\n  steam_savings:     if (steam_intervalSavingsCons.passesChecks) steam_intervalSavingsCons.get(\"id\")\n               else if (steam_monthlySavingsCons.passesChecks) steam_monthlySavingsCons.get(\"id\")\n               else missingValue\n  steam_cost :       if (steam_intervalCost.passesChecks) steam_intervalCost.get(\"id\")\n               else if (steam_monthlyCost.passesChecks) steam_monthlyCost.get(\"id\")\n               else missingValue\n  steam_savingsCost: if (steam_intervalSavingsCost.passesChecks) steam_intervalSavingsCost.get(\"id\")\n               else if (steam_monthlySavingsCost.passesChecks) steam_monthlySavingsCost.get(\"id\")\n               else missingValue\n\n  effectivePoints: {steam_usage:steam_usage,\n                    steam_model:steam_model,\n                    steam_savings:steam_savings,\n                    steam_cost:steam_cost,\n                    steam_savingsCost:steam_savingsCost,\n                    }\n\n  //this is just to get the \"effective\" his columns for widgets, ignoring the points that don't matter\n  hisCols: his.cols\n  effectiveHisCols: {ts: \"ts\",\n                     steam_usage:       try hisCols.find(r=>r.meta.get(\"id\")==steam_usage).name catch missingValue,\n                     steam_model:       try hisCols.find(r=>r.meta.get(\"id\")==steam_model).name catch missingValue,\n                     steam_savings:     try hisCols.find(r=>r.meta.get(\"id\")==steam_savings).name catch missingValue,\n                     steam_cost:        try hisCols.find(r=>r.meta.get(\"id\")==steam_cost).name catch missingValue,\n                     steam_savingsCost: try hisCols.find(r=>r.meta.get(\"id\")==steam_savingsCost).name catch missingValue,\n                    }\n\n  //get effective rollups - only for the points that are relevant\n  rollup_steam_usage: if (rollup_steam_intervalCons!=null and rollup_steam_intervalCons!=missingValue) rollup_steam_intervalCons\n                else if (rollup_steam_monthlyCons!=null and rollup_steam_monthlyCons!=missingValue) rollup_steam_monthlyCons\n                else null\n  rollup_steam_model: if (rollup_steam_intervalModelCons!=null and rollup_steam_intervalModelCons!=missingValue) rollup_steam_intervalModelCons\n                else if (rollup_steam_monthlyModelCons!=null and rollup_steam_monthlyModelCons!=missingValue) rollup_steam_monthlyModelCons\n                else null\n  rollup_steam_savings: if (rollup_steam_intervalSavingsCons!=null and rollup_steam_intervalSavingsCons!=missingValue) rollup_steam_intervalSavingsCons\n                else if (rollup_steam_monthlySavingsCons!=null and rollup_steam_monthlySavingsCons!=missingValue) rollup_steam_monthlySavingsCons\n                else null\n  rollup_steam_cost: if (rollup_steam_intervalCost!=null and rollup_steam_intervalCost!=missingValue) rollup_steam_intervalCost\n                else if (rollup_steam_monthlyCost!=null and rollup_steam_monthlyCost!=missingValue) rollup_steam_monthlyCost\n                else null\n  rollup_steam_savingsCost: if (rollup_steam_intervalSavingsCost!=null and rollup_steam_intervalSavingsCost!=missingValue) rollup_steam_intervalSavingsCost\n                else if (rollup_steam_monthlySavingsCost!=null and rollup_steam_monthlySavingsCost!=missingValue) rollup_steam_monthlySavingsCost\n                else null\n  rollup_steam_rate: if (siteRec.has(\"steamRate\")) siteRec.get(\"steamRate\")\n                else if (rollup_steam_monthlyRateCost!=null and rollup_steam_monthlyRateCost!=missingValue) rollup_steam_monthlyRateCost\n                else null\n\n  effectiveRollup: {steam_usage: rollup_steam_usage,\n                    steam_model: rollup_steam_model,\n                    steam_savings: rollup_steam_savings,\n                    steam_cost: rollup_steam_cost,\n                    steam_savingsCost: rollup_steam_savingsCost,\n                    steam_rate:rollup_steam_rate,\n                    }\n  //output mode\n  out: his\n\n  //FULL MODE\n  if (mode==\"full\") do //returns everything\n    meta:  {points:points,\n            hisSummary: hisSummary,\n            rollupSummary:rollup,\n            effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup,\n            effectiveHisCols:effectiveHisCols}\n    return out.addMeta(meta)\n  end\n\n  //EFFECTIVE MODE\n  else do //returns only effective points we care about\n\n    meta:  {effectivePoints:effectivePoints,\n            effectiveRollup:effectiveRollup}\n\n    //which columns to keep\n    effectiveCols: effectiveHisCols.vals.findAll(v=>v!=missingValue)\n\n    //what to rename the columns to in order to standardize them\n    renameDict: {}\n    if (effectiveCols.contains(\"steam_intervalCons\")) renameDict=renameDict.set(\"steam_intervalCons\",\"steam_usage\")\n    if (effectiveCols.contains(\"steam_monthlyCons\")) renameDict=renameDict.set(\"steam_monthlyCons\",\"steam_usage\")\n    if (effectiveCols.contains(\"steam_intervalModelCons\")) renameDict=renameDict.set(\"steam_intervalModelCons\",\"steam_model\")\n    if (effectiveCols.contains(\"steam_monthlyModelCons\")) renameDict=renameDict.set(\"steam_monthlyModelCons\",\"steam_model\")\n    if (effectiveCols.contains(\"steam_intervalSavingsCons\")) renameDict=renameDict.set(\"steam_intervalSavingsCons\",\"steam_savings\")\n    if (effectiveCols.contains(\"steam_monthlySavingsCons\")) renameDict=renameDict.set(\"steam_monthlySavingsCons\",\"steam_savings\")\n    if (effectiveCols.contains(\"steam_intervalCost\")) renameDict=renameDict.set(\"steam_intervalCost\",\"steam_cost\")\n    if (effectiveCols.contains(\"steam_monthlyCost\")) renameDict=renameDict.set(\"steam_monthlyCost\",\"steam_cost\")\n    if (effectiveCols.contains(\"steam_intervalSavingsCost\")) renameDict=renameDict.set(\"steam_intervalSavingsCost\",\"steam_savingsCost\")\n    if (effectiveCols.contains(\"steam_monthlySavingsCost\")) renameDict=renameDict.set(\"steam_monthlySavingsCost\",\"steam_savingsCost\")\n    if (effectiveCols.contains(\"steam_monthlyRateCost\")) renameDict=renameDict.set(\"steam_monthlyRateCost\",\"steam_rate\")\n\n    return out.keepCols(effectiveCols)\n             .renameCols(renameDict)\n             .addMeta(meta)\n  end\nend",
      "diff": "@@ -7,7 +7,6 @@   //normalize inputs\n   siteRec: site.toRec\n   siteId: site->id\n-  dates = dates.toSpan\n \n   //get opts\n   debug: if(opts.has(\"debug\")) opts.get(\"debug\")\n@@ -57,14 +56,14 @@   if (points.vals.all(v=>v==missingPointValue)) return {dis:\"Missing all required points\", action:\"Add a site meter and the required points\"}\n \n   //define col meta options on points\n-  if (steam_intervalCons!=null and steam_intervalCons!=missingValue) steam_intervalCons= steam_intervalCons                               .merge({dis:\"Steam Consumption\", chartGroup:\"steam\", color:\"slateBlue\"})\n-  if (steam_intervalModelCons!=null and steam_intervalModelCons!=missingValue) steam_intervalModelCons=    steam_intervalModelCons        .merge({dis:\"Steam Model\", chartGroup:\"steam\", color:\"slateBlue\", strokeDasharray:\"1,1\"})\n+  if (steam_intervalCons!=null and steam_intervalCons!=missingValue) steam_intervalCons= steam_intervalCons                               .merge({dis:\"Steam Consumption\", chartGroup:\"steam\", color:\"purple\"})\n+  if (steam_intervalModelCons!=null and steam_intervalModelCons!=missingValue) steam_intervalModelCons=    steam_intervalModelCons        .merge({dis:\"Steam Model\", chartGroup:\"steam\", color:\"purple\", strokeDasharray:\"1,1\"})\n   if (steam_intervalSavingsCons!=null and steam_intervalSavingsCons!=missingValue) steam_intervalSavingsCons=  steam_intervalSavingsCons  .merge({})\n   if (steam_intervalSavingsCost!=null and steam_intervalSavingsCost!=missingValue) steam_intervalSavingsCost= steam_intervalSavingsCost   .merge({})\n   if (steam_intervalCost!=null and steam_intervalCost!=missingValue) steam_intervalCost= steam_intervalCost                               .merge({})\n \n-  if (steam_monthlyCons!=null and steam_monthlyCons!=missingValue) steam_monthlyCons= steam_monthlyCons                                   .merge({dis:\"Steam Consumption\", chartGroup:\"steam\", color:\"slateBlue\"})\n-  if (steam_monthlyModelCons!=null and steam_monthlyModelCons!=missingValue) steam_monthlyModelCons= steam_monthlyModelCons               .merge({dis:\"Steam Model\", chartGroup:\"steam\", color:\"slateBlue\", strokeDasharray:\"1,1\"})\n+  if (steam_monthlyCons!=null and steam_monthlyCons!=missingValue) steam_monthlyCons= steam_monthlyCons                                   .merge({dis:\"Steam Consumption\", chartGroup:\"steam\", color:\"purple\"})\n+  if (steam_monthlyModelCons!=null and steam_monthlyModelCons!=missingValue) steam_monthlyModelCons= steam_monthlyModelCons               .merge({dis:\"Steam Model\", chartGroup:\"steam\", color:\"purple\", strokeDasharray:\"1,1\"})\n   if (steam_monthlySavingsCons!=null and steam_monthlySavingsCons!=missingValue) steam_monthlySavingsCons= steam_monthlySavingsCons       .merge({})\n   if (steam_monthlySavingsCost!=null and steam_monthlySavingsCost!=missingValue) steam_monthlySavingsCost= steam_monthlySavingsCost       .merge({})\n   if (steam_monthlyCost!=null and steam_monthlyCost!=missingValue) steam_monthlyCost= steam_monthlyCost                                   .merge({})\n@@ -73,11 +72,11 @@ \n \n   //get his\n-  his_steam_intervalCons: try steam_intervalCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_intervalCons\") catch null\n-  his_steam_intervalModelCons: try steam_intervalModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_intervalModelCons\") catch null\n-  his_steam_intervalSavingsCons: try steam_intervalSavingsCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).renameCol(\"v0\",\"steam_intervalSavingsCons\") catch null\n-  his_steam_intervalSavingsCost: try steam_intervalSavingsCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_intervalSavingsCost\") catch null\n-  his_steam_intervalCost: try steam_intervalCost.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_intervalCost\") catch null\n+  his_steam_intervalCons: try steam_intervalCons.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"steam_intervalCons\") catch null\n+  his_steam_intervalModelCons: try steam_intervalModelCons.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"steam_intervalModelCons\") catch null\n+  his_steam_intervalSavingsCons: try steam_intervalSavingsCons.hisRead(dates, {-limit}).hisRollup(avg, 1hr).hisInterpolate().renameCol(\"v0\",\"steam_intervalSavingsCons\") catch null\n+  his_steam_intervalSavingsCost: try steam_intervalSavingsCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"steam_intervalSavingsCost\") catch null\n+  his_steam_intervalCost: try steam_intervalCost.hisRead(dates, {-limit}).hisRollup(avg,1hr).hisInterpolate().renameCol(\"v0\",\"steam_intervalCost\") catch null\n \n   his_steam_monthlyCons: try steam_monthlyCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlyCons\") catch null\n   his_steam_monthlyModelCons: try steam_monthlyModelCons.hisRead(dates, {-limit}).renameCol(\"v0\",\"steam_monthlyModelCons\") catch null\n@@ -120,9 +119,9 @@   if (debug==\"his\") return hisSummary\n \n   //rollup data into summary values we can use quickly in widgets\n-  rollup_steam_intervalCons: try his_steam_intervalCons.hisRollup(avg,1hr).hisRollup(sum,\"*\").first.get(\"steam_intervalCons\") catch missingValue\n-  rollup_steam_intervalModelCons:    try his_steam_intervalModelCons.hisRollup(avg,1hr).hisRollup(sum,\"*\").first.get(\"steam_intervalModelCons\") catch missingValue\n-  rollup_steam_intervalSavingsCons:  try his_steam_intervalSavingsCons.hisRollup(avg,1hr).hisRollup(sum,\"*\").first.get(\"steam_intervalSavingsCons\") catch missingValue\n+  rollup_steam_intervalCons: try his_steam_intervalCons.hisRollup(avg,1hr)/*.hisRollup(sum,\"*\")*/.first.get(\"steam_intervalCons\") catch missingValue\n+  rollup_steam_intervalModelCons:    try his_steam_intervalModelCons.hisRollup(avg,1hr)/*.hisRollup(sum,\"*\")*/.first.get(\"steam_intervalModelCons\") catch missingValue\n+  rollup_steam_intervalSavingsCons:  try his_steam_intervalSavingsCons.hisRollup(avg,1hr)/*.hisRollup(sum,\"*\")*/.first.get(\"steam_intervalSavingsCons\") catch missingValue\n   rollup_steam_intervalSavingsCost:   try his_steam_intervalSavingsCost.hisRollup(sum,\"*\").first.get(\"steam_intervalSavingsCost\") catch missingValue\n   rollup_steam_intervalCost:   try his_steam_intervalCost.hisRollup(sum,\"*\").first.get(\"steam_intervalCost\") catch missingValue\n \n"
    },
    {
      "name": "getUtilityMeterSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(site, dates, opts:{}) => do\n\n  //options\n  dates = dates.toSpan\n\n  //get elec data\n  elec: getElecMeterSummary(site, dates, opts)\n  if (elec.isDict) throw elec\n  elecMeta: elec.meta\n  //elec = elec.hisMap(v=>v.to(\"kW\"))\n  combined: elec.addMeta({elecMeta:elecMeta})\n\n  //get Gas data\n  gas: getGasMeterSummary(site, dates, opts)\n  if (not gas.isDict) do\n    gasMeta: gas.meta\n    //hw = hw.hisMap(v=>v.to(\"kBTU/h\"))\n    combined = hisJoin([combined, gas]).addMeta({gasMeta:gasMeta})\n  end\n\n  //get Hot Water data\n  hw: getHWMeterSummary(site, dates, opts)\n  if (not hw.isDict) do\n    hwMeta: hw.meta\n    //hw = hw.hisMap(v=>v.to(\"kBTU/h\"))\n    combined = hisJoin([combined, hw]).addMeta({hwMeta:hwMeta})\n  end\n\n  //get Chilled Water data\n  chw: getCHWMeterSummary(site, dates, opts)\n  if (not chw.isDict) do\n    chwMeta: chw.meta\n    //chw = chw.hisMap(v=>v.to(\"tonref\"))\n    combined = hisJoin([combined, chw]).addMeta({chwMeta:chwMeta})\n  end\n\n  //get Steam data\n  steam: getSteamMeterSummary(site, dates, opts)\n  if (not steam.isDict) do\n    steamMeta: steam.meta\n    //steam = steam.hisMap(v=>v.to(\"klb/h\"))\n    combined = hisJoin([combined, steam]).addMeta({steamMeta:steamMeta})\n  end\n\n  //update meta\n  out: combined\n\n\nend",
      "local_src": "(site, dates, opts:{}) => do\n\n  //options\n\n  //get elec data\n  elec: getElecMeterSummary(site, dates, opts)\n  if (elec.isDict) throw elec\n  elecMeta: elec.meta\n  //elec = elec.hisMap(v=>v.to(\"kW\"))\n  combined: elec.addMeta({elecMeta:elecMeta})\n  \n  //get Hot Water data\n  hw: getHWMeterSummary(site, dates, opts)\n  if (not hw.isDict) do\n    hwMeta: hw.meta\n    //hw = hw.hisMap(v=>v.to(\"kBTU/h\"))\n    combined = hisJoin([combined, hw]).addMeta({hwMeta:hwMeta})\n  end\n\n  //get Chilled Water data\n  chw: getCHWMeterSummary(site, dates, opts)\n  if (not chw.isDict) do\n    chwMeta: chw.meta\n    //chw = chw.hisMap(v=>v.to(\"tonref\"))\n    combined = hisJoin([combined, chw]).addMeta({chwMeta:chwMeta})\n  end\n\n  //get Steam data\n  steam: getSteamMeterSummary(site, dates, opts)\n  if (not steam.isDict) do\n    steamMeta: steam.meta\n    //steam = steam.hisMap(v=>v.to(\"klb/h\"))\n    combined = hisJoin([combined, steam]).addMeta({steamMeta:steamMeta})\n  end\n\n  //update meta\n  out: combined\n\n\nend",
      "diff": "@@ -1,7 +1,6 @@ (site, dates, opts:{}) => do\n \n   //options\n-  dates = dates.toSpan\n \n   //get elec data\n   elec: getElecMeterSummary(site, dates, opts)\n@@ -9,14 +8,6 @@   elecMeta: elec.meta\n   //elec = elec.hisMap(v=>v.to(\"kW\"))\n   combined: elec.addMeta({elecMeta:elecMeta})\n-\n-  //get Gas data\n-  gas: getGasMeterSummary(site, dates, opts)\n-  if (not gas.isDict) do\n-    gasMeta: gas.meta\n-    //hw = hw.hisMap(v=>v.to(\"kBTU/h\"))\n-    combined = hisJoin([combined, gas]).addMeta({gasMeta:gasMeta})\n-  end\n \n   //get Hot Water data\n   hw: getHWMeterSummary(site, dates, opts)\n"
    },
    {
      "name": "kpi_chwp_percentRuntime",
      "pod": "kwLinkRuleExt",
      "pod_src": "/*\n*   Description :\n*     Given a CHWP, return runtime periods for that CHWP.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    target          - The chiller Ref.\n*     DateTime               dates           - The dates to look at.\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/27/2024   - Initial Creation\n*/\ndefcomp\n  target: {}\n  date:   {}\n  out:    {readonly}\n  pri_pump_speed  : {bind:\"water and pump and speed and cmd and primary and equipRef=={{target->id}}\"}\n  sec_pump_speed  : {bind:\"water and pump and speed and cmd and secondary and equipRef=={{target->id}}\"}\n  pri_pump_status : {bind:\"water and pump and run and cmd and primary and equipRef=={{target->id}}\"}\n  sec_pump_status : {bind:\"water and pump and run and cmd and secondary and equipRef=={{target->id}}\"}\n  pri_pump_power  : {bind:\"water and pump and power and sensor and primary and equipRef=={{target->id}}\"}\n  sec_pump_power  : {bind:\"water and pump and power and sensor and secondary and equipRef=={{target->id}}\"}\n  do\n    // Call logic function\n    his: logic_cwp_pumpIsRunning(target, date)\n\n    // Handle Edge-case\n    if (his.isStr) return null\n\n    // Calculate percentage\n    runtimePercent: try hisPeriodOverlapPercent(his, spanToPeriods(date)) catch 0%\n    return out = {avg: runtimePercent}\n  end\nend\n",
      "local_src": "/*\n*   Description :\n*     Given a CHWP, return runtime periods for that CHWP.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    target          - The chiller Ref.\n*     DateTime               dates           - The dates to look at.\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/27/2024   - Initial Creation\n*/\ndefcomp\n  target: {}\n  date:   {}\n  out:    {readonly}\n  pri_pump_speed  : {bind:\"water and pump and speed and cmd and primary and equipRef->equipRef->equipRef=={{target->id}}\"}\n  sec_pump_speed  : {bind:\"water and pump and speed and cmd and secondary and equipRef->equipRef->equipRef=={{target->id}}\"}\n  pri_pump_status : {bind:\"water and pump and run and cmd and primary and equipRef->equipRef->equipRef=={{target->id}}\"}\n  sec_pump_status : {bind:\"water and pump and run and cmd and secondary and equipRef->equipRef->equipRef=={{target->id}}\"}\n  pri_pump_power  : {bind:\"water and pump and power and sensor and primary and equipRef->equipRef->equipRef=={{target->id}}\"}\n  sec_pump_power  : {bind:\"water and pump and power and sensor and secondary and equipRef->equipRef->equipRef=={{target->id}}\"}\n  do\n    // Call logic function\n    his: logic_cwp_pumpIsRunning(target, date)\n\n    // Handle Edge-case\n    if (his.isStr) return null\n\n    // Calculate percentage\n    runtimePercent: try hisPeriodOverlapPercent(his, spanToPeriods(date)) catch 0%\n    return out = {avg: runtimePercent}\n  end\nend\n",
      "diff": "@@ -15,12 +15,12 @@   target: {}\n   date:   {}\n   out:    {readonly}\n-  pri_pump_speed  : {bind:\"water and pump and speed and cmd and primary and equipRef=={{target->id}}\"}\n-  sec_pump_speed  : {bind:\"water and pump and speed and cmd and secondary and equipRef=={{target->id}}\"}\n-  pri_pump_status : {bind:\"water and pump and run and cmd and primary and equipRef=={{target->id}}\"}\n-  sec_pump_status : {bind:\"water and pump and run and cmd and secondary and equipRef=={{target->id}}\"}\n-  pri_pump_power  : {bind:\"water and pump and power and sensor and primary and equipRef=={{target->id}}\"}\n-  sec_pump_power  : {bind:\"water and pump and power and sensor and secondary and equipRef=={{target->id}}\"}\n+  pri_pump_speed  : {bind:\"water and pump and speed and cmd and primary and equipRef->equipRef->equipRef=={{target->id}}\"}\n+  sec_pump_speed  : {bind:\"water and pump and speed and cmd and secondary and equipRef->equipRef->equipRef=={{target->id}}\"}\n+  pri_pump_status : {bind:\"water and pump and run and cmd and primary and equipRef->equipRef->equipRef=={{target->id}}\"}\n+  sec_pump_status : {bind:\"water and pump and run and cmd and secondary and equipRef->equipRef->equipRef=={{target->id}}\"}\n+  pri_pump_power  : {bind:\"water and pump and power and sensor and primary and equipRef->equipRef->equipRef=={{target->id}}\"}\n+  sec_pump_power  : {bind:\"water and pump and power and sensor and secondary and equipRef->equipRef->equipRef=={{target->id}}\"}\n   do\n     // Call logic function\n     his: logic_cwp_pumpIsRunning(target, date)\n"
    },
    {
      "name": "kpi_chws_avgSystemFlow",
      "pod": "kwLinkRuleExt",
      "pod_src": "/*\n*   Description :\n*     KPI that calculates (simplistically) the average system flow of a chilled water system's building loop.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/05/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024       - Added edge case handling fix. Added opts + loop override support.\n*/\ndefcomp\n  target: {}\n  date:   {}\n  out:    {readonly}\n  opts:   {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Retrieve loop\n    points: buildingLoopDigest(target, opts).get(\"buildingLoop\")\n\n    // Edge-case handling for non-existing loop\n    if (points.isStr) return null\n\n    // Retrieve required points\n    chw_flow: points.first.get(\"chw_flow\")\n\n    // Edge-case Handling\n    if (chw_flow.isStr) return chw_flow\n\n    // hisRead\n    his : chw_flow.hisRead(date.toSpan, {-limit}).renameCol(\"v0\", \"chw_flow\").hisInterpolate.hisClip\n\n    // Edge-case Handling\n    if (his.isEmpty) return \"No his data\"\n\n    //Filter for when anu building loop pump is running\n    opts = opts.set(\"id\", true)\n    bLoopId: buildingLoopDigest(target, opts)\n    pumpRunningHis: logic_chws_equipsRunning(bLoopId, date, {equip:\"pump\"})\n\n    // Edge-case handling\n    if (pumpRunningHis.isStr) return pumpRunningHis\n\n    pumpRunningHis = pumpRunningHis.keepCols([\"ts\", \"anyEquipRunning\"]).findAll(r => r.get(\"anyEquipRunning\").isNonNull)\n\n    if(pumpRunningHis.isNonNull) his = his.hisFindInPeriods(pumpRunningHis)\n\n    // Fold resulting hisGrid column based on average, and return.\n    average: his.foldCol(\"chw_flow\", avg)\n\n    // Return\n    out = {avg: average}\n  end\nend",
      "local_src": "/*\n*   Description :\n*     KPI that calculates (simplistically) the average system flow of a chilled water system's building loop.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/05/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024       - Added edge case handling fix. Added opts + loop override support.\n*/\ndefcomp\n  target: {}\n  date:   {}\n  out:    {readonly}\n  opts:   {bindTuning:\"opts\",      defVal:{}}\n\n  do\n      \n    opts = {secondaryLoopOverride: \"null\"}\n      \n    // Retrieve loop\n    points: buildingLoopDigest(target, opts).get(\"buildingLoop\")\n\n    // Edge-case handling for non-existing loop\n    if (points.isStr) return null\n\n    // Retrieve required points\n    chw_flow: points.first.get(\"chw_flow\")\n\n    // Edge-case Handling\n    if (chw_flow.isStr) return chw_flow\n\n    // hisRead\n    his : chw_flow.hisRead(date.toSpan, {-limit}).renameCol(\"v0\", \"chw_flow\").hisInterpolate.hisClip\n\n    // Edge-case Handling\n    if (his.isEmpty) return \"No his data\"\n\n    //Filter for when anu building loop pump is running\n    opts = opts.set(\"id\", true)\n    bLoopId: buildingLoopDigest(target, opts)\n    pumpRunningHis: logic_chws_equipsRunning(bLoopId, date, {equip:\"pump\"})\n\n    // Edge-case handling\n    //if (pumpRunningHis.isStr) return pumpRunningHis\n\n    pumpRunningHis = try pumpRunningHis.keepCols([\"ts\", \"anyEquipRunning\"]).findAll(r => r.get(\"anyEquipRunning\").isNonNull) catch null\n\n    if(pumpRunningHis.isNonNull) his = his.hisFindInPeriods(pumpRunningHis)\n\n    // Fold resulting hisGrid column based on average, and return.\n    average: his.foldCol(\"chw_flow\", avg)\n\n    // Return\n    out = {avg: average}\n  end\nend",
      "diff": "@@ -14,6 +14,9 @@   opts:   {bindTuning:\"opts\",      defVal:{}}\n \n   do\n+\n+    opts = {secondaryLoopOverride: \"null\"}\n+\n     // Retrieve loop\n     points: buildingLoopDigest(target, opts).get(\"buildingLoop\")\n \n@@ -38,9 +41,9 @@     pumpRunningHis: logic_chws_equipsRunning(bLoopId, date, {equip:\"pump\"})\n \n     // Edge-case handling\n-    if (pumpRunningHis.isStr) return pumpRunningHis\n+    //if (pumpRunningHis.isStr) return pumpRunningHis\n \n-    pumpRunningHis = pumpRunningHis.keepCols([\"ts\", \"anyEquipRunning\"]).findAll(r => r.get(\"anyEquipRunning\").isNonNull)\n+    pumpRunningHis = try pumpRunningHis.keepCols([\"ts\", \"anyEquipRunning\"]).findAll(r => r.get(\"anyEquipRunning\").isNonNull) catch null\n \n     if(pumpRunningHis.isNonNull) his = his.hisFindInPeriods(pumpRunningHis)\n \n"
    },
    {
      "name": "kpi_chws_systemPeakDemand",
      "pod": "kwLinkRuleExt",
      "pod_src": "/*\n*   Description :\n*     KPI that calculates (simplistically) the min/max demand of the chilled water system.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/02/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024       - Added opts + loop override support.\n*/\ndefcomp\n  target: {}\n  date:   {}\n  out:    {readonly}\n  opts:   {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Retrieve demand data\n    opts = opts.set(\"quickModeDemand\", true)\n    demandHis : logic_chws_energyData(target, date, opts)\n\n    // Handle Edge-case\n    if (demandHis.isStr) return null\n\n    // Fold resulting hisGrid column based on min/max, and return.\n    maxPeak : demandHis.foldCol(\"systemDemand\", max)\n    minPeak : demandHis.foldCol(\"systemDemand\", min)\n\n    out = {min: minPeak, max: maxPeak}\n  end\nend",
      "local_src": "/*\n*   Description :\n*     KPI that calculates (simplistically) the min/max demand of the chilled water system.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/02/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024       - Added opts + loop override support.\n*/\ndefcomp\n  target: {}\n  date:   {}\n  out:    {readonly}\n  opts:   {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Retrieve demand data\n    opts = opts.set(\"quickModeDemand\", true)\n    demandHis : logic_chws_energyData(target, date, opts)\n\n    // Handle Edge-case\n    if (demandHis.isStr) return null\n\n    // Fold resulting hisGrid column based on min/max, and return.\n    maxPeak : demandHis.foldCol(\"v0\", max)\n    minPeak : demandHis.foldCol(\"v0\", min)\n\n    out = {min: minPeak, max: maxPeak}\n  end\nend",
      "diff": "@@ -22,8 +22,8 @@     if (demandHis.isStr) return null\n \n     // Fold resulting hisGrid column based on min/max, and return.\n-    maxPeak : demandHis.foldCol(\"systemDemand\", max)\n-    minPeak : demandHis.foldCol(\"systemDemand\", min)\n+    maxPeak : demandHis.foldCol(\"v0\", max)\n+    minPeak : demandHis.foldCol(\"v0\", min)\n \n     out = {min: minPeak, max: maxPeak}\n   end\n"
    },
    {
      "name": "kpi_chws_totalSystemEnergyUse",
      "pod": "kwLinkRuleExt",
      "pod_src": "/*\n*   Description :\n*     KPI that calculates (simplistically) the average energy use of a CHW system.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/02/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024       - Added opts + loop override support.\n*/\ndefcomp\n  target: {}\n  date:   {}\n  out:    {readonly}\n  opts:   {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Retrieve demand data\n    opts = opts.set(\"quickModeUsage\", true)\n    usageHis : logic_chws_energyData(target, date, opts)\n\n    // Handle Edge-case\n    if (usageHis.isStr) return null\n\n    // Fold resulting hisGrid column based on min/max, and return.\n    totalUsage : usageHis.findAll(r => not r.get(\"systemUsage\").isNaN).foldCol(\"systemUsage\", sum)\n\n    out = {sum: totalUsage}\n  end\nend",
      "local_src": "/*\n*   Description :\n*     KPI that calculates (simplistically) the average energy use of a CHW system.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/02/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024       - Added opts + loop override support.\n*/\ndefcomp\n  target: {}\n  date:   {}\n  out:    {readonly}\n  opts:   {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Retrieve demand data\n    opts = opts.set(\"quickModeUsage\", true)\n    usageHis : logic_chws_energyData(target, date, opts)\n\n    // Handle Edge-case\n    if (usageHis.isStr) return null\n\n    // Fold resulting hisGrid column based on min/max, and return.\n    totalUsage : usageHis.findAll(r => not r.get(\"v0\").isNaN).foldCol(\"v0\", sum)\n\n    out = {sum: totalUsage}\n  end\nend",
      "diff": "@@ -22,7 +22,7 @@     if (usageHis.isStr) return null\n \n     // Fold resulting hisGrid column based on min/max, and return.\n-    totalUsage : usageHis.findAll(r => not r.get(\"systemUsage\").isNaN).foldCol(\"systemUsage\", sum)\n+    totalUsage : usageHis.findAll(r => not r.get(\"v0\").isNaN).foldCol(\"v0\", sum)\n \n     out = {sum: totalUsage}\n   end\n"
    },
    {
      "name": "kpi_hhws_secondaryLoop_percentRuntimeOnePump",
      "pod": "kwLinkRuleExt",
      "pod_src": "/*\n*     KPI that calculates (simplistically) the percent runtime of one pump\n*     in the secondary loop.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/27/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/10/2024      - Added individual loop override support (Line 23-27)\n*/\ndefcomp\n  target: {}\n  date:   {}\n  out:    {readonly}\n  opts:   {bindTuning:\"opts\", defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec.get(\"id\")\n    date = date.toSpan\n\n    // Retrieve Secondary Loop\n    secondaryLoop: if (opts.get(\"secondaryLoopOverride\").isRef) opts.get(\"secondaryLoopOverride\")\n                   else do\n                     try read(secondaryLoop and equipRef == targetId).toRec.get(\"id\")\n                     catch \"No Secondary Loop Present\"\n                   end\n\n    // Handle Edge-case\n    if (secondaryLoop.isStr) return null\n\n    // Call Logic\n    his: logic_hhws_equipsRunning(secondaryLoop, date, {equip: \"pump\"})\n\n    // Handle Edge-case\n    if (his.isStr) return null\n\n    runtimePercent: try hisPeriodOverlapPercent(his.keepCols([\"ts\", \"oneEquipRunning\"]), spanToPeriods(date)) catch 0%\n\n    return out = {avg: runtimePercent}\n  end\nend\n",
      "local_src": "/*\n*     KPI that calculates (simplistically) the percent runtime of one pump\n*     in the secondary loop.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/27/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/10/2024      - Added individual loop override support (Line 23-27)\n*/\ndefcomp\n  target: {}\n  date:   {}\n  out:    {readonly}\n  opts:   {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec.get(\"id\")\n    date = date.toSpan\n\n    // Retrieve Secondary Loop\n    secondaryLoop: if (opts.get(\"secondaryLoopOverride\").isRef) opts.get(\"secondaryLoopOverride\")\n                   else do\n                     try read(secondaryLoop and equipRef == targetId).toRec.get(\"id\")\n                     catch \"No Secondary Loop Present\"\n                   end\n\n    // Handle Edge-case\n    if (secondaryLoop.isStr) return null\n\n    // Call Logic\n    his: logic_hhws_equipsRunning(secondaryLoop, date, {equip: \"pump\"})\n\n    // Handle Edge-case\n    if (his.isStr) return null\n\n    runtimePercent: try hisPeriodOverlapPercent(his.keepCols([\"ts\", \"oneEquipRunning\"]), spanToPeriods(date)) catch 0%\n\n    return out = {avg: runtimePercent}\n  end\nend\n",
      "diff": "@@ -11,7 +11,7 @@   target: {}\n   date:   {}\n   out:    {readonly}\n-  opts:   {bindTuning:\"opts\", defVal:{}}\n+  opts:   {bindTuning:\"opts\",      defVal:{}}\n \n   do\n     // Normalize Inputs\n"
    },
    {
      "name": "kpi_siteMeter_actualElecKwh",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/*\n*   Description :\n*   Total Electric Usage (kWh)\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:              {}\n  date:                {}\n  out:                 {readonly}\n  elec_intervalPwr:    {bind:\"elec and power and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the interval power point for the siteMeters.\n    elec_intervalPwr = pointQuery(\"elec_intervalPwr\",{read, equipRef: targetId, checked: false})\n    if(elec_intervalPwr.isNull) return out = null\n\n    // Rollup to an hour\n    his : elec_intervalPwr.hisRead(date).hisRollup(avg,1hr).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n\n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n\n    // Change units to kWh, fold column based on sum, and declare out before returning\n    his = his.map(r=>r.set(\"v0\", r.get(\"v0\").as(1kWh)))\n    actualkWh : his.foldCol(\"v0\", sum)\n    out = {sum: actualkWh}\n  end\nend",
      "local_src": "/*\n*   Description :\n*   Total Electric Usage (kWh) \n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:              {}\n  date:                {}\n  out:                 {readonly}\n  elec_intervalPwr:    {bind:\"elec and power and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the interval power point for the siteMeters.\n    elec_intervalPwr = pointQuery(\"elec_intervalPwr\",{read, equipRef: targetId, checked: false})\n    if(elec_intervalPwr.isNull) return out = null\n    \n    // Rollup to an hour\n    his : elec_intervalPwr.hisRead(date).hisRollup(avg,1hr).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n    \n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n    \n    // Change units to kWh, fold column based on sum, and declare out before returning\n    his = his.map(r=>r.set(\"v0\", r.get(\"v0\").as(1kWh)))\n    actualkWh : his.foldCol(\"v0\", sum)\n    out = {sum: actualkWh}\n  end\nend",
      "diff": "@@ -10,7 +10,7 @@   target:              {}\n   date:                {}\n   out:                 {readonly}\n-  elec_intervalPwr:    {bind:\"elec and power and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n+  elec_intervalPwr:    {bind:\"elec and power and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n   opts:                {bindTuning:\"opts\",      defVal:{}}\n \n   do\n"
    },
    {
      "name": "kpi_siteMeter_actualSteamKlbs",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/*\n*   Description :\n*   Actual Steam (klbs)\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:              {}\n  date:                {}\n  out:                 {readonly}\n  steam_intervalCons:  {bind:\"steam and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the steam average mass flow for the siteMeters.\n    steam_intervalCons = pointQuery(\"steam_intervalCons\", {read, equipRef: targetId, checked: false})\n    if(steam_intervalCons.isNull) return out = null\n\n    // Rollup to an hour\n    his : steam_intervalCons.hisRead(date).hisRollup(avg,1hr).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n\n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n\n    // Change units to klb, fold column based on sum, and declare out before returning\n    his = his.map(r=>r.set(\"v0\",  r.get(\"v0\").to(\"klb/h\").as(\"klb\")))\n    actualKlbs : his.foldCol(\"v0\", sum)\n    out = {sum: actualKlbs}\n  end\nend",
      "local_src": "/*\n*   Description :\n*   Actual Steam (klbs) \n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:              {}\n  date:                {}\n  out:                 {readonly}\n  steam_intervalCons:  {bind:\"steam and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the steam average mass flow for the siteMeters.\n    steam_intervalCons = pointQuery(\"steam_intervalCons\", {read, equipRef: targetId, checked: false})\n    if(steam_intervalCons.isNull) return out = null\n    \n    // Rollup to an hour\n    his : steam_intervalCons.hisRead(date).hisRollup(avg,1hr).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n    \n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n    \n    // Change units to klb, fold column based on sum, and declare out before returning\n    his = his.map(r=>r.set(\"v0\",  r.get(\"v0\").to(\"klb/h\").as(\"klb\")))\n    actualKlbs : his.foldCol(\"v0\", sum)\n    out = {sum: actualKlbs}\n  end\nend",
      "diff": "@@ -10,7 +10,7 @@   target:              {}\n   date:                {}\n   out:                 {readonly}\n-  steam_intervalCons:  {bind:\"steam and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n+  steam_intervalCons:  {bind:\"steam and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n   opts:                {bindTuning:\"opts\",      defVal:{}}\n \n   do\n"
    },
    {
      "name": "kpi_siteMeter_actualchwTonHrs",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/*\n*   Description :\n*   Actual CHW ton-hrs\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:                    {}\n  date:                      {}\n  out:                       {readonly}\n  chilledWater_intervalCons: {bind:\"chilled and water and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                      {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the steam average mass flow for the siteMeters.\n    chilledWater_intervalCons = pointQuery(\"chilledWater_intervalCons\", {read, equipRef: targetId, checked: false})\n    if(chilledWater_intervalCons.isNull) return out = null\n\n    // Rollup to an hour\n    his : chilledWater_intervalCons.hisRead(date).hisRollup(avg,1hr).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n\n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n\n    // Change units, fold column based on sum, and declare out before returning\n    his = his.map(r=>r.set(\"v0\",  r.get(\"v0\").to(\"tonref\").as(\"tonrefh\")))\n    actualChw : his.foldCol(\"v0\", sum)\n    out = {sum: actualChw}\n  end\nend",
      "local_src": "/*\n*   Description :\n*   Actual CHW ton-hrs \n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:                    {}\n  date:                      {}\n  out:                       {readonly}\n  chilledWater_intervalCons: {bind:\"chilled and water and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                      {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the steam average mass flow for the siteMeters.\n    chilledWater_intervalCons = pointQuery(\"chilledWater_intervalCons\", {read, equipRef: targetId, checked: false})\n    if(chilledWater_intervalCons.isNull) return out = null\n    \n    // Rollup to an hour\n    his : chilledWater_intervalCons.hisRead(date).hisRollup(avg,1hr).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n    \n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n    \n    // Change units, fold column based on sum, and declare out before returning\n    his = his.map(r=>r.set(\"v0\",  r.get(\"v0\").to(\"tonref\").as(\"tonrefh\")))\n    actualChw : his.foldCol(\"v0\", sum)\n    out = {sum: actualChw}\n  end\nend",
      "diff": "@@ -10,7 +10,7 @@   target:                    {}\n   date:                      {}\n   out:                       {readonly}\n-  chilledWater_intervalCons: {bind:\"chilled and water and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n+  chilledWater_intervalCons: {bind:\"chilled and water and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n   opts:                      {bindTuning:\"opts\",      defVal:{}}\n \n   do\n"
    },
    {
      "name": "kpi_siteMeter_maxChwsDemand",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/*\n*   Description :\n*   Max CHWS Demand (tonref)\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:                    {}\n  date:                      {}\n  out:                       {readonly}\n  chilledWater_intervalCons: {bind:\"chilled and water and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                      {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the interval chws point for the siteMeters.\n    chilledWater_intervalCons = pointQuery(\"chilledWater_intervalCons\",{read, equipRef: targetId, checked: false})\n    if(chilledWater_intervalCons.isNull) return out = null\n\n    // Rollup to an hour\n    his : chilledWater_intervalCons.hisRead(date).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n\n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n\n    // Fold column on max to find the max chws demand\n    maxChwsDemand : his.foldCol(\"v0\", max)\n    minChwsDemand : his.foldCol(\"v0\", min)\n    out = {min: minChwsDemand, max: maxChwsDemand}\n  end\nend",
      "local_src": "/*\n*   Description :\n*   Max CHWS Demand (tonref) \n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:                    {}\n  date:                      {}\n  out:                       {readonly}\n  chilledWater_intervalCons: {bind:\"chilled and water and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                      {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the interval chws point for the siteMeters.\n    chilledWater_intervalCons = pointQuery(\"chilledWater_intervalCons\",{read, equipRef: targetId, checked: false})\n    if(chilledWater_intervalCons.isNull) return out = null\n    \n    // Rollup to an hour\n    his : chilledWater_intervalCons.hisRead(date).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n    \n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n    \n    // Fold column on max to find the max chws demand\n    maxChwsDemand : his.foldCol(\"v0\", max)\n    minChwsDemand : his.foldCol(\"v0\", min)\n    out = {min: minChwsDemand, max: maxChwsDemand}\n  end\nend",
      "diff": "@@ -10,7 +10,7 @@   target:                    {}\n   date:                      {}\n   out:                       {readonly}\n-  chilledWater_intervalCons: {bind:\"chilled and water and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n+  chilledWater_intervalCons: {bind:\"chilled and water and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n   opts:                      {bindTuning:\"opts\",      defVal:{}}\n \n   do\n"
    },
    {
      "name": "kpi_siteMeter_maxElecDemand",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/*\n*   Description :\n*   Max Electric Demand (kW)\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:              {}\n  date:                {}\n  out:                 {readonly}\n  elec_intervalPwr:    {bind:\"elec and power and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the interval power point for the siteMeters.\n    elec_intervalPwr = pointQuery(\"elec_intervalPwr\",{read, equipRef: targetId, checked: false})\n    if(elec_intervalPwr.isNull) return out = null\n\n    // Rollup to an hour\n    his : elec_intervalPwr.hisRead(date).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n\n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n\n    // Fold column on max to find the max electric demand\n    maxElecDemand : his.foldCol(\"v0\", max)\n    minElecDemand : his.foldCol(\"v0\", min)\n    out = {min: minElecDemand, max: maxElecDemand}\n  end\nend",
      "local_src": "/*\n*   Description :\n*   Max Electric Demand (kW) \n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:              {}\n  date:                {}\n  out:                 {readonly}\n  elec_intervalPwr:    {bind:\"elec and power and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the interval power point for the siteMeters.\n    elec_intervalPwr = pointQuery(\"elec_intervalPwr\",{read, equipRef: targetId, checked: false})\n    if(elec_intervalPwr.isNull) return out = null\n    \n    // Rollup to an hour\n    his : elec_intervalPwr.hisRead(date).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n    \n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n    \n    // Fold column on max to find the max electric demand\n    maxElecDemand : his.foldCol(\"v0\", max)\n    minElecDemand : his.foldCol(\"v0\", min)\n    out = {min: minElecDemand, max: maxElecDemand}\n  end\nend",
      "diff": "@@ -10,7 +10,7 @@   target:              {}\n   date:                {}\n   out:                 {readonly}\n-  elec_intervalPwr:    {bind:\"elec and power and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n+  elec_intervalPwr:    {bind:\"elec and power and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n   opts:                {bindTuning:\"opts\",      defVal:{}}\n \n   do\n"
    },
    {
      "name": "kpi_siteMeter_maxSteamDemand",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/*\n*   Description :\n*    Max Steam Demand (klb/h)\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:              {}\n  date:                {}\n  out:                 {readonly}\n  steam_intervalCons:  {bind:\"steam and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the interval steam point for the siteMeter.\n    steam_intervalCons = pointQuery(\"steam_intervalCons\",{read, equipRef: targetId, checked: false})\n    if(steam_intervalCons.isNull) return out = null\n\n    // Rollup to an hour\n    his : steam_intervalCons.hisRead(date).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n\n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n\n    // Fold column on max to find the max steam demand\n    maxSteamDemand : his.foldCol(\"v0\", max).to(1klb/h)\n    minSteamDemand : his.foldCol(\"v0\", min).to(1klb/h)\n    out = {min: minSteamDemand, max: maxSteamDemand}\n  end\nend",
      "local_src": "/*\n*   Description :\n*    Max Steam Demand (klb/h) \n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    03/17/2025      - Initial Creation\n*/\ndefcomp\n  target:              {}\n  date:                {}\n  out:                 {readonly}\n  steam_intervalCons:  {bind:\"steam and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n  opts:                {bindTuning:\"opts\",      defVal:{}}\n\n  do\n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec->id\n    site: targetRec.get(\"siteRef\")\n    date = date.toSpan()\n\n    // Query the interval steam point for the siteMeter.\n    steam_intervalCons = pointQuery(\"steam_intervalCons\",{read, equipRef: targetId, checked: false})\n    if(steam_intervalCons.isNull) return out = null\n    \n    // Rollup to an hour\n    his : steam_intervalCons.hisRead(date).hisClip.findAll(r=>r.get(\"v0\").isNonNull)\n    \n    // Handle Edge-case (Empty His)\n    if (his.isEmpty or his.missing(\"v0\")) return out = null\n    \n    // Fold column on max to find the max steam demand\n    maxSteamDemand : his.foldCol(\"v0\", max).to(1klb/h)\n    minSteamDemand : his.foldCol(\"v0\", min).to(1klb/h)\n    out = {min: minSteamDemand, max: maxSteamDemand}\n  end\nend",
      "diff": "@@ -10,7 +10,7 @@   target:              {}\n   date:                {}\n   out:                 {readonly}\n-  steam_intervalCons:  {bind:\"steam and interval and equipRef->siteMeter and not cost and not modelPoint and not savings and equipRef=={{target->id}}\"}\n+  steam_intervalCons:  {bind:\"steam and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings and equipRef=={{target->id}}\"}\n   opts:                {bindTuning:\"opts\",      defVal:{}}\n \n   do\n"
    },
    {
      "name": "logic_ahu_ahuCoolingLoad",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(ahu, dates, opts:{}, pointOverrides:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  ahuRec: ahu.toRec\n  ahuId: ahuRec->id\n  dates = dates.toSpan\n\n  //opts\n  rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n  if (rollupOverride.isNull and (dates.end - dates.start).to(1hr)<=25hr) rollupOverride=1hr\n  constant: if (opts.has(\"constant\")) opts.get(\"constant\") else 1\n  debug: opts.has(\"debug\") and opts.get(\"debug\")!=\"Disable\"\n  occDisp: opts.optNorm(\"occDisp\", false)\n\n  //get points\n  requiredPoints: [\"mat_sensor\",\"dat_sensor\"]\n  optionalPoints: [\"daFlow_sensor\",\"oat_sensor\"]\n  points: retrievePoints(requiredPoints, optionalPoints, {equipRef:ahuId}, pointOverrides)\n  if (not points.get(\"oat_sensor\").isRef) points = points.set(\"oat_sensor\", try ahuToOutsideTemp(ahuRec).get(\"id\") catch \"missing point\") //CHANGED\n  //REMOVED if (not points.get(\"htgValve_cmd\").isRef) points=points.set(\"htgValve_cmd\", pointQuery(\"phtValve_cmd\", {equipRef:ahuId}))\n  if (debug and opts.get(\"debug\")==\"Points\") return points\n\n  //get vavs and points\n  vavs: readAll(vav and airRef==ahuId)\n  if (vavs.size==0 and not points.get(\"daFlow_sensor\").isRef) return \"No flow point, and no vavs connected\"\n\n  vavFlowPoints: vavs.map(r => pointQuery(\"znflow_sensor\", {equipRef:r->id}))\n  if (vavFlowPoints.size==0 and not points.get(\"daFlow_sensor\").isRef) return \"No vav flow points\"\n\n  //get his\n  if(occDisp) hisDict: points.retrieveHis(dates, {})\n  else hisDict: points.retrieveHis(dates, {rollupOverride:rollupOverride})\n  if (debug and opts.get(\"debug\")==\"His\") return hisDict\n  if (hisDict.has(\"err\")) throw {dis:hisDict.get(\"err\").size+\" errors\", errList:hisDict.get(\"err\")}\n  his: try do\n        if(occDisp) hisDict.vals.findAll(v=>v.isGrid).hisJoin.hisFindInPeriods(logic_ahu_occPeriods(ahuId, dates, {}, {})).hisRollupAuto(rollupOverride)\n        else hisDict.vals.findAll(v=>v.isGrid).hisJoin\n       end\n       catch return \"no his\"\n\n  if(occDisp) vavFlowPointsHis: vavFlowPoints.hisRead(dates,{-limit}).hisFindInPeriods(logic_ahu_occPeriods(ahuId, dates, {}, {})).hisRollupAuto(rollupOverride, his=>avg)\n  else vavFlowPointsHis: vavFlowPoints.hisRead(dates,{-limit}).hisRollupAuto(rollupOverride, his=>avg)\n  if (vavFlowPointsHis.hisClip.size==0) \"No vav flow his\"\n  else vavFlowPointsHis=vavFlowPointsHis.hisFoldCols(sum).renameCol(\"v0\",\"flow\")\n\n  flowHis: if (points.get(\"daFlow_sensor\").isRef and hisDict.get(\"daFlow_sensor\").isGrid) hisDict.get(\"daFlow_sensor\").renameCol(\"daFlow_sensor\",\"flow\")\n      else if (vavFlowPoints.size>0 and vavFlowPointsHis.size>0) vavFlowPointsHis\n      else null\n  if (flowHis.isNull) return \"Cannot determine ahu flow\"\n\n  //combine his\n  his=[his, flowHis].hisJoin\n\n  //calculate heating load\n  coolingLoad: his.map row => do\n    temp1: row.get(\"mat_sensor\")\n    temp2: row.get(\"dat_sensor\")\n    deltaT: temp2-temp1\n    flow: row.get(\"flow\")\n    btu: constant*flow*deltaT\n    if(btu.isNonNull) return {ts:row->ts, coolLoad:btu.as(\"BTU/h\")} //ADDED\n    else return null //ADDED\n  end\n\n  //sum up heating load over dates\n  coolLoad: coolingLoad.reorderCols([\"ts\",\"coolLoad\"])\n  out: coolLoad\n  out=out.addColMetaClean(\"coolLoad\", {dis:ahuRec.dis+\" Cool Load\"})\n\n  return out\n\nend",
      "local_src": "(ahu, dates, opts:{}, pointOverrides:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  ahuRec: ahu.toRec\n  ahuId: ahuRec->id\n\n  //opts\n  rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n  if (rollupOverride.isNull and (dates.end - dates.start).to(1hr)<=25hr) rollupOverride=1hr\n  constant: if (opts.has(\"constant\")) opts.get(\"constant\") else 1\n  debug: opts.has(\"debug\") and opts.get(\"debug\")!=\"Disable\"\n\n  //get points\n  requiredPoints: [\"mat_sensor\",\"dat_sensor\"]\n  optionalPoints: [\"daFlow_sensor\",\"oat_sensor\"]\n  points: retrievePoints(requiredPoints, optionalPoints, {equipRef:ahuId}, pointOverrides)\n  if (not points.get(\"oat_sensor\").isRef) points = points.set(\"oat_sensor\", try ahuToOutsideTemp(ahuRec).get(\"id\") catch \"missing point\") //CHANGED\n  //REMOVED if (not points.get(\"htgValve_cmd\").isRef) points=points.set(\"htgValve_cmd\", pointQuery(\"phtValve_cmd\", {equipRef:ahuId}))\n  if (debug and opts.get(\"debug\")==\"Points\") return points\n\n  //get vavs and points\n  vavs: readAll(vav and airRef==ahuId)\n  if (vavs.size==0 and not points.get(\"daFlow_sensor\").isRef) return \"No flow point, and no vavs connected\"\n\n  vavFlowPoints: vavs.map(r => pointQuery(\"znflow_sensor\", {equipRef:r->id}))\n  if (vavFlowPoints.size==0 and not points.get(\"daFlow_sensor\").isRef) return \"No vav flow points\"\n\n  //get his\n  hisDict: points.retrieveHis(dates, {rollupOverride:rollupOverride})\n  if (debug and opts.get(\"debug\")==\"His\") return hisDict\n  if (hisDict.has(\"err\")) throw {dis:hisDict.get(\"err\").size+\" errors\", errList:hisDict.get(\"err\")}\n  his: try hisDict.vals.findAll(v=>v.isGrid).hisJoin\n       catch return \"no his\"\n\n  vavFlowPointsHis: vavFlowPoints.hisRead(dates,{-limit}).hisRollupAuto(rollupOverride, his=>avg)\n  if (vavFlowPointsHis.hisClip.size==0) \"No vav flow his\"\n  else vavFlowPointsHis=vavFlowPointsHis.hisFoldCols(sum).renameCol(\"v0\",\"flow\")\n\n  flowHis: if (points.get(\"daFlow_sensor\").isRef and hisDict.get(\"daFlow_sensor\").isGrid) hisDict.get(\"daFlow_sensor\").renameCol(\"daFlow_sensor\",\"flow\")\n      else if (vavFlowPoints.size>0 and vavFlowPointsHis.size>0) vavFlowPointsHis\n      else null\n  if (flowHis.isNull) return \"Cannot determine ahu flow\"\n\n  //combine his\n  his=[his, flowHis].hisJoin\n\n  //calculate heating load\n  coolingLoad: his.map row => do\n    temp1: row.get(\"mat_sensor\")\n    temp2: row.get(\"dat_sensor\")\n    deltaT: temp2-temp1\n    flow: row.get(\"flow\")\n    btu: constant*flow*deltaT\n    if(btu.isNonNull) return {ts:row->ts, coolLoad:btu.as(\"BTU/h\")} //ADDED\n    else return null //ADDED\n  end\n\n  //sum up heating load over dates\n  coolLoad: coolingLoad.reorderCols([\"ts\",\"coolLoad\"])\n  out: coolLoad\n  out=out.addColMetaClean(\"coolLoad\", {dis:ahuRec.dis+\" Cool Load\"})\n\n  return out\n\nend",
      "diff": "@@ -6,14 +6,12 @@   //normalize inputs\n   ahuRec: ahu.toRec\n   ahuId: ahuRec->id\n-  dates = dates.toSpan\n \n   //opts\n   rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n   if (rollupOverride.isNull and (dates.end - dates.start).to(1hr)<=25hr) rollupOverride=1hr\n   constant: if (opts.has(\"constant\")) opts.get(\"constant\") else 1\n   debug: opts.has(\"debug\") and opts.get(\"debug\")!=\"Disable\"\n-  occDisp: opts.optNorm(\"occDisp\", false)\n \n   //get points\n   requiredPoints: [\"mat_sensor\",\"dat_sensor\"]\n@@ -31,18 +29,13 @@   if (vavFlowPoints.size==0 and not points.get(\"daFlow_sensor\").isRef) return \"No vav flow points\"\n \n   //get his\n-  if(occDisp) hisDict: points.retrieveHis(dates, {})\n-  else hisDict: points.retrieveHis(dates, {rollupOverride:rollupOverride})\n+  hisDict: points.retrieveHis(dates, {rollupOverride:rollupOverride})\n   if (debug and opts.get(\"debug\")==\"His\") return hisDict\n   if (hisDict.has(\"err\")) throw {dis:hisDict.get(\"err\").size+\" errors\", errList:hisDict.get(\"err\")}\n-  his: try do\n-        if(occDisp) hisDict.vals.findAll(v=>v.isGrid).hisJoin.hisFindInPeriods(logic_ahu_occPeriods(ahuId, dates, {}, {})).hisRollupAuto(rollupOverride)\n-        else hisDict.vals.findAll(v=>v.isGrid).hisJoin\n-       end\n+  his: try hisDict.vals.findAll(v=>v.isGrid).hisJoin\n        catch return \"no his\"\n \n-  if(occDisp) vavFlowPointsHis: vavFlowPoints.hisRead(dates,{-limit}).hisFindInPeriods(logic_ahu_occPeriods(ahuId, dates, {}, {})).hisRollupAuto(rollupOverride, his=>avg)\n-  else vavFlowPointsHis: vavFlowPoints.hisRead(dates,{-limit}).hisRollupAuto(rollupOverride, his=>avg)\n+  vavFlowPointsHis: vavFlowPoints.hisRead(dates,{-limit}).hisRollupAuto(rollupOverride, his=>avg)\n   if (vavFlowPointsHis.hisClip.size==0) \"No vav flow his\"\n   else vavFlowPointsHis=vavFlowPointsHis.hisFoldCols(sum).renameCol(\"v0\",\"flow\")\n \n"
    },
    {
      "name": "logic_anomaly_anomalyImpact",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/*\n*   Description :\n*    Converts interval electric power consumption (kW) meter readings into total electric consumption (kWh) recorded at that meter for those dates.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref/Rec                meter           site electric meter rec/ref\n*     span                   dates           dates to calculate the total electric consumption for\n*     dict                   opts\n*                             - debug         Available options: \"all anomalies\", \"filtered anomalies\", \"filtered anomaly count\", \"disable\", \"enable\"\n*\n*   Output :\n*     Type                   Name            Description\n*     Number                 out            - total electric consumption (kWh) recorded at that meter for those dates\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         12/16/2024      - Initial Creation\n*/\n\n// Example of how to use/test\n// // Case 1: For a random elec meter // Also try: @p:kwLink_energyAgent:r:2ee9da1e-d45ae1e8\n// totalUsage: logic_elecMeter_usage(@p:kwLink_energyAgent:r:2ee9da19-1ea7d73f, 2023)\n// totalKWH: 0\n// logic_elecMeter_usage(@p:kwLink_energyAgent:r:2ee9da19-1ea7d73f, 2023, {debug:\"his\"}).get(\"elec_intervalPwr\").each() r=> do\n//   currentUse: r.get(\"elec_intervalPwr\")\n//   if(currentUse.isNumber) totalKWH = totalKWH + currentUse else totalKWH = totalKWH\n// end\n// // Double checking logic\n// return {totalUsage:totalUsage, totalKWH:totalKWH}\n// ",
      "local_src": "/*\n*   Description :\n*    Converts interval electric power consumption (kW) meter readings into total electric consumption (kWh) recorded at that meter for those dates.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref/Rec                meter           site electric meter rec/ref\n*     span                   dates           dates to calculate the total electric consumption for\n*     dict                   opts\n*                             - debug         Available options: \"all anomalies\", \"filtered anomalies\", \"filtered anomaly count\", \"disable\", \"enable\"\n*\n*   Output :\n*     Type                   Name            Description\n*     Number                 out            - total electric consumption (kWh) recorded at that meter for those dates\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         12/16/2024      - Initial Creation\n*/\n\n// Example of how to use/test\n// // Case 1: For a random elec meter // Also try: @p:kwLink_energyAgent:r:2ee9da1e-d45ae1e8\n// totalUsage: logic_elecMeter_usage(@p:kwLink_energyAgent:r:2ee9da19-1ea7d73f, 2023)\n// totalKWH: 0\n// logic_elecMeter_usage(@p:kwLink_energyAgent:r:2ee9da19-1ea7d73f, 2023, {debug:\"his\"}).get(\"elec_intervalPwr\").each() r=> do\n//   currentUse: r.get(\"elec_intervalPwr\")\n//   if(currentUse.isNumber) totalKWH = totalKWH + currentUse else totalKWH = totalKWH\n// end\n// // Double checking logic\n// return {totalUsage:totalUsage, totalKWH:totalKWH}\n// ------------------------------------------------------------------------\n// // Case 2: For a random gas meter (fails and throws err)\n// logic_elecMeter_usage(@p:kwLink_energyAgent:r:2ee9da19-8d561eae, 2023)\n// ------------------------------------------------------------------------\n\n(meter, dates, opts:{}, pointOverrides:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  if (meter.isNull) return \"meter point missing\"\n  meterRec: meter.toRec\n  meterId: meterRec.get(\"id\")\n  dates = dates.toSpan\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  debug: opts.optNorm(\"debug\", \"Disable\").lower // Available options: \"all anomalies\", \"filtered anomalies\", \"filtered anomaly count\"\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // Points\n  allAnomalies: readAll(anomaly and targetVarRef==meterId)\n    if (debug==\"all anomalies\")   return allAnomalies\n\n  // Only keep the anomalies within the selected span\n  filteredAnomalies: allAnomalies.findAll(v=> v.get(\"ts\") >= dates.start and v.get(\"ts\") < dates.end )  // Robust: check if ts + duration is outside dates.end? Would we still count it as an anomaly for this span?\n    if (debug==\"filtered anomalies\")           return filteredAnomalies\n    if (debug==\"filtered anomaly count\")       return filteredAnomalies.size\n\n  // Calculate total anomaly impact over the selected span\n  totalAnomalyImpact: 0\n\n  filteredAnomalies.each() r=> do\n    anomalyImpact: r.get(\"anomalyImpactLikely\")\n    totalAnomalyImpact = if (anomalyImpact >= 0) totalAnomalyImpact + anomalyImpact else if (anomalyImpact < 0) totalAnomalyImpact  // workaround for now. TODO: Update the anomaly detection funcs to include +ve only anomalies.\n  end\n\n\n    if (debug==\"enable\") return {errList: [\"TODO: Add potential errors above in try catch statements and list them out here\"], powerGrid:powerGrid, usageGrid:usageGrid, totalUsage:totalUsage}\n\n  // TODO: Add potential errors above in try catch statements\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // FORMAT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  //RETURN RESULT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  return totalAnomalyImpact\n\nend",
      "diff": "@@ -28,4 +28,63 @@ // end\n // // Double checking logic\n // return {totalUsage:totalUsage, totalKWH:totalKWH}\n-//+// ------------------------------------------------------------------------\n+// // Case 2: For a random gas meter (fails and throws err)\n+// logic_elecMeter_usage(@p:kwLink_energyAgent:r:2ee9da19-8d561eae, 2023)\n+// ------------------------------------------------------------------------\n+\n+(meter, dates, opts:{}, pointOverrides:{}) => do\n+\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+  // NORMALIZE + VALIDATE INPUTS\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+\n+  if (meter.isNull) return \"meter point missing\"\n+  meterRec: meter.toRec\n+  meterId: meterRec.get(\"id\")\n+  dates = dates.toSpan\n+\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+  // OPTS\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+\n+  debug: opts.optNorm(\"debug\", \"Disable\").lower // Available options: \"all anomalies\", \"filtered anomalies\", \"filtered anomaly count\"\n+\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+  // LOGIC\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+\n+  // Points\n+  allAnomalies: readAll(anomaly and targetVarRef==meterId)\n+    if (debug==\"all anomalies\")   return allAnomalies\n+\n+  // Only keep the anomalies within the selected span\n+  filteredAnomalies: allAnomalies.findAll(v=> v.get(\"ts\") >= dates.start and v.get(\"ts\") < dates.end )  // Robust: check if ts + duration is outside dates.end? Would we still count it as an anomaly for this span?\n+    if (debug==\"filtered anomalies\")           return filteredAnomalies\n+    if (debug==\"filtered anomaly count\")       return filteredAnomalies.size\n+\n+  // Calculate total anomaly impact over the selected span\n+  totalAnomalyImpact: 0\n+\n+  filteredAnomalies.each() r=> do\n+    anomalyImpact: r.get(\"anomalyImpactLikely\")\n+    totalAnomalyImpact = if (anomalyImpact >= 0) totalAnomalyImpact + anomalyImpact else if (anomalyImpact < 0) totalAnomalyImpact  // workaround for now. TODO: Update the anomaly detection funcs to include +ve only anomalies.\n+  end\n+\n+\n+    if (debug==\"enable\") return {errList: [\"TODO: Add potential errors above in try catch statements and list them out here\"], powerGrid:powerGrid, usageGrid:usageGrid, totalUsage:totalUsage}\n+\n+  // TODO: Add potential errors above in try catch statements\n+\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+  // FORMAT\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+\n+\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+  //RETURN RESULT\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+\n+  return totalAnomalyImpact\n+\n+end"
    },
    {
      "name": "logic_chw_supTempSpReset",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*    Given a Chilled Water System, returns when the supply temperature\n*    setpoint is resetting.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    target          - The chws ref.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     ?                      ?           - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024   - Added loop override support (Line 47)\n*/\n(target, dates, opts:{}, pointOverrides:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  equipRec: if (target.toRec.has(\"site\"))\n              try   read(equip and chilled and plant and water and siteRef == targetId)\n              catch return blankChart(\"No CHWS in this Site\")\n            else targetRec\n  equipId: equipRec->id\n\n  siteRec: if(targetRec.has(\"site\")) target.toRec else targetRec.get(\"siteRef\")\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  //opts\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n  minChanges:       if (opts.has(\"minChanges\")) opts.get(\"minChanges\") else 6\n  mustChangeBy:     if (opts.has(\"mustChangeBy\")) opts.get(\"mustChangeBy\") else 0.1\n  lookBack:         if (opts.has(\"lookBack\")) opts.get(\"lookBack\") else 3mo\n  peggedDeadband:  if (opts.has(\"peggedDeadband\")) opts.get(\"peggedDeadband\") else 0.5\n\n  minMaxColor: \"red\"\n\n  //points\n  points: buildingLoopDigest(equipId, opts)\n  try chw_supply_temp_sp: points.get(\"buildingLoop\").first.get(\"chw_supply_temp_sp\") catch return blankChart(\"No Chilled Water Loops Present\")\n  try chw_supply_temp: points.get(\"buildingLoop\").first.get(\"chw_supply_temp\") catch return blankChart(\"No Chilled Water Loops Present\")\n  points ={chw_supply_temp_sp:chw_supply_temp_sp, chw_supply_temp:chw_supply_temp}\n\n  //return points\n  if (debug and opts.get(\"debug\")==\"Points\") return points\n  if (points.has(\"err\")) return \"missingPoint\"\n\n  //get his\n  hisDict: points.retrieveHis(dates, {rollupOverride:rollupOverride})\n  if (debug and opts.get(\"debug\")==\"His\") return hisDict\n  if (hisDict.has(\"err\")) return \"missingHis\"\n  his: try hisDict.vals.findAll(v=>v.isGrid).hisJoin\n       catch return \"missingHis\"\n\n  //look back in time to find min/max\n  searchStart: dates.start - lookBack\n  searchSpan: searchStart..dates.end\n  try extendedHis: points.get(\"chw_supply_temp_sp\").hisRead(searchSpan, {-limit}) catch return blankChart(\"Supply Temp SP doesn't exist\")\n  upperLimit: extendedHis.foldCol(\"v0\", max) //percentile90)\n  lowerLimit: extendedHis.colToList(\"v0\").findAll(v=>v.isNumber and v>0).fold(min) //percentile10)\n\n  //construct his grid\n  out:  his.addCol(\"hisMax\", r => upperLimit)\n           .addCol(\"hisMin\", r => lowerLimit)\n           .addColDelta(\"chw_supply_temp_sp\")\n\n  //count times value changes\n  if (not out.colNames.contains(\"delta\")) return \"Cannot calculate delta for Supply Temp Point\"\n  changes: out.colToList(\"delta\").findAll(v=>v>mustChangeBy)\n  changesCount: if (changes.isEmpty) 0 else changes.size\n  uniqueVals: his.colToList(\"chw_supply_temp_sp\").unique\n  singleVal: if (uniqueVals.size==1) uniqueVals.first\n             else null\n\n  //see if pegged at max/min\n  peggedMax: if ((singleVal.isNumber) and (singleVal >= upperLimit or abs(singleVal-upperLimit)<=peggedDeadband)) true else false\n  peggedMin: if ((singleVal.isNumber) and (singleVal <= lowerLimit or abs(singleVal-lowerLimit)<=peggedDeadband)) true else false\n\n  //determine out\n  outString: if (changesCount > minChanges) \"yes\"\n             else if (lowerLimit == upperLimit) \"noConstant\"\n             else if (peggedMax) \"noPeggedMax\"\n             else if (peggedMin) \"noPeggedMin\"\n             else \"no\"\n\n  outMeta: {numChanges: changesCount,\n            datResetting: outString}\n\n  //output\n  subtitle: if (outString==\"yes\") \"Reset detected!\"\n            else if (outString==\"noConstant\") \"No reset detected, value has been constant for at least \"+lookBack.toStr\n            else if (outString==\"noPeggedMax\") \"No reset detected, value is at the max value \"+singleVal.format(\"#.##\")\n            else if (outString==\"noPeggedMin\") \"No reset detected, value is at the min value \"+singleVal.format(\"#.##\")\n            else if (outString==\"no\") \"No reset detected\"\n  out = out.reorderCols([\"ts\",\"hisMin\",\"chw_supply_temp_sp\", \"chw_supply_temp\", \"hisMax\"])\n  out=out.addColMetaClean(\"hisMax\",{dis:\"Max: \"+upperLimit.format(\"#.##\")+\" (last \"+lookBack.toStr+\")\", color:minMaxColor, strokeDasharray:\"4,10\",chartGroup:\"supTemp\",chartType:\"area\"})\n         .addColMetaClean(\"hisMin\",{dis:\"Min: \"+lowerLimit.format(\"#.##\")+\" (last \"+lookBack.toStr+\")\", color:minMaxColor, strokeDasharray:\"4,10\",chartGroup:\"supTemp\",chartType:\"area\"})\n         .addColMetaClean(\"chw_supply_temp_sp\",{chartGroup:\"supTemp\", title:\"CHWS Supply Temp SP Reset Analytics\", subtitle:subtitle, chartType:\"area\", opacity:0.3})\n         .addColMetaClean(\"chw_supply_temp\",{chartGroup:\"supTemp\"})\n\n  out = out.removeCol(\"delta\")\n\n  return out.addMeta(outMeta)\n\nend",
      "local_src": "/*\n*   Description :\n*    Given a Chilled Water System, returns when the supply temperature\n*    setpoint is resetting.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    target          - The chws ref.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     ?                      ?           - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024   - Added loop override support (Line 47)\n*/\n(target, dates, opts:{}, pointOverrides:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  equipRec: if (target.toRec.has(\"site\"))\n              try   read(equip and chilled and plant and water and siteRef == targetId)\n              catch return blankChart(\"No CHWS in this Site\")\n            else targetRec\n  equipId: equipRec->id\n\n  siteRec: if(targetRec.has(\"site\")) target.toRec else targetRec.get(\"siteRef\")\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  //opts\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n  minChanges:       if (opts.has(\"minChanges\")) opts.get(\"minChanges\") else 6\n  mustChangeBy:     if (opts.has(\"mustChangeBy\")) opts.get(\"mustChangeBy\") else 0.1\n  lookBack:         if (opts.has(\"lookBack\")) opts.get(\"lookBack\") else 3mo\n  peggedDeadband:  if (opts.has(\"peggedDeadband\")) opts.get(\"peggedDeadband\") else 0.5\n\n  minMaxColor: \"red\"\n\n  //points\n  points: buildingLoopDigest(equipId, opts)\n  plantId: readById(points.get(\"buildingLoop\").first.get(\"chw_supply_temp\"))->equipRef->equipRef.nestedEquips.find(e=>e.get(\"navName\").contains(\"Primary\"))->id\n  priVlvCmd: try read(primary and cmd and valve and equipRef == plantId) catch null\n  priVlvPos: try read(primary and valve and position and equipRef == plantId) catch null\n \n  try chw_supply_temp_sp: points.get(\"buildingLoop\").first.get(\"chw_supply_temp_sp\") catch return blankChart(\"No Chilled Water Loops Present\")\n  try chw_supply_temp: points.get(\"buildingLoop\").first.get(\"chw_supply_temp\") catch return blankChart(\"No Chilled Water Loops Present\")\n  points ={chw_supply_temp_sp:chw_supply_temp_sp, chw_supply_temp:chw_supply_temp}\n\n  //return points\n  if (debug and opts.get(\"debug\")==\"Points\") return points\n  if (points.has(\"err\")) return \"missingPoint\"\n\n  //get his\n  hisDict: points.retrieveHis(dates, {rollupOverride:rollupOverride})\n  if (debug and opts.get(\"debug\")==\"His\") return hisDict\n  if (hisDict.has(\"err\")) return \"missingHis\"\n  his: try hisDict.vals.findAll(v=>v.isGrid).hisJoin\n       catch return \"missingHis\"\n       \n       \n  priVlvCmdHis: try priVlvCmd.hisRead(dates) catch null\n  priVlvPosHis: try priVlvPos.hisRead(dates) catch null\n\n  //look back in time to find min/max\n  searchStart: dates.start - lookBack\n  searchSpan: searchStart..dates.end\n  try extendedHis: points.get(\"chw_supply_temp_sp\").hisRead(searchSpan, {-limit}) catch return blankChart(\"Supply Temp SP doesn't exist\")\n  upperLimit: extendedHis.foldCol(\"v0\", max) //percentile90)\n  lowerLimit: extendedHis.colToList(\"v0\").findAll(v=>v.isNumber and v>0).fold(min) //percentile10)\n\n  //construct his grid\n  out:  his.addCol(\"hisMax\", r => upperLimit)\n           .addCol(\"hisMin\", r => lowerLimit)\n           .addColDelta(\"chw_supply_temp_sp\")\n\n  //count times value changes\n  if (not out.colNames.contains(\"delta\")) return \"Cannot calculate delta for Supply Temp Point\"\n  changes: out.colToList(\"delta\").findAll(v=>v>mustChangeBy)\n  changesCount: if (changes.isEmpty) 0 else changes.size\n  uniqueVals: his.colToList(\"chw_supply_temp_sp\").unique\n  singleVal: if (uniqueVals.size==1) uniqueVals.first\n             else null\n\n  //see if pegged at max/min\n  peggedMax: if ((singleVal.isNumber) and (singleVal >= upperLimit or abs(singleVal-upperLimit)<=peggedDeadband)) true else false\n  peggedMin: if ((singleVal.isNumber) and (singleVal <= lowerLimit or abs(singleVal-lowerLimit)<=peggedDeadband)) true else false\n\n  //determine out\n  outString: if (changesCount > minChanges) \"yes\"\n             else if (lowerLimit == upperLimit) \"noConstant\"\n             else if (peggedMax) \"noPeggedMax\"\n             else if (peggedMin) \"noPeggedMin\"\n             else \"no\"\n\n  outMeta: {numChanges: changesCount,\n            datResetting: outString}\n\n  //output\n  subtitle: if (outString==\"yes\") \"Reset detected!\"\n            else if (outString==\"noConstant\") \"No reset detected, value has been constant for at least \"+lookBack.toStr\n            else if (outString==\"noPeggedMax\") \"No reset detected, value is at the max value \"+singleVal.format(\"#.##\")\n            else if (outString==\"noPeggedMin\") \"No reset detected, value is at the min value \"+singleVal.format(\"#.##\")\n            else if (outString==\"no\") \"No reset detected\"\n  out = out.reorderCols([\"ts\",\"hisMin\",\"chw_supply_temp_sp\", \"chw_supply_temp\", \"hisMax\"])\n  out=out.addColMetaClean(\"hisMax\",{dis:\"Max: \"+upperLimit.format(\"#.##\")+\" (last \"+lookBack.toStr+\")\", color:minMaxColor, strokeDasharray:\"4,10\",chartGroup:\"supTemp\",chartType:\"area\"})\n         .addColMetaClean(\"hisMin\",{dis:\"Min: \"+lowerLimit.format(\"#.##\")+\" (last \"+lookBack.toStr+\")\", color:minMaxColor, strokeDasharray:\"4,10\",chartGroup:\"supTemp\",chartType:\"area\"})\n         .addColMetaClean(\"chw_supply_temp_sp\",{chartGroup:\"supTemp\", title:\"CHWS Supply Temp SP Reset Analytics\", subtitle:subtitle, chartType:\"area\", opacity:0.3})\n         .addColMetaClean(\"chw_supply_temp\",{chartGroup:\"supTemp\"})\n\n  out = out.removeCol(\"delta\")\n  out = [out, priVlvCmdHis, priVlvPosHis].hisJoin\n\n  return out.addMeta(outMeta)\nend",
      "diff": "@@ -44,6 +44,10 @@ \n   //points\n   points: buildingLoopDigest(equipId, opts)\n+  plantId: readById(points.get(\"buildingLoop\").first.get(\"chw_supply_temp\"))->equipRef->equipRef.nestedEquips.find(e=>e.get(\"navName\").contains(\"Primary\"))->id\n+  priVlvCmd: try read(primary and cmd and valve and equipRef == plantId) catch null\n+  priVlvPos: try read(primary and valve and position and equipRef == plantId) catch null\n+\n   try chw_supply_temp_sp: points.get(\"buildingLoop\").first.get(\"chw_supply_temp_sp\") catch return blankChart(\"No Chilled Water Loops Present\")\n   try chw_supply_temp: points.get(\"buildingLoop\").first.get(\"chw_supply_temp\") catch return blankChart(\"No Chilled Water Loops Present\")\n   points ={chw_supply_temp_sp:chw_supply_temp_sp, chw_supply_temp:chw_supply_temp}\n@@ -58,6 +62,10 @@   if (hisDict.has(\"err\")) return \"missingHis\"\n   his: try hisDict.vals.findAll(v=>v.isGrid).hisJoin\n        catch return \"missingHis\"\n+\n+\n+  priVlvCmdHis: try priVlvCmd.hisRead(dates) catch null\n+  priVlvPosHis: try priVlvPos.hisRead(dates) catch null\n \n   //look back in time to find min/max\n   searchStart: dates.start - lookBack\n@@ -106,7 +114,7 @@          .addColMetaClean(\"chw_supply_temp\",{chartGroup:\"supTemp\"})\n \n   out = out.removeCol(\"delta\")\n+  out = [out, priVlvCmdHis, priVlvPosHis].hisJoin\n \n   return out.addMeta(outMeta)\n-\n end"
    },
    {
      "name": "logic_chws_deltaT",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*    Given a Chilled Water System, returns the delta T of the\n*    building loop between the supply and return temperatures.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    target          - The chws ref.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/05/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024   - Added edge case handling fix. Added loop override support (Lines 26 & 53-54)\n*/\n(target, dates, opts : {}) => do\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  site: targetRec.get(\"siteRef\")\n  dates = dates.toSpan()\n\n  // Retrieve Points\n  points: buildingLoopDigest(targetId, opts)\n\n  // Missing building loop edge-case handling.\n  if (points.isStr or points.get(\"buildingLoop\").isStr) return \"No building loop exists.\"\n\n  // Retrieve points\n  supplyTemp: points.get(\"buildingLoop\").first.get(\"chw_supply_temp\")\n  returnTemp: points.get(\"buildingLoop\").first.get(\"chw_return_temp\")\n\n  // Missing points edge-case handling.\n  if (supplyTemp.isStr and returnTemp.isStr) return \"Neither supply nor return temperature points exists.\"\n  if (supplyTemp.isStr)                      return \"No supply temp point exists.\"\n  if (returnTemp.isStr)                      return \"no return temp point exists.\"\n\n  // Retrieve his\n  supplyTempHis: supplyTemp.hisRead(dates, {-limit}).hisInterpolate.hisClip\n  returnTempHis: returnTemp.hisRead(dates, {-limit}).hisInterpolate.hisClip\n\n  // Missing His edge-case handling.\n  if (supplyTempHis.isNull or supplyTempHis.isEmpty) return \"No his data for supply temp point\"\n  if (returnTempHis.isNull or returnTempHis.isEmpty) return \"No his data for return temp point\"\n\n  // Combine his points, and calculate delta\n  his : hisJoin([supplyTempHis, returnTempHis]).hisInterpolate\n  his = his.addColTwoPointDelta(\"delta\")\n\n  //Filter for when any building loop pump is running\n  opts = opts.set(\"id\", true)\n  bLoopId: buildingLoopDigest(targetId, opts)\n\n  // Call Logic Func\n  pumpRunningHis: logic_chws_equipsRunning(bLoopId, dates, {equip:\"pump\"})\n\n  // Handle edge-case\n  if (pumpRunningHis.isStr) return pumpRunningHis\n\n  pumpRunningHis = pumpRunningHis.keepCols([\"ts\", \"anyEquipRunning\"]).findAll(r => r.get(\"anyEquipRunning\").isNonNull)\n\n  if(pumpRunningHis.isNonNull) his = his.hisFindInPeriods(pumpRunningHis)\n\n  his = his.map() r => if(r.get(\"delta\") < 0) r = r.set(\"delta\", abs(r.get(\"delta\"))) else r = r\n\n  // CHWS 2.0 Preparation\n  funcName: \"logic_chws_deltaT\"\n  titles: logicFuncTitle(funcName, targetRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  his = his.stream\n           .addMeta(outputMeta)\n           .collect\n  return his\nend",
      "local_src": "/*\n*   Description :\n*    Given a Chilled Water System, returns the delta T of the\n*    building loop between the supply and return temperatures.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    target          - The chws ref.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/05/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024   - Added edge case handling fix. Added loop override support (Lines 26 & 53-54)\n*/\n(target, dates, opts : {}) => do\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  site: targetRec.get(\"siteRef\")\n  dates = dates.toSpan()\n\n  // Retrieve Points\n  points: buildingLoopDigest(targetId, opts)\n\n  // Missing building loop edge-case handling.\n  if (points.isStr or points.get(\"buildingLoop\").isStr) return \"No building loop exists.\"\n\n  // Retrieve points\n  supplyTemp: points.get(\"buildingLoop\").first.get(\"chw_supply_temp\")\n  returnTemp: points.get(\"buildingLoop\").first.get(\"chw_return_temp\")\n\n  // Missing points edge-case handling.\n  if (supplyTemp.isStr and returnTemp.isStr) return \"Neither supply nor return temperature points exists.\"\n  if (supplyTemp.isStr)                      return \"No supply temp point exists.\"\n  if (returnTemp.isStr)                      return \"no return temp point exists.\"\n\n  // Retrieve his\n  supplyTempHis: supplyTemp.hisRead(dates, {-limit}).hisInterpolate.hisClip\n  returnTempHis: returnTemp.hisRead(dates, {-limit}).hisInterpolate.hisClip\n\n  // Missing His edge-case handling.\n  if (supplyTempHis.isNull or supplyTempHis.isEmpty) return \"No his data for supply temp point\"\n  if (returnTempHis.isNull or returnTempHis.isEmpty) return \"No his data for return temp point\"\n\n  // Combine his points, and calculate delta\n  his : hisJoin([supplyTempHis, returnTempHis]).hisInterpolate\n  his = his.addColTwoPointDelta(\"delta\")\n\n  //Filter for when any building loop pump is running\n  opts = opts.set(\"id\", true)\n  bLoopId: buildingLoopDigest(targetId, opts)\n\n  // Call Logic Func\n  pumpRunningHis: if (opts.get(\"secondaryLoopOverride\") != \"null\") logic_chws_equipsRunning(bLoopId, dates, {equip:\"pump\"}) else null\n\n  // Handle edge-case\n  if (pumpRunningHis.isStr) return pumpRunningHis\n\n  pumpRunningHis = try pumpRunningHis.keepCols([\"ts\", \"anyEquipRunning\"]).findAll(r => r.get(\"anyEquipRunning\").isNonNull) catch null\n\n  if(pumpRunningHis.isNonNull) his = his.hisFindInPeriods(pumpRunningHis)\n\n  his = his.map() r => if(r.get(\"delta\") < 0) r = r.set(\"delta\", abs(r.get(\"delta\"))) else r = r\n\n  // CHWS 2.0 Preparation\n  funcName: \"logic_chws_deltaT\"\n  titles: logicFuncTitle(funcName, targetRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  his = his.stream\n           .addMeta(outputMeta)\n           .collect\n  return his\nend",
      "diff": "@@ -54,12 +54,12 @@   bLoopId: buildingLoopDigest(targetId, opts)\n \n   // Call Logic Func\n-  pumpRunningHis: logic_chws_equipsRunning(bLoopId, dates, {equip:\"pump\"})\n+  pumpRunningHis: if (opts.get(\"secondaryLoopOverride\") != \"null\") logic_chws_equipsRunning(bLoopId, dates, {equip:\"pump\"}) else null\n \n   // Handle edge-case\n   if (pumpRunningHis.isStr) return pumpRunningHis\n \n-  pumpRunningHis = pumpRunningHis.keepCols([\"ts\", \"anyEquipRunning\"]).findAll(r => r.get(\"anyEquipRunning\").isNonNull)\n+  pumpRunningHis = try pumpRunningHis.keepCols([\"ts\", \"anyEquipRunning\"]).findAll(r => r.get(\"anyEquipRunning\").isNonNull) catch null\n \n   if(pumpRunningHis.isNonNull) his = his.hisFindInPeriods(pumpRunningHis)\n \n"
    },
    {
      "name": "logic_chws_energyData",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     Given a Chilled Water System, returns the total energy usage and the\n*     peak demand of the system.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    target          - The chws input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/02/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024   - Passed opts into chwsDigest function call (Line 30)\n*/\n(target, dates, opts : {}) => do\n\n  // Handle Opts\n  quickModeUsage  : opts.get(\"quickModeUsage\") == true\n  quickModeDemand : opts.get(\"quickModeDemand\") == true\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  site: targetRec.get(\"siteRef\")\n  dates = dates.toSpan()\n\n  // Retrieve points\n  points: chwsDigest(targetId, opts)\n\n  // Retrieve loops from logic.\n  primaryLoop  : points.get(\"primaryLoop\").toGrid\n  secondaryLoop: points.get(\"secondaryLoop\").toGrid\n  condenserLoop: points.get(\"condenserLoop\").toGrid\n\n  // Edge-case where no loop has equip data.\n  if (primaryLoop.isStr and secondaryLoop.isStr and condenserLoop.isStr) return \"No equips in any existing loop.\"\n\n  loops: [primaryLoop, secondaryLoop, condenserLoop]\n  equipPowerPoints: []\n  loops.each() (loop) => do\n    if (loop.isGrid) do\n      // Retrieve all possible equip list from loop\n      chillers      : loop.first.get(\"chillers\")\n      pumps         : loop.first.get(\"pumps\")\n      coolingTowers : loop.first.get(\"coolingTowers\")\n\n      // If chillers are present, retrieve its power points if they exist\n      if (chillers.isGrid) do\n        chillerPowerPoints: chillers.colToList(\"points\").map(v => v.get(\"chlr_power\")).findAll(v => v.isRef)\n        if (not chillerPowerPoints.isEmpty) equipPowerPoints = equipPowerPoints.add(chillerPowerPoints)\n      end\n\n      // If pumps are present, rertrieve its power points if they exist.\n      if (pumps.isGrid) do\n        priPowerPoints: pumps.colToList(\"points\").map(v => v.get(\"pri_pump_power\")).findAll(v => v.isRef)\n        secPowerPoints: pumps.colToList(\"points\").map(v => v.get(\"sec_pump_power\")).findAll(v => v.isRef)\n        if (not priPowerPoints.isEmpty) equipPowerPoints = equipPowerPoints.add(priPowerPoints)\n        if (not secPowerPoints.isEmpty) equipPowerPoints = equipPowerPoints.add(secPowerPoints)\n      end\n\n      // If cooling towers are present, retrieve its power points if they exist\n      if (coolingTowers.isGrid) do\n        ctPowerPoints: coolingTowers.colToList(\"points\").map(v => v.get(\"clgtwr_power\")).findAll(v => v.isRef)\n        if (not ctPowerPoints.isEmpty) equipPowerPoints = equipPowerPoints.add(ctPowerPoints)\n      end\n    end\n  end\n  equipPowerPoints = equipPowerPoints.flatten\n\n  // Handle Edge-Case where no equip power points where found, but equips were found.\n  if (equipPowerPoints.isEmpty) return \"No power point in any of the equips of the system.\"\n\n  // Perform a hisRead on all the points\n  equipPowerHis: equipPowerPoints.hisRead(dates, {-limit}).hisInterpolate.hisClip\n\n  // Edge-case for when the points exist, but have no his data for the given dates.\n  if (equipPowerHis.isEmpty) return \"No his data found\"\n\n  // Calculate the system demand and usage.\n  systemDemand : equipPowerHis.hisFoldCols(sum).renameCol(\"v0\", \"systemDemand\")\n  systemUsage  : systemDemand.hisRollup(avg, 1h).hisMap(v => v.as(1kWh)).renameCol(\"systemDemand\", \"systemUsage\")\n\n  // QuickMode\n  if (quickModeUsage)  return systemUsage\n  if (quickModeDemand) return systemDemand\n\n  // Combine grids.\n  his : hisJoin([systemUsage, equipPowerHis, systemDemand])\n\n  // CHWS 2.0 Preparation\n  funcName: \"logic_chws_energyData\"\n  titles: logicFuncTitle(funcName, targetRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  his = his.stream\n           .addColMeta(\"systemUsage\",  {chartGroup: \"usage\", dis: \"Usage (kWh)\", subtitle: \"System Usage\", color: \"blue\"})\n           .addColMeta(\"systemDemand\", {chartGroup: \"demand\", dis: \"Demand (kW)\", subtitle: \"System Demand\", color:\"green\"})\n           .addMeta(outputMeta)\n           .collect\n  return his\nend",
      "local_src": "/*\n*   Description :\n*     Given a Chilled Water System, returns the total energy usage and the\n*     peak demand of the system.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    target          - The chws input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/02/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024   - Passed opts into chwsDigest function call (Line 30)\n*/\n(target, date, opts : {}) => do\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  site: targetRec.get(\"siteRef\")\n  date = date.toSpan()\n\n  // Retrieve points\n  points: try \"chilledWater_intervalCons\".pointQuery({read, siteRef: site}) catch null\n\n  // Handle Edge-Case where no equip power points where found, but equips were found.\n  if (points.isNull or points.isEmpty) return \"No energy point on the system.\"\n\n  // Perform a hisRead on all the points\n  his : points.hisRead(date, {-limit}).hisInterpolate.hisClip\n\n  // Edge-case for when the points exist, but have no his data for the given dates.\n  if (his.isEmpty) return \"No his data found\"\n\n  return his\n   \nend",
      "diff": "@@ -14,94 +14,26 @@ *     Thomas Kuhrke Limia    8/02/2024   - Initial Creation\n *     Thomas Kuhrke Limia    10/8/2024   - Passed opts into chwsDigest function call (Line 30)\n */\n-(target, dates, opts : {}) => do\n-\n-  // Handle Opts\n-  quickModeUsage  : opts.get(\"quickModeUsage\") == true\n-  quickModeDemand : opts.get(\"quickModeDemand\") == true\n+(target, date, opts : {}) => do\n \n   // Normalize Inputs\n   targetRec: target.toRec\n   targetId: targetRec->id\n   site: targetRec.get(\"siteRef\")\n-  dates = dates.toSpan()\n+  date = date.toSpan()\n \n   // Retrieve points\n-  points: chwsDigest(targetId, opts)\n-\n-  // Retrieve loops from logic.\n-  primaryLoop  : points.get(\"primaryLoop\").toGrid\n-  secondaryLoop: points.get(\"secondaryLoop\").toGrid\n-  condenserLoop: points.get(\"condenserLoop\").toGrid\n-\n-  // Edge-case where no loop has equip data.\n-  if (primaryLoop.isStr and secondaryLoop.isStr and condenserLoop.isStr) return \"No equips in any existing loop.\"\n-\n-  loops: [primaryLoop, secondaryLoop, condenserLoop]\n-  equipPowerPoints: []\n-  loops.each() (loop) => do\n-    if (loop.isGrid) do\n-      // Retrieve all possible equip list from loop\n-      chillers      : loop.first.get(\"chillers\")\n-      pumps         : loop.first.get(\"pumps\")\n-      coolingTowers : loop.first.get(\"coolingTowers\")\n-\n-      // If chillers are present, retrieve its power points if they exist\n-      if (chillers.isGrid) do\n-        chillerPowerPoints: chillers.colToList(\"points\").map(v => v.get(\"chlr_power\")).findAll(v => v.isRef)\n-        if (not chillerPowerPoints.isEmpty) equipPowerPoints = equipPowerPoints.add(chillerPowerPoints)\n-      end\n-\n-      // If pumps are present, rertrieve its power points if they exist.\n-      if (pumps.isGrid) do\n-        priPowerPoints: pumps.colToList(\"points\").map(v => v.get(\"pri_pump_power\")).findAll(v => v.isRef)\n-        secPowerPoints: pumps.colToList(\"points\").map(v => v.get(\"sec_pump_power\")).findAll(v => v.isRef)\n-        if (not priPowerPoints.isEmpty) equipPowerPoints = equipPowerPoints.add(priPowerPoints)\n-        if (not secPowerPoints.isEmpty) equipPowerPoints = equipPowerPoints.add(secPowerPoints)\n-      end\n-\n-      // If cooling towers are present, retrieve its power points if they exist\n-      if (coolingTowers.isGrid) do\n-        ctPowerPoints: coolingTowers.colToList(\"points\").map(v => v.get(\"clgtwr_power\")).findAll(v => v.isRef)\n-        if (not ctPowerPoints.isEmpty) equipPowerPoints = equipPowerPoints.add(ctPowerPoints)\n-      end\n-    end\n-  end\n-  equipPowerPoints = equipPowerPoints.flatten\n+  points: try \"chilledWater_intervalCons\".pointQuery({read, siteRef: site}) catch null\n \n   // Handle Edge-Case where no equip power points where found, but equips were found.\n-  if (equipPowerPoints.isEmpty) return \"No power point in any of the equips of the system.\"\n+  if (points.isNull or points.isEmpty) return \"No energy point on the system.\"\n \n   // Perform a hisRead on all the points\n-  equipPowerHis: equipPowerPoints.hisRead(dates, {-limit}).hisInterpolate.hisClip\n+  his : points.hisRead(date, {-limit}).hisInterpolate.hisClip\n \n   // Edge-case for when the points exist, but have no his data for the given dates.\n-  if (equipPowerHis.isEmpty) return \"No his data found\"\n+  if (his.isEmpty) return \"No his data found\"\n \n-  // Calculate the system demand and usage.\n-  systemDemand : equipPowerHis.hisFoldCols(sum).renameCol(\"v0\", \"systemDemand\")\n-  systemUsage  : systemDemand.hisRollup(avg, 1h).hisMap(v => v.as(1kWh)).renameCol(\"systemDemand\", \"systemUsage\")\n+  return his\n \n-  // QuickMode\n-  if (quickModeUsage)  return systemUsage\n-  if (quickModeDemand) return systemDemand\n-\n-  // Combine grids.\n-  his : hisJoin([systemUsage, equipPowerHis, systemDemand])\n-\n-  // CHWS 2.0 Preparation\n-  funcName: \"logic_chws_energyData\"\n-  titles: logicFuncTitle(funcName, targetRec, dates)\n-  outputMeta: {title:titles[0],\n-              subtitle: titles[1],\n-              funcName: funcName,\n-              }\n-\n-  // Add Meta\n-  his = his.stream\n-           .addColMeta(\"systemUsage\",  {chartGroup: \"usage\", dis: \"Usage (kWh)\", subtitle: \"System Usage\", color: \"blue\"})\n-           .addColMeta(\"systemDemand\", {chartGroup: \"demand\", dis: \"Demand (kW)\", subtitle: \"System Demand\", color:\"green\"})\n-           .addMeta(outputMeta)\n-           .collect\n-  return his\n end"
    },
    {
      "name": "logic_chws_equipSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     Given a site, returns a summary of the KPI results for the given equip.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/13/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/7/2024   - Added loop override support.\n*\n*/\n(site, dates, opts: {equip: \"none\"}) => do\n\n  // Retrieve Opts\n  hideEmptyValues   : opts.get(\"hideEmptyValues\") == true\n  valueSource       : if (opts.has(\"valueSource\")) opts.get(\"valueSource\").lower else \"auto\"\n  chwsEquipOverride : if (opts.has(\"chwsEquipOverride\")) opts.get(\"chwsEquipOverride\") else null\n  returnDict        : opts.get(\"returnDict\") == true // TODO\n\n  // Retrieve CHWS\n  chws: if (chwsEquipOverride != \"\" and chwsEquipOverride.isNonNull) try readById(chwsEquipOverride)\n                                                                     catch return \"CHWS Override provided is invalid.\"\n        else logic_chws_siteToChws(site)\n\n  // Handle Edge-Case (Added a check for primary/condenser loop)\n  if ((chws.isNull or chws.isEmpty) and (not opts.get(\"primaryLoopOverride\").isRef and not opts.get(\"condenserLoopOverride\").isRef)) return \"No Chilled Water System on this Site\"\n  chws = try chws.toRec.get(\"id\") catch null\n\n  // Retrieve primary loop (added check to use the override with priority)\n  primaryLoop: if (opts.get(\"primaryLoopOverride\").isRef) opts.get(\"primaryLoopOverride\")\n               else try   read(chilled and equip and water and primaryLoop and output and equipRef == chws)->id\n                    catch null\n\n  // Retrieve secondary loop (added check to use the override with priority)\n  secondaryLoop: if (opts.get(\"secondaryLoopOverride\").isRef) opts.get(\"secondaryLoopOverride\")\n                 else try   read(chilled and equip and water and secondaryLoop and output and equipRef == chws)->id\n                      catch null\n\n  // Retrieve condenser loop (added check to use the override with priority)\n  condLoop: if (opts.get(\"condenserLoopOverride\").isRef) opts.get(\"condenserLoopOverride\")\n            else try   read(chilled and equip and water and condenserLoop and output and equipRef == chws)->id\n                 catch null\n  if (opts.get(\"equip\") == \"cdwp\" and condLoop.isNull) return \"No Condenser Loop Available\"\n\n  // Normalize Input\n  date: dates.toSpan\n\n  // Find targets\n  targets: if      (opts.get(\"equip\") == \"chlr\") try chillersDigest(primaryLoop).colToList(\"equip\")\n                                                 catch (err) return \"No Chillers Available\"\n           else if (opts.get(\"equip\") == \"chwp\") try do\n                                                   list1: pumpsDigest(primaryLoop).colToList(\"equip\")\n                                                   list2: pumpsDigest(secondaryLoop).colToList(\"equip\")\n                                                   [list1, list2].flatten\n                                                 end\n                                                 catch (err) return \"No Chilled Water Loop Pumps Available\"\n           else if (opts.get(\"equip\") == \"cdwp\") try pumpsDigest(condLoop).colToList(\"equip\")         catch (err) return \"No Condenser Loop Pumps Available\"\n           else if (opts.get(\"equip\") == \"ct\")   try coolingTowersDigest(condLoop).colToList(\"equip\") catch (err) return \"No Cooling Towers Available\"\n           else                                  null\n\n  if (targets.isNull) return \"No equips selected\"\n  if (targets.isStr)  return targets\n  targets = targets.sort((a,b) => a->navName <=> b->navName)\n\n  // KPI Funcs (Missing ct -> wetBulb, approach])\n  funcs: if      (opts.get(\"equip\") == \"chlr\") [kpi_chiller_kWTonAvg,\n                                                kpi_chiller_avgPercentFLA,\n                                                kpi_chiller_percentRuntime,\n                                                kpi_chiller_totalChillerEnergyUse,\n                                                kpi_chiller_chillerPeakDemand,\n                                                kpi_chiller_avgChillerLift,\n                                                kpi_chiller_avgDeltaT,\n                                                kpi_chiller_avgCHWLeavingTemp,\n                                                kpi_chiller_avgCDWLeavingTemp,\n                                                kpi_chiller_avgRuntimePerCycle\n                                                ]\n\n         else if (opts.get(\"equip\") == \"chwp\") [kpi_chwp_percentRuntime,\n                                                kpi_chwp_averagePumpSpeed,\n                                                kpi_chwp_totalPumpEnergyUse,\n                                                kpi_chwp_pumpPeakDemand]\n\n         else if (opts.get(\"equip\") == \"cdwp\") [kpi_cdwp_percentRuntime,\n                                                kpi_cdwp_averagePumpSpeed,\n                                                kpi_cdwp_totalPumpEnergyUse,\n                                                kpi_cdwp_pumpPeakDemand]\n\n         else if (opts.get(\"equip\") == \"ct\")   [kpi_coolingTower_percentRuntime,\n                                                kpi_coolingTower_averageCTFanSpeed,\n                                                kpi_coolingTower_totalCTEnergyUse,\n                                                kpi_coolingTower_CTPeakDemand,\n                                                kpi_coolingTower_averageCTLeavingTemp,\n                                                kpi_coolingTower_averageCTRange,\n                                                kpi_coolingTower_avgRuntimePerCycle,\n                                                kpi_coolingTower_averageCTApproach,\n                                                kpi_coolingTower_averageRunningOATWetBulb\n                                                ]\n\n  // Create Output (NOTE: Add support for retrieving \"min\" values. Idea: By checking the funcs displayName, ex: demand or max == max, min == min)\n  res: []\n  targets.each() (target) => do\n    out : (funcs.map() (curFunc) => do\n      cacheResSize    : try ruleKpis(target, dates, read(rule and ruleFunc == curFunc.get(\"name\")).colToList(\"date\")) catch null\n      incompleteCache : cacheResSize != date.numDays\n      engineRes: if (valueSource == \"auto\" or valueSource == \"kpi\") betterRuleKpis(curFunc, target, date) else null// Read rule cache\n      res : if (valueSource != \"kpi\" and (engineRes.isNull or valueSource == \"calc\" or incompleteCache)) call(curFunc, [{target: target, date: date}]).get(\"out\")\n            else engineRes // Call the function regularly if the cache had no info.\n      val : if      (res.isNull or res.isEmpty)     \"missing\"\n            else if (res.isDict and res.has(\"max\")) ((res.get(\"max\")*10).round)/10\n            else                                    ((res.get(res.names.get(0))*10).round)/10\n      if   (incompleteCache and valueSource == \"kpi\" and val != \"missing\")  return {kpi: func(curFunc).get(\"dis\"), value : val.toStr + \" (Incomplete Cache)\"}\n      else                                                                  return {kpi: func(curFunc).get(\"dis\"), value : val}\n    end).toGrid\n    out = try out.addColMeta(\"value\", {dis: target.toRec.get(\"navName\"), enum:{missing: {icon:\"x\", iconColor:\"#FF0000\", dis:\"\"}}}).renameCol(\"value\", \"v\" + res.size) catch out\n  res = res.add(out)\n  end\n\n  // Join all equips\n  out : res.joinAll(\"kpi\")\n\n  // Empty Values Opt\n  if (hideEmptyValues) out = out.findAll(r => not r.rowToList[1..-1].all(v => v==\"missing\"))\n\n  // Add Meta\n  out = out.stream\n           .addColMeta(\"kpi\", {dis: \"KPI\"})\n           .collect()\n  return out\nend",
      "local_src": "/*\n*   Description :\n*     Given a site, returns a summary of the KPI results for the given equip.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/13/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/7/2024   - Added loop override support.\n*\n*/\n(site, dates, opts: {equip: \"none\"}) => do\n\n  // Retrieve Opts\n  hideEmptyValues   : opts.get(\"hideEmptyValues\") == true\n  valueSource       : if (opts.has(\"valueSource\")) opts.get(\"valueSource\").lower else \"auto\"\n  chwsEquipOverride : if (opts.has(\"chwsEquipOverride\")) opts.get(\"chwsEquipOverride\") else null\n  returnDict        : opts.get(\"returnDict\") == true // TODO\n\n  // Retrieve CHWS\n  chws: if (chwsEquipOverride != \"\" and chwsEquipOverride.isNonNull) try readById(chwsEquipOverride)\n                                                                     catch return \"CHWS Override provided is invalid.\"\n        else logic_chws_siteToChws(site)\n\n  // Handle Edge-Case (Added a check for primary/condenser loop)\n  if ((chws.isNull or chws.isEmpty) and (not opts.get(\"primaryLoopOverride\").isRef and not opts.get(\"condenserLoopOverride\").isRef)) return \"No Chilled Water System on this Site\"\n  chws = try chws.toRec.get(\"id\") catch null\n\n  // Retrieve primary loop (added check to use the override with priority)\n  primaryLoop: if (opts.get(\"primaryLoopOverride\").isRef) opts.get(\"primaryLoopOverride\")\n               else try   read(chilled and equip and water and primaryLoop and output and equipRef == chws)->id\n                    catch null\n\n  // Retrieve secondary loop (added check to use the override with priority)\n  secondaryLoop: if (opts.get(\"secondaryLoopOverride\").isRef) opts.get(\"secondaryLoopOverride\")\n                 else try   read(chilled and equip and water and secondaryLoop and output and equipRef == chws)->id\n                      catch null\n\n  // Retrieve condenser loop (added check to use the override with priority)\n  condLoop: if (opts.get(\"condenserLoopOverride\").isRef) opts.get(\"condenserLoopOverride\")\n            else try   read(condenser and equip and water and condenserLoop and output and equipRef == chws)->id  //ADDED (condenser, removed chilled)\n                 catch null\n  if (opts.get(\"equip\") == \"cdwp\" and condLoop.isNull) return \"No Condenser Loop Available\"\n\n  // Normalize Input\n  date: dates.toSpan\n\n  // Find targets\n  targets: if      (opts.get(\"equip\") == \"chlr\") try chillersDigest(primaryLoop).colToList(\"equip\")\n                                                 catch (err) return \"No Chillers Available\"\n           else if (opts.get(\"equip\") == \"chwp\") try do\n                                                   list1: try pumpsDigest(primaryLoop).colToList(\"equip\")   catch (err) //ADDED (try catch)\n                                                   list2: try pumpsDigest(secondaryLoop).colToList(\"equip\") catch (err) //ADDED (try catch)\n                                                   [list1, list2].flatten\n                                                 end\n                                                 catch (err) return \"No Chilled Water Loop Pumps Available\"\n           else if (opts.get(\"equip\") == \"cdwp\") try pumpsDigest(condLoop).colToList(\"equip\")         catch (err) return \"No Condenser Loop Pumps Available\"\n           else if (opts.get(\"equip\") == \"ct\")   try coolingTowersDigest(condLoop).colToList(\"equip\") catch (err) return \"No Cooling Towers Available\"\n           else                                  null\n \n  if (targets.isNull) return \"No equips selected\"\n  if (targets.isStr)  return targets\n  targets = targets.sort((a,b) => a->navName <=> b->navName)\n\n  // KPI Funcs (Missing ct -> wetBulb, approach])\n  funcs: if      (opts.get(\"equip\") == \"chlr\") [kpi_chiller_kWTonAvg,\n                                                kpi_chiller_avgPercentFLA,\n                                                kpi_chiller_percentRuntime,\n                                                kpi_chiller_totalChillerEnergyUse,\n                                                kpi_chiller_chillerPeakDemand,\n                                                kpi_chiller_avgChillerLift,\n                                                kpi_chiller_avgDeltaT,\n                                                kpi_chiller_avgCHWLeavingTemp,\n                                                kpi_chiller_avgCDWLeavingTemp,\n                                                kpi_chiller_avgRuntimePerCycle\n                                                ]\n\n         else if (opts.get(\"equip\") == \"chwp\") [kpi_chwp_percentRuntime,\n                                                kpi_chwp_averagePumpSpeed,\n                                                kpi_chwp_totalPumpEnergyUse,\n                                                kpi_chwp_pumpPeakDemand]\n\n         else if (opts.get(\"equip\") == \"cdwp\") [kpi_cdwp_percentRuntime,\n                                                kpi_cdwp_averagePumpSpeed,\n                                                kpi_cdwp_totalPumpEnergyUse,\n                                                kpi_cdwp_pumpPeakDemand]\n\n         else if (opts.get(\"equip\") == \"ct\")   [kpi_coolingTower_percentRuntime,\n                                                kpi_coolingTower_averageCTFanSpeed,\n                                                kpi_coolingTower_totalCTEnergyUse,\n                                                kpi_coolingTower_CTPeakDemand,\n                                                kpi_coolingTower_averageCTLeavingTemp,\n                                                kpi_coolingTower_averageCTRange,\n                                                kpi_coolingTower_avgRuntimePerCycle,\n                                                kpi_coolingTower_averageCTApproach,\n                                                kpi_coolingTower_averageRunningOATWetBulb\n                                                ]\n\n  // Create Output (NOTE: Add support for retrieving \"min\" values. Idea: By checking the funcs displayName, ex: demand or max == max, min == min)\n  res: []\n  targets.each() (target) => do\n    out : (funcs.map() (curFunc) => do\n      cacheResSize    : try ruleKpis(target, dates, read(rule and ruleFunc == curFunc.get(\"name\")).colToList(\"date\")) catch null\n      incompleteCache : cacheResSize != date.numDays\n      engineRes: if (valueSource == \"auto\" or valueSource == \"kpi\") betterRuleKpis(curFunc, target, date) else null// Read rule cache\n      res : if (valueSource != \"kpi\" and (engineRes.isNull or valueSource == \"calc\" or incompleteCache)) call(curFunc, [{target: target, date: date}]).get(\"out\")\n            else engineRes // Call the function regularly if the cache had no info.\n      val : if      (res.isNull or res.isEmpty)     \"missing\"\n            else if (res.isDict and res.has(\"max\")) ((res.get(\"max\")*10).round)/10\n            else                                    ((res.get(res.names.get(0))*10).round)/10\n      if   (incompleteCache and valueSource == \"kpi\" and val != \"missing\")  return {kpi: func(curFunc).get(\"dis\"), value : val.toStr + \" (Incomplete Cache)\", help: func(curFunc).get(\"help\")} //ADDED (help tag/val)\n      else                                                                  return {kpi: func(curFunc).get(\"dis\"), value : val, help: func(curFunc).get(\"help\")} //ADDED (help tag/val)\n    end).toGrid\n    out = try out.addColMeta(\"value\", {dis: target.toRec.get(\"navName\"), enum:{missing: {icon:\"x\", iconColor:\"#FF0000\", dis:\"\"}}}).renameCol(\"value\", \"v\" + res.size) catch out\n  res = res.add(out)\n  end\n  \n  //ADDED start\n  \n  help: res.first.keepCols([\"kpi\", \"help\"])\n  \n  res = res.map(g => return g.removeCol(\"help\")).add(help)\n  \n  //ADDED end\n\n  // Join all equips\n  out : res.joinAll(\"kpi\")\n \n  // Empty Values Opt\n  if (hideEmptyValues) out = out.findAll(r => not r.rowToList[1..-1].all(v => v==\"missing\"))\n\n  // Add Meta\n  out = out.stream\n           .addColMeta(\"kpi\", {dis: \"KPI\"})\n           .collect()\n  return out\nend",
      "diff": "@@ -43,7 +43,7 @@ \n   // Retrieve condenser loop (added check to use the override with priority)\n   condLoop: if (opts.get(\"condenserLoopOverride\").isRef) opts.get(\"condenserLoopOverride\")\n-            else try   read(chilled and equip and water and condenserLoop and output and equipRef == chws)->id\n+            else try   read(condenser and equip and water and condenserLoop and output and equipRef == chws)->id  //ADDED (condenser, removed chilled)\n                  catch null\n   if (opts.get(\"equip\") == \"cdwp\" and condLoop.isNull) return \"No Condenser Loop Available\"\n \n@@ -54,8 +54,8 @@   targets: if      (opts.get(\"equip\") == \"chlr\") try chillersDigest(primaryLoop).colToList(\"equip\")\n                                                  catch (err) return \"No Chillers Available\"\n            else if (opts.get(\"equip\") == \"chwp\") try do\n-                                                   list1: pumpsDigest(primaryLoop).colToList(\"equip\")\n-                                                   list2: pumpsDigest(secondaryLoop).colToList(\"equip\")\n+                                                   list1: try pumpsDigest(primaryLoop).colToList(\"equip\")   catch (err) //ADDED (try catch)\n+                                                   list2: try pumpsDigest(secondaryLoop).colToList(\"equip\") catch (err) //ADDED (try catch)\n                                                    [list1, list2].flatten\n                                                  end\n                                                  catch (err) return \"No Chilled Water Loop Pumps Available\"\n@@ -113,12 +113,20 @@       val : if      (res.isNull or res.isEmpty)     \"missing\"\n             else if (res.isDict and res.has(\"max\")) ((res.get(\"max\")*10).round)/10\n             else                                    ((res.get(res.names.get(0))*10).round)/10\n-      if   (incompleteCache and valueSource == \"kpi\" and val != \"missing\")  return {kpi: func(curFunc).get(\"dis\"), value : val.toStr + \" (Incomplete Cache)\"}\n-      else                                                                  return {kpi: func(curFunc).get(\"dis\"), value : val}\n+      if   (incompleteCache and valueSource == \"kpi\" and val != \"missing\")  return {kpi: func(curFunc).get(\"dis\"), value : val.toStr + \" (Incomplete Cache)\", help: func(curFunc).get(\"help\")} //ADDED (help tag/val)\n+      else                                                                  return {kpi: func(curFunc).get(\"dis\"), value : val, help: func(curFunc).get(\"help\")} //ADDED (help tag/val)\n     end).toGrid\n     out = try out.addColMeta(\"value\", {dis: target.toRec.get(\"navName\"), enum:{missing: {icon:\"x\", iconColor:\"#FF0000\", dis:\"\"}}}).renameCol(\"value\", \"v\" + res.size) catch out\n   res = res.add(out)\n   end\n+\n+  //ADDED start\n+\n+  help: res.first.keepCols([\"kpi\", \"help\"])\n+\n+  res = res.map(g => return g.removeCol(\"help\")).add(help)\n+\n+  //ADDED end\n \n   // Join all equips\n   out : res.joinAll(\"kpi\")\n"
    },
    {
      "name": "logic_chws_systemSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     Given a site, returns a summary of the KPI results for the given\n*     chilled water system.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site ref.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/13/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024   - Added the 'opts' parameter into the 'call' function call (Line 59).\n*/\n(site, dates, opts:{}) => do\n\n  // Retrieve Opts\n  hideEmptyValues   : opts.get(\"hideEmptyValues\") == true\n  valueSource       : if (opts.has(\"valueSource\")) opts.get(\"valueSource\").lower else \"auto\"\n  chwsEquipOverride : if (opts.has(\"chwsEquipOverride\")) opts.get(\"chwsEquipOverride\") else null\n  returnDict        : opts.get(\"returnDict\") == true // TODO\n\n  // Retrieve CHWS\n  targets: if (chwsEquipOverride != \"\" and chwsEquipOverride.isNonNull) try readById(chwsEquipOverride)\n                                                                        catch return \"CHWS Override provided is invalid.\"\n           else logic_chws_siteToChws(site)\n  date: dates.toSpan\n\n  // Edge-case handling\n  if (targets.isNull or targets.isEmpty) return \"No Chilled Water System on this Site\"\n  target: targets.toRec.get(\"id\")\n\n  // KPI Funcs\n  funcs: [kpi_chws_kWTonAvg,\n          kpi_chws_totalSystemEnergyUse,\n          kpi_chws_systemPeakDemand,\n          kpi_chws_avgDeltaT,\n          kpi_chws_avgCHWSupplySetpoint,\n          kpi_chws_avgSystemFlow,\n          kpi_chws_primaryLoop_percentRuntimeOnePump,\n          kpi_chws_primaryLoop_percentRuntimeMultiplePumps,\n          kpi_chws_secondaryLoop_percentRuntimeOnePump,\n          kpi_chws_secondaryLoop_percentRuntimeMultiplePumps,\n          kpi_chws_condenserLoop_percentRuntimeOnePump,\n          kpi_chws_condenserLoop_percentRuntimeMultiplePumps,\n          kpi_chws_condenserLoop_percentRuntimeOneTower,\n          kpi_chws_condenserLoop_percentRuntimeMultipleTowers,\n          kpi_chws_primaryLoop_percentRuntimeOneChiller,\n          kpi_chws_primaryLoop_percentRuntimeMultipleChillers\n          ]\n\n  // Create Output (NOTE: Add support for retrieving \"min\" values by checking the funcs displayName)\n  out : (funcs.map() (curFunc) => do\n    cacheResSize    : try ruleKpis(target, dates, read(rule and ruleFunc == curFunc.get(\"name\")).colToList(\"date\")) catch null\n    incompleteCache : cacheResSize != date.numDays\n    engineRes: if (valueSource == \"auto\" or valueSource == \"kpi\") betterRuleKpis(curFunc, target, date) else null// Read rule cache\n    res : if (valueSource != \"kpi\" and (engineRes.isNull or valueSource == \"calc\" or incompleteCache)) call(curFunc, [{target: target, date: date, opts: opts}]).get(\"out\")\n          else engineRes // Call the function regularly if the cache had no info.\n    val : if      (res.isNull or res.isEmpty)     \"missing\"\n          else if (res.isDict and res.has(\"max\")) ((res.get(\"max\")*10).round)/10\n          else                                    ((res.get(res.names.get(0))*10).round)/10\n    if   (incompleteCache and valueSource == \"kpi\" and val != \"missing\")  return {kpi: func(curFunc).get(\"dis\"), value : val.toStr + \" (Incomplete Cache)\"}\n    else                                                                  return {kpi: func(curFunc).get(\"dis\"), value : val}\n  end).toGrid\n\n  // Empty Values Opt\n  if (hideEmptyValues) out = out.findAll(r => not r.rowToList[1..-1].all(v => v==\"missing\"))\n\n  // Add Meta\n  out = out.stream\n           .addColMeta(\"kpi\", {dis: \"KPI\"})\n           .addColMeta(\"value\", {dis: \"System\", enum:{missing: {icon:\"x\", iconColor:\"#FF0000\", dis:\"\"}}})\n           .collect()\n  return out\nend",
      "local_src": "/*\n*   Description :\n*     Given a site, returns a summary of the KPI results for the given\n*     chilled water system.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site ref.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/13/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024   - Added the 'opts' parameter into the 'call' function call (Line 59).\n*/\n(site, dates, opts:{}) => do\n\n  // Retrieve Opts\n  hideEmptyValues   : opts.get(\"hideEmptyValues\") == true\n  valueSource       : if (opts.has(\"valueSource\")) opts.get(\"valueSource\").lower else \"auto\"\n  chwsEquipOverride : if (opts.has(\"chwsEquipOverride\")) opts.get(\"chwsEquipOverride\") else null\n  returnDict        : opts.get(\"returnDict\") == true // TODO\n\n  // Retrieve CHWS\n  targets: if (chwsEquipOverride != \"\" and chwsEquipOverride.isNonNull) try readById(chwsEquipOverride)\n                                                                        catch return \"CHWS Override provided is invalid.\"\n           else logic_chws_siteToChws(site)\n  date: dates.toSpan\n\n  // Edge-case handling\n  if (targets.isNull or targets.isEmpty) return \"No Chilled Water System on this Site\"\n  target: targets.toRec.get(\"id\")\n\n  // KPI Funcs\n  funcs: [//kpi_chws_kWTonAvg,\n          kpi_chws_totalSystemEnergyUse,\n          kpi_chws_systemPeakDemand,\n          kpi_chws_avgMeterDeltaT,\n          kpi_chws_avgCHWSupplySetpoint,\n          kpi_chwp_percentRuntime_chws_dashboard,\n          kpi_chws_avgSystemFlow,\n          kpi_chws_primaryLoopDeltaT,\n          kpi_chws_avgCHWSupplyTempPrimaryLoop,\n          //kpi_chws_primaryLoop_percentRuntimeOnePump,\n          //kpi_chws_primaryLoop_percentRuntimeMultiplePumps,\n          kpi_chws_secondaryLoopDeltaT,\n          kpi_chws_avgCHWSupplyTempSecondaryLoop,\n          kpi_chws_secondaryLoop_percentRuntimeOnePump,\n          kpi_chws_secondaryLoop_percentRuntimeMultiplePumps,\n          kpi_chwp_averagePumpSpeedSecLoop,\n          ]\n\n  // Create Output (NOTE: Add support for retrieving \"min\" values by checking the funcs displayName)\n  out : (funcs.map() (curFunc) => do\n    cacheResSize    : try ruleKpis(target, dates, read(rule and ruleFunc == curFunc.get(\"name\")).colToList(\"date\")) catch null\n    incompleteCache : cacheResSize != date.numDays\n    engineRes: if (valueSource == \"auto\" or valueSource == \"kpi\") betterRuleKpis(curFunc, target, date) else null// Read rule cache\n    res : if (valueSource != \"kpi\" and (engineRes.isNull or valueSource == \"calc\" or incompleteCache)) call(curFunc, [{target: target, date: date, opts: opts}]).get(\"out\")\n          else engineRes // Call the function regularly if the cache had no info.\n    val : if      (res.isNull or res.isEmpty)     \"missing\"\n          else if (res.isDict and res.has(\"max\")) ((res.get(\"max\")*10).round)/10\n          else                                    ((res.get(res.names.get(0))*10).round)/10\n    if   (incompleteCache and valueSource == \"kpi\" and val != \"missing\")  return {kpi: func(curFunc).get(\"dis\"), value : val.toStr + \" (Incomplete Cache)\", help: func(curFunc).get(\"help\")} //ADDED (help tag/val)\n    else                                                                  return {kpi: func(curFunc).get(\"dis\"), value : val, help: func(curFunc).get(\"help\")} //ADDED (help tag/val)\n  end).toGrid.reorderKeepCols([\"kpi\", \"value\", \"help\"])\n\n  // Empty Values Opt\n  if (hideEmptyValues) out = out.findAll(r => not r.rowToList[1..-1].all(v => v==\"missing\"))\n\n  // Add Meta\n  out = out.stream\n           .addColMeta(\"kpi\", {dis: \"KPI\"})\n           .addColMeta(\"value\", {dis: \"Values\", enum:{missing: {icon:\"x\", iconColor:\"#FF0000\", dis:\"\"}}})\n           .collect()\n  return out\nend",
      "diff": "@@ -33,22 +33,22 @@   target: targets.toRec.get(\"id\")\n \n   // KPI Funcs\n-  funcs: [kpi_chws_kWTonAvg,\n+  funcs: [//kpi_chws_kWTonAvg,\n           kpi_chws_totalSystemEnergyUse,\n           kpi_chws_systemPeakDemand,\n-          kpi_chws_avgDeltaT,\n+          kpi_chws_avgMeterDeltaT,\n           kpi_chws_avgCHWSupplySetpoint,\n+          kpi_chwp_percentRuntime_chws_dashboard,\n           kpi_chws_avgSystemFlow,\n-          kpi_chws_primaryLoop_percentRuntimeOnePump,\n-          kpi_chws_primaryLoop_percentRuntimeMultiplePumps,\n+          kpi_chws_primaryLoopDeltaT,\n+          kpi_chws_avgCHWSupplyTempPrimaryLoop,\n+          //kpi_chws_primaryLoop_percentRuntimeOnePump,\n+          //kpi_chws_primaryLoop_percentRuntimeMultiplePumps,\n+          kpi_chws_secondaryLoopDeltaT,\n+          kpi_chws_avgCHWSupplyTempSecondaryLoop,\n           kpi_chws_secondaryLoop_percentRuntimeOnePump,\n           kpi_chws_secondaryLoop_percentRuntimeMultiplePumps,\n-          kpi_chws_condenserLoop_percentRuntimeOnePump,\n-          kpi_chws_condenserLoop_percentRuntimeMultiplePumps,\n-          kpi_chws_condenserLoop_percentRuntimeOneTower,\n-          kpi_chws_condenserLoop_percentRuntimeMultipleTowers,\n-          kpi_chws_primaryLoop_percentRuntimeOneChiller,\n-          kpi_chws_primaryLoop_percentRuntimeMultipleChillers\n+          kpi_chwp_averagePumpSpeedSecLoop,\n           ]\n \n   // Create Output (NOTE: Add support for retrieving \"min\" values by checking the funcs displayName)\n@@ -61,9 +61,9 @@     val : if      (res.isNull or res.isEmpty)     \"missing\"\n           else if (res.isDict and res.has(\"max\")) ((res.get(\"max\")*10).round)/10\n           else                                    ((res.get(res.names.get(0))*10).round)/10\n-    if   (incompleteCache and valueSource == \"kpi\" and val != \"missing\")  return {kpi: func(curFunc).get(\"dis\"), value : val.toStr + \" (Incomplete Cache)\"}\n-    else                                                                  return {kpi: func(curFunc).get(\"dis\"), value : val}\n-  end).toGrid\n+    if   (incompleteCache and valueSource == \"kpi\" and val != \"missing\")  return {kpi: func(curFunc).get(\"dis\"), value : val.toStr + \" (Incomplete Cache)\", help: func(curFunc).get(\"help\")} //ADDED (help tag/val)\n+    else                                                                  return {kpi: func(curFunc).get(\"dis\"), value : val, help: func(curFunc).get(\"help\")} //ADDED (help tag/val)\n+  end).toGrid.reorderKeepCols([\"kpi\", \"value\", \"help\"])\n \n   // Empty Values Opt\n   if (hideEmptyValues) out = out.findAll(r => not r.rowToList[1..-1].all(v => v==\"missing\"))\n@@ -71,7 +71,7 @@   // Add Meta\n   out = out.stream\n            .addColMeta(\"kpi\", {dis: \"KPI\"})\n-           .addColMeta(\"value\", {dis: \"System\", enum:{missing: {icon:\"x\", iconColor:\"#FF0000\", dis:\"\"}}})\n+           .addColMeta(\"value\", {dis: \"Values\", enum:{missing: {icon:\"x\", iconColor:\"#FF0000\", dis:\"\"}}})\n            .collect()\n   return out\n end"
    },
    {
      "name": "logic_cwp_pumpIsRunning",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     Given a pump from a chilled water system, return runtime periods for that pump\n*\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Grid                   target          - The pump input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Additional Options\n*\n*   Work :\n*     Who                    When        Change\n*     William Ludwig         8/01/2024   - Initial Creation\n*     Thomas Kuhrke Limia    8/01/2024   - Added fallback support\n*     Thomas Kuhrke Limia    8/27/2024   - Changed fallback order\n*/\n(target, dates, opts:{}) => do\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  site: targetRec.get(\"siteRef\")\n  dates = dates.toSpan()\n\n  // Handle opts\n  boolPeriods: opts.get(\"boolPeriods\") == true\n\n  // Retrieve points\n  points: pumpDigest(targetId)\n\n  // Save ALL points into their respective slot\n  pri_pump_speed  : points.get(\"pri_pump_speed\")\n  sec_pump_speed  : points.get(\"sec_pump_speed\")\n  pri_pump_power  : points.get(\"pri_pump_power\")\n  sec_pump_power  : points.get(\"sec_pump_power\")\n  pri_pump_status : points.get(\"pri_pump_status\")\n  sec_pump_status : points.get(\"sec_pump_status\")\n\n  // Find point based on fallback order\n  existing : if      (not (pri_pump_speed.isStr  and sec_pump_speed.isStr))  [pri_pump_speed, sec_pump_speed].find(v => not v.isStr)\n             else if (not (pri_pump_power.isStr  and sec_pump_power.isStr))  [pri_pump_power, sec_pump_power].find(v => not v.isStr)\n             else if (not (pri_pump_status.isStr and sec_pump_status.isStr)) [pri_pump_status, sec_pump_status].find(v => not v.isStr)\n             else \"All points doesn't exist, or the existing ones have no his.\"\n\n  // Handle edge case where there are no existing points\n  if (existing.isStr) return existing\n\n  // Retrieve his\n  his : hisRead(existing, dates, {-limit}).hisInterpolate.findAll(r => r.get(\"v0\").isNonNull)\n\n  // Handle Edge-case\n  if (his.isEmpty) return \"No his data\"\n\n  // Transform number hisGrid into boolean\n  if (his.first.get(\"v0\").isNumber) his = his.hisRollupAuto().map(r => return {ts: r.get(\"ts\"), v0: r.get(\"v0\") > 1})\n\n  // Return boolean grid if opt is present\n  if (boolPeriods) return his\n\n  // Transform into period grid.\n  his = his.conditionToPeriods(\"v0\", r => r.get(\"v0\") == true).addColMeta(\"v0\", his.col(\"v0\").meta)\n\n  // CHWS 2.0 Preparation\n  funcName: \"logic_cwp_pumpIsRunning\"\n  titles: logicFuncTitle(funcName, targetRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  outHis : his.stream\n              .addMeta(outputMeta)\n              .collect\n\n  return outHis\n\nend",
      "local_src": "/*\n*   Description :\n*     Given a pump from a chilled water system, return runtime periods for that pump\n*\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Grid                   target          - The pump input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Additional Options\n*\n*   Work :\n*     Who                    When        Change\n*     William Ludwig         8/01/2024   - Initial Creation\n*     Thomas Kuhrke Limia    8/01/2024   - Added fallback support\n*     Thomas Kuhrke Limia    8/27/2024   - Changed fallback order\n*/\n(target, dates, opts:{}) => do\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  site: targetRec.get(\"siteRef\")\n  dates = dates.toSpan()\n\n  // Handle opts\n  boolPeriods: opts.get(\"boolPeriods\") == true\n\n  // Retrieve points\n  points: pumpDigest(targetId)\n\n  // Save ALL points into their respective slot\n  pri_pump_speed  : points.get(\"pri_pump_speed\")\n  sec_pump_speed  : points.get(\"sec_pump_speed\")\n  pri_pump_power  : points.get(\"pri_pump_power\")\n  sec_pump_power  : points.get(\"sec_pump_power\")\n  pri_pump_status : points.get(\"pri_pump_status\")\n  sec_pump_status : points.get(\"sec_pump_status\")\n\n  // Find point based on fallback order\n  existing : if      (not (pri_pump_speed.isStr  and sec_pump_speed.isStr))  [pri_pump_speed, sec_pump_speed].find(v => not v.isStr)\n             else if (not (pri_pump_power.isStr  and sec_pump_power.isStr))  [pri_pump_power, sec_pump_power].find(v => not v.isStr)\n             else if (not (pri_pump_status.isStr and sec_pump_status.isStr)) [pri_pump_status, sec_pump_status].find(v => not v.isStr)\n             else \"All points don't exist, or the existing ones have no his.\"\n\n  // Handle edge case where there are no existing points\n  if (existing.isStr) return existing\n\n  // Retrieve his\n  his : hisRead(existing, dates, {-limit}).hisInterpolate.findAll(r => r.get(\"v0\").isNonNull)\n\n  // Handle Edge-case\n  if (his.isEmpty) return \"No his data\"\n\n  // Transform number hisGrid into boolean\n  if (his.first.get(\"v0\").isNumber) his = his.map(r => return {ts: r.get(\"ts\"), v0: r.get(\"v0\") > 1})\n\n  // Return boolean grid if opt is present\n  if (boolPeriods) return his\n\n  // Transform into period grid.\n  his = his.conditionToPeriods(\"v0\", r => r.get(\"v0\") == true).addColMeta(\"v0\", his.col(\"v0\").meta)\n\n  // CHWS 2.0 Preparation\n  funcName: \"logic_cwp_pumpIsRunning\"\n  titles: logicFuncTitle(funcName, targetRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  outHis : his.stream\n              .addMeta(outputMeta)\n              .collect\n\n  return outHis\n\nend",
      "diff": "@@ -41,7 +41,7 @@   existing : if      (not (pri_pump_speed.isStr  and sec_pump_speed.isStr))  [pri_pump_speed, sec_pump_speed].find(v => not v.isStr)\n              else if (not (pri_pump_power.isStr  and sec_pump_power.isStr))  [pri_pump_power, sec_pump_power].find(v => not v.isStr)\n              else if (not (pri_pump_status.isStr and sec_pump_status.isStr)) [pri_pump_status, sec_pump_status].find(v => not v.isStr)\n-             else \"All points doesn't exist, or the existing ones have no his.\"\n+             else \"All points don't exist, or the existing ones have no his.\"\n \n   // Handle edge case where there are no existing points\n   if (existing.isStr) return existing\n@@ -53,7 +53,7 @@   if (his.isEmpty) return \"No his data\"\n \n   // Transform number hisGrid into boolean\n-  if (his.first.get(\"v0\").isNumber) his = his.hisRollupAuto().map(r => return {ts: r.get(\"ts\"), v0: r.get(\"v0\") > 1})\n+  if (his.first.get(\"v0\").isNumber) his = his.map(r => return {ts: r.get(\"ts\"), v0: r.get(\"v0\") > 1})\n \n   // Return boolean grid if opt is present\n   if (boolPeriods) return his\n"
    },
    {
      "name": "logic_energyAgent_meterPointToPredictionsChart",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n/*\n*   Description :\n*    This widget (chart) displays the raw his data, predictions, and SHAP values over time\n*\n*   Parameters :\n*     Type             Name                 Description\n*     ref              modelPointRef        ModelPoint id\n*     number           modelRunIndex        index for the selected model run\n*\n*   Output :\n*     Type                   Name            Description\n*     Grid                   out            - A gbGrid containing all connected sites and relevant info/KPIs\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         05/21/2025      - Initial Creation\n*/\n\n(meterPoint, dates, opts:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // USER VALIDATION\n  validateUser()\n\n  // Normalize inputs\n  meterPointRec: try meterPoint.toRec catch null\n  meterPointRef: try meterPointRec.get(\"id\") catch null\n    if (meterPointRef.isNull)  return {info: \"Meter not found.\"}.toGrid.addMeta({noData: \"Select a meter point before proceeding.\"}).addColMeta(\"info\", {dis: \"Information\"})\n\n  dates = dates.toSpan\n\n  // Edge case handling:\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  meterHisGrid: meterPointRef.hisRead(dates)\n  modelPoint: try read(syntheticPoint and pointRef==meterPointRef).toRec catch null\n    //akTODO: Just return Meter history if model point missing\n    if (modelPoint.isNull) do\n      //out: meterPointRef.hisRead(dates)\n      gridMeta: {title: \"Meter history for \"+meterPointRec.get(\"navName\"),\n                 subtitle: \"(Run model to show predictions)\"}\n      return meterHisGrid.addColMeta(\"v0\", {dis:\"Meter readings\", strokeWidth:1, chartGroup: \"modelFit\", chartType:\"line\"}).addMeta(gridMeta)//\n    end\n  modelPointRef: modelPoint.get(\"id\")\n\n  // Retrieve the detailed model results\n  modelResults : logic_kwModeling_getDetailedModelResults(modelPointRef)\n    if (modelResults.isStr) return {info: modelResults}.toGrid.addMeta({noData: modelResults}).addColMeta(\"info\", {dis: \"Information\"})\n\n\n  modelPredictionsGrid: [{}].toGrid\n\n  modelResults.each() r => do\n    currentPredictions: r.get(\"modelPredictions\")\n    modelPredictionsGrid = try modelPredictionsGrid.addRows(currentPredictions) catch modelPredictionsGrid\n  end\n\n  modelPredictionsGrid = modelPredictionsGrid.removeCol(\"v0\").reorderKeepCols([\"ts\"]).filter((ts>dates.start) and (ts<dates.end)).addMeta({hisStart: dates.start, hisEnd: dates.end})\n\n  modelPredictionsGrid = hisJoin([modelPredictionsGrid, meterHisGrid]).reorderKeepCols([\"ts\", \"v0\"])\n  // akTODO: add spanStart to modelPointStart meter his\n\n  // akTODO: Add runtime chart for all anomalies\n    // Keep all anomalies runtime (no selected anomaly)\n  allAnomalies: readAll(anomaly and targetVarRef==meterPointRef and ts >= dates.start and ts < dates.end)\n  try allAnomalies = allAnomalies.reorderCols([\"ts\", \"duration\"]).toGrid.addMeta({hisStart: dates.start, hisEnd: dates.end, hisSize:allAnomalies.size}) //\n  catch null //\n  //return allAnomalies\n\n  out: try hisJoin([modelPredictionsGrid, allAnomalies]) catch modelPredictionsGrid\n  //return out\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // FORMAT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // Update hisStart, hisEnd etc.\n  // Add chart groups and group orders\n  meterEnd: meterPointRec.get(\"hisEnd\")\n  modelEnd: modelPoint.get(\"hisEnd\")\n  spanEnd: dates.end\n\n  chartEnd: if ((spanEnd > meterEnd) and (spanEnd > modelEnd) and (meterEnd > modelEnd)) meterEnd\n       else if ((spanEnd > meterEnd) and (spanEnd > modelEnd) and (meterEnd < modelEnd)) modelEnd\n       else                                                                              spanEnd\n\n  // NOTE: for Cornell, because we are only passing in the bare minimum of 6 weeks of training data, model fits don't provide any information\n  // But if we had 6 months of training data, we would have predictions starting week 6 to the end of month 6, plus the predictions for the testingDateSpan\n  gridMeta: {title: \"Model Predictions for \"+meterPointRec.get(\"navName\"),\n             subtitle: \"(\"+dates.start.date+\" to \"+chartEnd.date+\")\",\n             hisStart: dates.start,\n             hisEnd: chartEnd}\n             //subtitle: anomalyRec.get(\"targetVarRef\").toRec.get(\"navName\").toStr + \" (\"+dates.start.date+\" to \"+chartEnd.date+\")\",\n             //hisStart:dateSpan.start,\n             //hisEnd:dateSpan.end}\n\n  out = out.stream\n                     .addColMeta(\"v0\",            {dis:\"Actual\", strokeWidth:1, chartGroup: \"modelFit\", chartType:\"line\", subtitile:\"Model Predictions and Fits\"})//\n                     .addColMeta(\"timegpt_hi_95\", {dis:\"Upper Bound\", strokeWidth:0.5, chartGroup: \"modelFit\", chartAreaMode: \"nextSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n                     .addColMeta(\"timegpt\",       {dis:\"Model\", strokeWidth:1.5, chartGroup: \"modelFit\", chartType:\"line\", strokeDasharray:\"6,3\"})//\n                     .addColMeta(\"timegpt_lo_95\", {dis:\"Lower Bound\", strokeWidth:0.5, chartGroup: \"modelFit\", chartAreaMode: \"prevSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n                     .addColMeta(\"duration\", {chartGroup: \"xduration\", chartType:\"runtime\", dis: \"All Anomalies\"})\n                     .addPrez_chartOrder(\"\", [ \"v0\", \"v1\", \"timegpt_lo_95\", \"timegpt_hi_95\"])//\n                     .collect\n                     .addMeta(gridMeta)\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  //RETURN RESULT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  return out.chart\n\nend",
      "local_src": "//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n/*\n*   Description :\n*    This widget (chart) displays the raw his data, predictions, and SHAP values over time\n*\n*   Parameters :\n*     Type             Name                 Description\n*     ref              modelPointRef        ModelPoint id\n*     number           modelRunIndex        index for the selected model run\n*\n*   Output :\n*     Type                   Name            Description\n*     Grid                   out            - A gbGrid containing all connected sites and relevant info/KPIs\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         05/21/2025      - Initial Creation\n*/\n\n(meterPoint, dates, opts:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // USER VALIDATION\n  validateUser()\n\n  // Normalize inputs\n  meterPointRec: try meterPoint.toRec catch null\n  meterPointRef: try meterPointRec.get(\"id\") catch null\n    if (meterPointRef.isNull)  return {info: \"Meter not found.\"}.toGrid.addMeta({noData: \"Select a meter point before proceeding.\"}).addColMeta(\"info\", {dis: \"Information\"})\n  \n  dates = dates.toSpan\n  \n  // Edge case handling: \n  \n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  meterHisGrid: meterPointRef.hisRead(dates)\n  modelPoint: try read(syntheticPoint and pointRef==meterPointRef, false).toRec catch null\n    if (modelPoint.isNull) return {info: modelPoint}.toGrid.addMeta({noData: \"Model point missing for the selected utility.\"}).addColMeta(\"info\", {dis: \"Information\"}) \n  modelPointRef: modelPoint.get(\"id\")\n  \n  // Retrieve the detailed model results\n  modelResults : logic_kwModeling_getDetailedModelResults(modelPointRef)\n    if (modelResults.isStr) return {info: modelResults}.toGrid.addMeta({noData: modelResults}).addColMeta(\"info\", {dis: \"Information\"})\n\n  \n  modelPredictionsGrid: [{}].toGrid\n  \n  modelResults.each() r => do\n    currentPredictions: r.get(\"modelPredictions\")\n    modelPredictionsGrid = try modelPredictionsGrid.addRows(currentPredictions) catch modelPredictionsGrid\n  end\n  \n  modelPredictionsGrid = modelPredictionsGrid.removeCol(\"v0\").reorderKeepCols([\"ts\"]).filter((ts>dates.start) and (ts<dates.end)).addMeta({hisStart: dates.start, hisEnd: dates.end})\n  \n  \n  modelPredictionsGrid = hisJoin([modelPredictionsGrid, meterHisGrid]).reorderKeepCols([\"ts\", \"v0\"])\n  // akTODO: add spanStart to modelPointStart meter his\n\n  // akTODO: Add runtime chart for all anomalies\n    // Keep all anomalies runtime (no selected anomaly)\n  allAnomalies: readAll(anomaly and targetVarRef==meterPointRef and ts >= dates.start and ts < dates.end)\n  try allAnomalies = allAnomalies.reorderCols([\"ts\", \"duration\"]).toGrid.addMeta({hisStart: dates.start, hisEnd: dates.end, hisSize:allAnomalies.size}) //\n  catch null //\n  //return allAnomalies\n\n  out: try hisJoin([modelPredictionsGrid, allAnomalies]) catch modelPredictionsGrid\n  //return out\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // FORMAT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // Update hisStart, hisEnd etc.\n  // Add chart groups and group orders\n  meterEnd: meterPointRec.get(\"hisEnd\")\n  modelEnd: modelPoint.get(\"hisEnd\")\n  spanEnd: dates.end\n\n  chartEnd: if ((spanEnd > meterEnd) and (spanEnd > modelEnd) and (meterEnd > modelEnd)) meterEnd \n       else if ((spanEnd > meterEnd) and (spanEnd > modelEnd) and (meterEnd < modelEnd)) modelEnd\n       else                                                                              spanEnd\n\n  // NOTE: for Cornell, because we are only passing in the bare minimum of 6 weeks of training data, model fits don't provide any information\n  // But if we had 6 months of training data, we would have predictions starting week 6 to the end of month 6, plus the predictions for the testingDateSpan\n  gridMeta: {title: \"Model Predictions for \"+meterPointRec.get(\"navName\"),\n             subtitle: \"(\"+dates.start.date+\" to \"+chartEnd.date+\")\",\n             hisStart: dates.start,\n             hisEnd: chartEnd}\n             //subtitle: anomalyRec.get(\"targetVarRef\").toRec.get(\"navName\").toStr + \" (\"+dates.start.date+\" to \"+chartEnd.date+\")\", \n             //hisStart:dateSpan.start, \n             //hisEnd:dateSpan.end}\n\n  out = out.stream\n                     .addColMeta(\"v0\",            {dis:\"Actual\", strokeWidth:1, chartGroup: \"modelFit\", chartType:\"line\", subtitile:\"Model Predictions and Fits\"})//\n                     .addColMeta(\"timegpt_hi_95\", {dis:\"Upper Bound\", strokeWidth:0.5, chartGroup: \"modelFit\", chartAreaMode: \"nextSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n                     .addColMeta(\"timegpt\",       {dis:\"Model\", strokeWidth:1.5, chartGroup: \"modelFit\", chartType:\"line\", strokeDasharray:\"6,3\"})//\n                     .addColMeta(\"timegpt_lo_95\", {dis:\"Lower Bound\", strokeWidth:0.5, chartGroup: \"modelFit\", chartAreaMode: \"prevSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n                     .addColMeta(\"duration\", {chartGroup: \"xduration\", chartType:\"runtime\", dis: \"All Anomalies\"})\n                     .addPrez_chartOrder(\"\", [ \"v0\", \"v1\", \"timegpt_lo_95\", \"timegpt_hi_95\"])//\n                     .collect\n                     .addMeta(gridMeta)\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  //RETURN RESULT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  return out.chart\n\nend",
      "diff": "@@ -49,14 +49,8 @@   //////////////////////////////////////////////////////////////////////////////////////////\n \n   meterHisGrid: meterPointRef.hisRead(dates)\n-  modelPoint: try read(syntheticPoint and pointRef==meterPointRef).toRec catch null\n-    //akTODO: Just return Meter history if model point missing\n-    if (modelPoint.isNull) do\n-      //out: meterPointRef.hisRead(dates)\n-      gridMeta: {title: \"Meter history for \"+meterPointRec.get(\"navName\"),\n-                 subtitle: \"(Run model to show predictions)\"}\n-      return meterHisGrid.addColMeta(\"v0\", {dis:\"Meter readings\", strokeWidth:1, chartGroup: \"modelFit\", chartType:\"line\"}).addMeta(gridMeta)//\n-    end\n+  modelPoint: try read(syntheticPoint and pointRef==meterPointRef, false).toRec catch null\n+    if (modelPoint.isNull) return {info: modelPoint}.toGrid.addMeta({noData: \"Model point missing for the selected utility.\"}).addColMeta(\"info\", {dis: \"Information\"})\n   modelPointRef: modelPoint.get(\"id\")\n \n   // Retrieve the detailed model results\n@@ -72,6 +66,7 @@   end\n \n   modelPredictionsGrid = modelPredictionsGrid.removeCol(\"v0\").reorderKeepCols([\"ts\"]).filter((ts>dates.start) and (ts<dates.end)).addMeta({hisStart: dates.start, hisEnd: dates.end})\n+\n \n   modelPredictionsGrid = hisJoin([modelPredictionsGrid, meterHisGrid]).reorderKeepCols([\"ts\", \"v0\"])\n   // akTODO: add spanStart to modelPointStart meter his\n"
    },
    {
      "name": "logic_eui",
      "pod": "kwLinkVirtualExt",
      "pod_src": "//todo figure out hisClips\n//todo figure out what to do with missing data\n\n(points, dates, opts:{}) => do\n\n  //normalize rec\n  extendedSpan: (dates.toSpan.start-1yr)..(dates.toSpan.end)\n  if (points.size==0 or points.isEmpty) throw \"No points provided, list is empty\"\n  recs: points.toRecList.toGrid\n  siteRec: recs.first.get(\"siteRef\").toRec\n\n  //get last complete month\n  lastHisEnd: (recs).colToList(\"hisEnd\").sort.first\n  lastTs: lastHisEnd - lastHisEnd.day\n\n  //get opts\n  if (not opts.isDict) throw \"Opts is not a dict\"\n  naHandling: opts.get(\"naHandling\")\n  if (naHandling.isNull) naHandling=\"interpolate\"\n  eci: if (opts.has(\"eci\")) true else false\n\n  //read his\n  rawHis: recs.map(r=>r.merge({chartGroup:\"raw\",subtitle:\"Raw Data\"})).hisRead(extendedSpan, {-limit}).hisClip\n  if (rawHis.size==0) throw \"No his on provided points: \"+points\n\n  his: []\n  recs.map(r => r.set(\"hisLimit\", null)).each() (p,i) => do\n\n    pointHis: p.merge({chartGroup:\"cleaned\", subtitle:\"Cleaned His\"})\n               .hisRead(extendedSpan, {-limit})\n    if (pointHis.size==0) throw \"No point his\"\n\n    //interpolate if applicable\n    if (naHandling==\"interpolate\") pointHis = pointHis.hisInterpolate\n\n    //convert units\n    if (not eci) do\n      if (p.get(\"unit\")==\"kW\") pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\"))\n      if (p.get(\"unit\")==\"BTU/h\") pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"BTU\"))\n      if (p.get(\"unit\")==\"kBTU/h\") pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"kBTU\"))\n\n      if (p.get(\"unit\")==\"lb/h\") do\n        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*0.0012850675).as(\"kBTU\"))\n      end\n      if (p.get(\"unit\")==\"tonref\") do\n        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*12000).as(\"kBTU\"))\n      end\n      if (p.get(\"unit\")==\"klb/h\") do\n        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*1.2850675).as(\"kBTU\"))\n      end\n      if (p.get(\"unit\")==\"ft\u00b3_gas\") do\n        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"kBTU\"))\n      end\n    end\n\n    //convert kwh to btu\n    pointHis = pointHis.hisMap(v=>if (not eci) v.to(\"kBTU\") else v) //todo make sure hisLimit actually works otherwise hisMap is limited to 10k items. adding -hisLimit above hopefully does this\n\n    //if monthly data, then calendarize\n    if (p.has(\"monthly\")) pointHis = pointHis.calendarize(\"v0\")//.hisClip\n    else                  pointHis = pointHis.hisRollup(sum, 1day )                          //.hisClip\n\n    //throw pointHis.size\n    //rollup daily\n    pointHis = pointHis.addColMeta(\"v0\", {dis:\"Daily Rollup - \"+p.dis})\n                       .renameCol(\"v0\",\"daily\"+i)\n\n    //add to his column\n    his = his.add(pointHis)\n    end\n\n  //get square footage\n  sqft:  try getSiteSqftHis(siteRec, extendedSpan, {rollup:1day})\n         catch (err) err\n  if (sqft.isDict) throw \"No sqft point or site area tag on \"+siteRec.dis+\": \"+siteRec.get(\"area\") + \"\\n\"+sqft\n\n  //combine data\n  kbtu : hisJoin([his].flatten).toGrid\n  colNameSize: kbtu.colNames.size\n  if      (naHandling==\"na\")          kbtu = kbtu.findAll(r => r.vals.size==colNameSize and r.vals[1..-1].all(v => v.isNumber))\n  else if (naHandling==\"interpolate\") kbtu = kbtu.hisInterpolate\n\n  //sum up multiple points\n  kbtu = kbtu.map(r => if (r.vals.size==colNameSize) r.set(\"kbtu\",r.vals[1..-1].fold(sum))\n                       else r.set(\"kbtu\",na()))\n\n  //calculate eui\n  eui: hisJoin([kbtu, sqft]).hisClip\n        .map(r => r.set(\"eui\",r.get(\"kbtu\")/r.get(\"sqft\")))\n        .keepCols([\"ts\",\"eui\"])\n        .addColMeta(\"eui\",{dis:\"Daily\"})\n\n  //rollup monthly data\n  monthlyMeta: if (eci) {dis:\"Monthly\", chartGroup:\"monthly\", subtitle:\"Monthly ECI (monthly \\$ / sqft)\"}\n               else     {dis:\"Monthly\", chartGroup:\"monthly\", subtitle:\"Monthly EUI (monthly kBTU / sqft)\"}\n  monthlyHis: eui.hisRollup(sum, 1mo)\n                 .renameCol(\"eui\",\"monthlyEui\")\n                 .addColMeta(\"monthlyEui\", monthlyMeta)\n\n  //clip off last month where data is incomplete\n  monthlyHis = monthlyHis.findAll(r => r.get(\"ts\") < lastTs)\n  if (monthlyHis.colToList(\"monthlyEui\").findAll(v=>v!=null).size<12) throw \"Monthly his < 12 for span: \"+dates.toStr\n\n  annualHis: monthlyHis.map() (r,i)  => do\n    if (i<11) return null\n    monthlyVals: monthlyHis[i-11..i].colToList(\"monthlyEui\").findAll(v => v.isNumber)\n    if (monthlyVals.size != 12) return null\n    annual: monthlyVals.fold(sum)\n    {ts: r.get(\"ts\"), annualEui:annual}\n  end\n\n  annualMeta: if (eci) {dis:\"Annual\", chartGroup:\"annual\", subtitle:\"Annual ECI (annual \\$ / sqft)\"}\n              else     {dis:\"Annual\", chartGroup:\"annual\", subtitle:\"Annual EUI (annual kBTU / sqft)\"}\n  annualHis = annualHis.addColMetaClean(\"annualEui\",annualMeta)\n  if (annualHis.isEmpty) throw \"Annual his is empty. Monthly his of size: \"+monthlyHis.colToList(\"monthlyEui\").findAll(v=>v!=null).size\n\n  grids:  [rawHis, his, eui, monthlyHis, annualHis, sqft].flatten\n  try out: grids.hisJoin\n  catch (err) do\n    log(\"err\", {name:\"virtualSyncHis\",size:rawHis.size}.merge(rawHis.meta),\"check rawHis\")\n    log(\"err\", {name:\"virtualSyncHis\",size:eui.size}.merge(eui.meta),\"check eui\")\n    log(\"err\", {name:\"virtualSyncHis\",size:monthlyHis.size}.merge(monthlyHis.meta),\"check monthlyHis\")\n    log(\"err\", {name:\"virtualSyncHis\",size:annualHis.size}.merge(annualHis.meta),\"check annualHis\")\n    log(\"err\", {name:\"virtualSyncHis\",size:sqft.size}.merge(sqft.meta),\"check sqft\")\n\n    throw err\n  end\n\n  return out.reorderKeepCols([\"ts\"])\nend",
      "local_src": "//todo figure out hisClips\n//todo figure out what to do with missing data\n\n(points, dates, opts:{}) => do\n\n  //normalize rec\n  extendedSpan: (dates.toSpan.start-1yr)..(dates.toSpan.end)\n  if (points.size==0 or points.isEmpty) throw \"No points provided, list is empty\"\n  recs: points.toRecList.toGrid\n  siteRec: recs.first.get(\"siteRef\").toRec\n\n  //get last complete month\n  lastHisEnd: (recs).colToList(\"hisEnd\").sort.first\n  lastTs: lastHisEnd - lastHisEnd.day\n\n  //get opts\n  if (not opts.isDict) throw \"Opts is not a dict\"\n  naHandling: opts.get(\"naHandling\")\n  if (naHandling.isNull) naHandling=\"interpolate\"\n  eci: if (opts.has(\"eci\")) true else false\n\n  //read his\n  rawHis: recs.map(r=>r.merge({chartGroup:\"raw\",subtitle:\"Raw Data\"})).hisRead(extendedSpan, {-limit}).hisClip\n  if (rawHis.size==0) throw \"No his on provided points: \"+points\n\n  his: []\n  recs.map(r => r.set(\"hisLimit\", null)).each() (p,i) => do\n\n    pointHis: p.merge({chartGroup:\"cleaned\", subtitle:\"Cleaned His\"})\n               .hisRead(extendedSpan, {-limit})\n    if (pointHis.size==0) throw \"No point his\"\n\n    //interpolate if applicable\n    if (naHandling==\"interpolate\") pointHis = pointHis.hisInterpolate\n\n    //convert units\n    if (not eci) do\n      if (p.get(\"unit\")==\"kW\") pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\"))\n      if (p.get(\"unit\")==\"MW\") pointHis = pointHis.hisMap(v=>v.to(\"kW\")).hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\"))\n      if (p.get(\"unit\")==\"BTU/h\") pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"BTU\"))\n      if (p.get(\"unit\")==\"kBTU/h\") pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"kBTU\"))\n      \n      if (p.get(\"unit\")==\"lb/h\") do\n        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*1.03).as(\"kBTU\"))\n      end\n      if (p.get(\"unit\")==\"tonref\") do\n        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*12).as(\"kBTU\"))\n      end\n      if (p.get(\"unit\")==\"klb/h\") do\n        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*1030).as(\"kBTU\"))\n      end\n    end\n\n    //convert kwh to btu\n    pointHis = pointHis.hisMap(v=>if (not eci) v.to(\"kBTU\") else v) //todo make sure hisLimit actually works otherwise hisMap is limited to 10k items. adding -hisLimit above hopefully does this\n\n    //if monthly data, then calendarize\n    if (p.has(\"monthly\")) pointHis = pointHis.calendarize(\"v0\")//.hisClip\n    else                  pointHis = pointHis.hisRollup(sum, 1day )                          //.hisClip\n\n    //throw pointHis.size\n    //rollup daily\n    pointHis = pointHis.addColMeta(\"v0\", {dis:\"Daily Rollup - \"+p.dis})\n                       .renameCol(\"v0\",\"daily\"+i)\n\n    //add to his column\n    his = his.add(pointHis)\n    end\n\n  //get square footage\n  sqft:  try getSiteSqftHis(siteRec, extendedSpan, {rollup:1day})\n         catch (err) err\n  if (sqft.isDict) throw \"No sqft point or site area tag on \"+siteRec.dis+\": \"+siteRec.get(\"area\") + \"\\n\"+sqft\n\n  //combine data\n  kbtu : hisJoin([his].flatten).toGrid\n  colNameSize: kbtu.colNames.size\n  if      (naHandling==\"na\")          kbtu = kbtu.findAll(r => r.vals.size==colNameSize and r.vals[1..-1].all(v => v.isNumber))\n  else if (naHandling==\"interpolate\") kbtu = kbtu.hisInterpolate\n\n  //sum up multiple points\n  kbtu = kbtu.map(r => if (r.vals.size==colNameSize) r.set(\"kbtu\",r.vals[1..-1].fold(sum))\n                       else r.set(\"kbtu\",na()))\n\n  //calculate eui\n  eui: hisJoin([kbtu, sqft]).hisClip\n        .map(r => r.set(\"eui\",r.get(\"kbtu\")/r.get(\"sqft\")))\n        .keepCols([\"ts\",\"eui\"])\n        .addColMeta(\"eui\",{dis:\"Daily\"})\n\n  //rollup monthly data\n  monthlyMeta: if (eci) {dis:\"Monthly\", chartGroup:\"monthly\", subtitle:\"Monthly ECI (monthly \\$ / sqft)\"}\n               else     {dis:\"Monthly\", chartGroup:\"monthly\", subtitle:\"Monthly EUI (monthly kBTU / sqft)\"}\n  monthlyHis: eui.hisRollup(sum, 1mo)\n                 .renameCol(\"eui\",\"monthlyEui\")\n                 .addColMeta(\"monthlyEui\", monthlyMeta)\n\n  //clip off last month where data is incomplete\n  monthlyHis = monthlyHis.findAll(r => r.get(\"ts\") < lastTs)\n  if (monthlyHis.colToList(\"monthlyEui\").findAll(v=>v!=null).size<12) throw \"Monthly his < 12 for span: \"+dates.toStr\n\n  annualHis: monthlyHis.map() (r,i)  => do\n    if (i<11) return null\n    monthlyVals: monthlyHis[i-11..i].colToList(\"monthlyEui\").findAll(v => v.isNumber)\n    if (monthlyVals.size != 12) return null\n    annual: monthlyVals.fold(sum)\n    {ts: r.get(\"ts\"), annualEui:annual}\n  end\n\n  annualMeta: if (eci) {dis:\"Annual\", chartGroup:\"annual\", subtitle:\"Annual ECI (annual \\$ / sqft)\"}\n              else     {dis:\"Annual\", chartGroup:\"annual\", subtitle:\"Annual EUI (annual kBTU / sqft)\"}\n  annualHis = annualHis.addColMetaClean(\"annualEui\",annualMeta)\n  if (annualHis.isEmpty) throw \"Annual his is empty. Monthly his of size: \"+monthlyHis.colToList(\"monthlyEui\").findAll(v=>v!=null).size\n\n  grids:  [rawHis, his, eui, monthlyHis, annualHis, sqft].flatten\n  try out: grids.hisJoin\n  catch (err) do\n    log(\"err\", {name:\"virtualSyncHis\",size:rawHis.size}.merge(rawHis.meta),\"check rawHis\")\n    log(\"err\", {name:\"virtualSyncHis\",size:eui.size}.merge(eui.meta),\"check eui\")\n    log(\"err\", {name:\"virtualSyncHis\",size:monthlyHis.size}.merge(monthlyHis.meta),\"check monthlyHis\")\n    log(\"err\", {name:\"virtualSyncHis\",size:annualHis.size}.merge(annualHis.meta),\"check annualHis\")\n    log(\"err\", {name:\"virtualSyncHis\",size:sqft.size}.merge(sqft.meta),\"check sqft\")\n\n    throw err\n  end\n\n  return out.reorderKeepCols([\"ts\"])\nend",
      "diff": "@@ -36,20 +36,18 @@     //convert units\n     if (not eci) do\n       if (p.get(\"unit\")==\"kW\") pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\"))\n+      if (p.get(\"unit\")==\"MW\") pointHis = pointHis.hisMap(v=>v.to(\"kW\")).hisRollup(avg,1hr).hisMap(v=>v.as(\"kWh\"))\n       if (p.get(\"unit\")==\"BTU/h\") pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"BTU\"))\n       if (p.get(\"unit\")==\"kBTU/h\") pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"kBTU\"))\n \n       if (p.get(\"unit\")==\"lb/h\") do\n-        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*0.0012850675).as(\"kBTU\"))\n+        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*1.03).as(\"kBTU\"))\n       end\n       if (p.get(\"unit\")==\"tonref\") do\n-        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*12000).as(\"kBTU\"))\n+        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*12).as(\"kBTU\"))\n       end\n       if (p.get(\"unit\")==\"klb/h\") do\n-        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*1.2850675).as(\"kBTU\"))\n-      end\n-      if (p.get(\"unit\")==\"ft\u00b3_gas\") do\n-        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>v.as(\"kBTU\"))\n+        pointHis = pointHis.hisRollup(avg,1hr).hisMap(v=>(v*1030).as(\"kBTU\"))\n       end\n     end\n \n"
    },
    {
      "name": "logic_hhw_deltaT",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*    Given a Hot Water System, returns the delta T of the\n*    building loop between the supply and return temperatures.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Grid                   target          - The pump input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/12/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/10/2024  - Added individual loop override support (Line 26 & 53-54)\n*/\n(target, dates, opts : {}) => do\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec.get(\"id\")\n  dates = dates.toSpan()\n\n  // Retrieve Points\n  points: logic_hws_buildingLoopDigest(targetId, opts)\n\n  // Missing building loop edge-case handling.\n  if (points.isStr or points.get(\"buildingLoop\").isStr) return \"No building loop exists.\"\n\n  // Retrieve points\n  supplyTemp: points.get(\"buildingLoop\").first.get(\"hw_supply_temp\")\n  returnTemp: points.get(\"buildingLoop\").first.get(\"hw_return_temp\")\n\n  // Missing points edge-case handling.\n  if (supplyTemp.isStr and returnTemp.isStr) return \"Neither supply nor return temperature points exists.\"\n  if (supplyTemp.isStr)                      return \"No supply temp point exists.\"\n  if (returnTemp.isStr)                      return \"no return temp point exists.\"\n\n  // Retrieve his\n  supplyTempHis: supplyTemp.hisRead(dates, {-limit}).hisInterpolate.hisClip\n  returnTempHis: returnTemp.hisRead(dates, {-limit}).hisInterpolate.hisClip\n\n  // Missing His edge-case handling.\n  if (supplyTempHis.isNull or supplyTempHis.isEmpty) return \"No his data for supply temp point\"\n  if (returnTempHis.isNull or returnTempHis.isEmpty) return \"No his data for return temp point\"\n\n  // Combine his points, and calculate delta\n  his : hisJoin([supplyTempHis, returnTempHis]).hisInterpolate\n  his = his.addColTwoPointDelta(\"delta\")\n\n  //Filter for when any building loop pump is running\n  opts = opts.set(\"id\", true)\n  bLoopId: logic_hws_buildingLoopDigest(targetId, opts)\n  pumpRunningHis: logic_hhws_equipsRunning(bLoopId, dates, {equip:\"pump\"}).keepCols([\"ts\", \"anyEquipRunning\"])\n                                                                          .findAll(r => r.get(\"anyEquipRunning\").isNonNull)\n  if(pumpRunningHis.isNonNull) his = his.hisFindInPeriods(pumpRunningHis)\n\n  his = his.map() r => if(r.get(\"delta\") < 0) r = r.set(\"delta\", abs(r.get(\"delta\"))) else r = r\n\n  // HWS 2.0 Preparation\n  funcName: \"logic_hhw_deltaT\"\n  titles: logicFuncTitle(funcName, targetRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  his = his.stream\n           .addColMeta(\"delta\", {dis: \"Delta (\u0394T)\"})\n           .addMeta(outputMeta)\n           .collect\n  return his\nend",
      "local_src": "/*\n*   Description :\n*    Given a Hot Water System, returns the delta T of the\n*    building loop between the supply and return temperatures.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Grid                   target          - The pump input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/12/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/10/2024  - Added individual loop override support (Line 26 & 53-54)\n*/\n(target, dates, opts : {}) => do\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec.get(\"id\")\n  dates = dates.toSpan()\n\n  // Retrieve Points\n  points: logic_hws_buildingLoopDigest(targetId, opts)\n\n  // Missing building loop edge-case handling.\n  if (points.isStr or points.get(\"buildingLoop\").isStr) return \"No building loop exists.\"\n\n  // Retrieve points\n  supplyTemp: points.get(\"buildingLoop\").first.get(\"hw_supply_temp\")\n  returnTemp: points.get(\"buildingLoop\").first.get(\"hw_return_temp\")\n\n  // Missing points edge-case handling.\n  if (supplyTemp.isStr and returnTemp.isStr) return \"Neither supply nor return temperature points exists.\"\n  if (supplyTemp.isStr)                      return \"No supply temp point exists.\"\n  if (returnTemp.isStr)                      return \"no return temp point exists.\"\n\n  // Retrieve his\n  supplyTempHis: supplyTemp.hisRead(dates, {-limit}).hisInterpolate.hisClip\n  returnTempHis: returnTemp.hisRead(dates, {-limit}).hisInterpolate.hisClip\n\n  // Missing His edge-case handling.\n  if (supplyTempHis.isNull or supplyTempHis.isEmpty) return \"No his data for supply temp point\"\n  if (returnTempHis.isNull or returnTempHis.isEmpty) return \"No his data for return temp point\"\n\n  // Combine his points, and calculate delta\n  his : hisJoin([supplyTempHis, returnTempHis]).hisInterpolate\n  his = his.addColTwoPointDelta(\"delta\")\n\n  //Filter for when any building loop pump is running\n  opts = opts.set(\"id\", true)\n  bLoopId: logic_hws_buildingLoopDigest(targetId, opts)\n  pumpRunningHis: try logic_hhws_equipsRunning(bLoopId, dates, {equip:\"pump\"}).keepCols([\"ts\", \"anyEquipRunning\"])\n                                                                          .findAll(r => r.get(\"anyEquipRunning\").isNonNull) catch null //ADDED try catch\n  if(pumpRunningHis.isNonNull) his = his.hisFindInPeriods(pumpRunningHis)\n\n  his = his.map() r => if(r.get(\"delta\") < 0) r = r.set(\"delta\", abs(r.get(\"delta\"))) else r = r\n\n  // HWS 2.0 Preparation\n  funcName: \"logic_hhw_deltaT\"\n  titles: logicFuncTitle(funcName, targetRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  his = his.stream\n           .addColMeta(\"delta\", {dis: \"Delta (\u0394T)\"})\n           .addMeta(outputMeta)\n           .collect\n  return his\nend",
      "diff": "@@ -51,8 +51,8 @@   //Filter for when any building loop pump is running\n   opts = opts.set(\"id\", true)\n   bLoopId: logic_hws_buildingLoopDigest(targetId, opts)\n-  pumpRunningHis: logic_hhws_equipsRunning(bLoopId, dates, {equip:\"pump\"}).keepCols([\"ts\", \"anyEquipRunning\"])\n-                                                                          .findAll(r => r.get(\"anyEquipRunning\").isNonNull)\n+  pumpRunningHis: try logic_hhws_equipsRunning(bLoopId, dates, {equip:\"pump\"}).keepCols([\"ts\", \"anyEquipRunning\"])\n+                                                                          .findAll(r => r.get(\"anyEquipRunning\").isNonNull) catch null //ADDED try catch\n   if(pumpRunningHis.isNonNull) his = his.hisFindInPeriods(pumpRunningHis)\n \n   his = his.map() r => if(r.get(\"delta\") < 0) r = r.set(\"delta\", abs(r.get(\"delta\"))) else r = r\n"
    },
    {
      "name": "logic_hhwp_pumpIsRunning",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     Given a pump from a hot water system, returns the runtime periods for that pump.\n*\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Grid                   target          - The pump input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Additional Options\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/15/2024   - Initial Creation\n*     Thomas Kuhrke Limia    8/27/2024   - Changed fallback order\n*/\n(target, dates, opts:{}) => do\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  site: targetRec.get(\"siteRef\")\n  dates = dates.toSpan()\n\n  // Handle opts\n  boolPeriods: opts.get(\"boolPeriods\") == true\n\n  // Retrieve points\n  points: logic_hws_pumpDigest(targetId)\n\n  // Save ALL points into their respective slot\n  pri_pump_speed  : points.get(\"pri_pump_speed\")\n  sec_pump_speed  : points.get(\"sec_pump_speed\")\n  pri_pump_power  : points.get(\"pri_pump_power\")\n  sec_pump_power  : points.get(\"sec_pump_power\")\n  pri_pump_status : points.get(\"pri_pump_status\")\n  sec_pump_status : points.get(\"sec_pump_status\")\n\n  // Find point based on fallback order\n  existing : if      (not (pri_pump_speed.isStr  and sec_pump_speed.isStr))  [pri_pump_speed, sec_pump_speed].find(v => not v.isStr)\n             else if (not (pri_pump_power.isStr  and sec_pump_power.isStr))  [pri_pump_power, sec_pump_power].find(v => not v.isStr)\n             else if (not (pri_pump_status.isStr and sec_pump_status.isStr)) [pri_pump_status, sec_pump_status].find(v => not v.isStr)\n             else \"All points doesn't exist, or the existing ones have no his.\"\n\n  // Handle edge case where there are no existing points\n  if (existing.isStr) return existing\n\n  // Retrieve his\n  his : hisRead(existing, dates, {-limit}).hisInterpolate.findAll(r => r.get(\"v0\").isNonNull)\n\n  // Handle Edge-case\n  if (his.isEmpty) return \"No his data\"\n\n  // Transform number hisGrid into boolean\n  if (his.first.get(\"v0\").isNumber) his = his.hisRollupAuto().map(r => return {ts: r.get(\"ts\"), v0: r.get(\"v0\") > 1})\n\n  // Return boolean grid if opt is present\n  if (boolPeriods) return his.renameCol(\"v0\", \"running\")\n\n  // Transform into period grid.\n  his = his.conditionToPeriods(\"running\", r => r.get(\"v0\") == true).addColMeta(\"v0\", his.col(\"v0\").meta)\n\n  // CHWS 2.0 Preparation\n  funcName: \"logic_cwp_pumpIsRunning\"\n  titles: logicFuncTitle(funcName, targetRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  outHis : his.stream\n              .addMeta(outputMeta)\n              .collect\n\n  return outHis\n\nend",
      "local_src": "/*\n*   Description :\n*     Given a pump from a hot water system, returns the runtime periods for that pump.\n*\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Grid                   target          - The pump input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Additional Options\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/15/2024   - Initial Creation\n*     Thomas Kuhrke Limia    8/27/2024   - Changed fallback order\n*/\n(target, dates, opts:{}) => do\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  site: targetRec.get(\"siteRef\")\n  dates = dates.toSpan()\n\n  // Handle opts\n  boolPeriods: opts.get(\"boolPeriods\") == true\n\n  // Retrieve points\n  points: logic_hws_pumpDigest(targetId)\n\n  // Save ALL points into their respective slot\n  pri_pump_speed  : points.get(\"pri_pump_speed\")\n  sec_pump_speed  : points.get(\"sec_pump_speed\")\n  pri_pump_power  : points.get(\"pri_pump_power\")\n  sec_pump_power  : points.get(\"sec_pump_power\")\n  pri_pump_status : points.get(\"pri_pump_status\")\n  sec_pump_status : points.get(\"sec_pump_status\")\n\n  // Find point based on fallback order\n  existing : if      (not (pri_pump_speed.isStr  and sec_pump_speed.isStr))  [pri_pump_speed, sec_pump_speed].find(v => not v.isStr)\n             else if (not (pri_pump_power.isStr  and sec_pump_power.isStr))  [pri_pump_power, sec_pump_power].find(v => not v.isStr)\n             else if (not (pri_pump_status.isStr and sec_pump_status.isStr)) [pri_pump_status, sec_pump_status].find(v => not v.isStr)\n             else \"All points doesn't exist, or the existing ones have no his.\"\n\n  // Handle edge case where there are no existing points\n  if (existing.isStr) return existing\n\n  // Retrieve his\n  his : hisRead(existing, dates, {-limit}).hisInterpolate.findAll(r => r.get(\"v0\").isNonNull)\n\n  // Handle Edge-case\n  if (his.isEmpty) return \"No his data\"\n\n  // Transform number hisGrid into boolean\n  if (his.first.get(\"v0\").isNumber) his = his.map(r => return {ts: r.get(\"ts\"), v0: r.get(\"v0\") > 1})\n\n  // Return boolean grid if opt is present\n  if (boolPeriods) return his.renameCol(\"v0\", \"running\")\n\n  // Transform into period grid.\n  his = his.conditionToPeriods(\"running\", r => r.get(\"v0\") == true).addColMeta(\"v0\", his.col(\"v0\").meta)\n\n  // CHWS 2.0 Preparation\n  funcName: \"logic_hhwp_pumpIsRunning\"\n  titles: logicFuncTitle(funcName, targetRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  outHis : his.stream\n              .addMeta(outputMeta)\n              .collect\n\n  return outHis\n\nend",
      "diff": "@@ -52,7 +52,7 @@   if (his.isEmpty) return \"No his data\"\n \n   // Transform number hisGrid into boolean\n-  if (his.first.get(\"v0\").isNumber) his = his.hisRollupAuto().map(r => return {ts: r.get(\"ts\"), v0: r.get(\"v0\") > 1})\n+  if (his.first.get(\"v0\").isNumber) his = his.map(r => return {ts: r.get(\"ts\"), v0: r.get(\"v0\") > 1})\n \n   // Return boolean grid if opt is present\n   if (boolPeriods) return his.renameCol(\"v0\", \"running\")\n@@ -61,7 +61,7 @@   his = his.conditionToPeriods(\"running\", r => r.get(\"v0\") == true).addColMeta(\"v0\", his.col(\"v0\").meta)\n \n   // CHWS 2.0 Preparation\n-  funcName: \"logic_cwp_pumpIsRunning\"\n+  funcName: \"logic_hhwp_pumpIsRunning\"\n   titles: logicFuncTitle(funcName, targetRec, dates)\n   outputMeta: {title:titles[0],\n               subtitle: titles[1],\n"
    },
    {
      "name": "logic_hhws_systemSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     Given a site, returns a summary of the KPI results for the given\n*     chilled water system.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/15/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/10/2024  - Added individual loop override support (Line 63)\n*/\n(site, dates, opts:{}) => do\n\n  // Retrieve Opts\n  hideEmptyValues   : opts.get(\"hideEmptyValues\") == true\n  valueSource       : if (opts.has(\"valueSource\")) opts.get(\"valueSource\").lower else \"auto\"\n  hhwsEquipOverride : if (opts.has(\"hhwsEquipOverride\")) opts.get(\"hhwsEquipOverride\") else null\n  returnDict        : opts.get(\"returnDict\") == true // TODO\n\n  // Retrieve HHWS\n  hhws: if (hhwsEquipOverride != \"\" and hhwsEquipOverride.isNonNull) try readById(hhwsEquipOverride)\n                                                                     catch return \"HHWS Override provided is invalid.\"\n        else logic_hhws_siteToHhws(site)\n\n  // Edge-case handling\n  if (hhws.isNull or hhws.isEmpty) return \"No Hot Water System on this Site\"\n  target: try hhws.toRec.get(\"id\") catch null\n\n  // Normalize Input\n  date: dates.toSpan\n\n  // KPI Funcs\n  funcs: [kpi_hws_percentTimeDPAtSetpoint,\n          kpi_hhws_systemPeakElectricDemand,\n          kpi_hhws_peakSystemEnergyUse,\n          kpi_hhws_totalSystemEnergyUse,\n          kpi_hhw_avgDeltaT,\n          kpi_hhw_percentTimeLowDeltaT,\n          kpi_hhw_avgBypassValvePosition,\n          kpi_hhw_avgHHWReturnTemp,\n          kpi_hhw_avgHHWSupplySetpoint,\n          kpi_hhw_avgHHWSupplyTemp,\n          kpi_hhw_avgHHWSupplyFlow,\n          kpi_hhw_percentTimeSupTempAtSetpoint,\n          kpi_hws_percentTimeDPAtSetpoint,\n          kpi_hhws_primaryLoop_percentRuntimeOnePump,\n          kpi_hhws_primaryLoop_percentRuntimeMultiplePumps,\n          kpi_hhws_secondaryLoop_percentRuntimeOnePump,\n          kpi_hhws_secondaryLoop_percentRuntimeMultiplePumps,\n          kpi_hhws_primaryLoop_percentRuntimeOneBlr,\n          kpi_hhws_primaryLoop_percentRuntimeMultipleBlrs\n          ]\n\n  // Create Output (NOTE: Add support for retrieving \"min\" values by checking the funcs displayName)\n  out : (funcs.map() (curFunc) => do\n    cacheResSize    : try ruleKpis(target, dates, read(rule and ruleFunc == curFunc.get(\"name\")).colToList(\"date\")) catch null\n    incompleteCache : cacheResSize != date.numDays\n    engineRes: if (valueSource == \"auto\" or valueSource == \"kpi\") betterRuleKpis(curFunc, target, date) else null// Read rule cache\n    res : if (valueSource != \"kpi\" and (engineRes.isNull or valueSource == \"call\" or incompleteCache)) call(curFunc, [{target: target, date: date, opts: opts}]).get(\"out\")\n          else engineRes // Call the function regularly if the cache had no info.\n    val : if      (res.isNull or res.isEmpty)     \"missing\"\n          else if (res.isDict and res.has(\"max\")) ((res.get(\"max\")*10).round)/10\n          else                                    ((res.get(res.names.get(0))*10).round)/10\n    if   (incompleteCache and valueSource == \"kpi\" and val != \"missing\")  return {kpi: func(curFunc).get(\"dis\"), value : val.toStr + \" (Incomplete Cache)\"}\n    else                                                                  return {kpi: func(curFunc).get(\"dis\"), value : val}\n  end).toGrid\n\n  // Empty Values Opt\n  if (hideEmptyValues) out = out.findAll(r => not r.rowToList[1..-1].all(v => v==\"missing\"))\n\n  // Add Meta\n  out = out.stream\n           .addColMeta(\"kpi\", {dis: \"KPI\"})\n           .addColMeta(\"value\", {dis: \"System\", enum:{missing: {icon:\"x\", iconColor:\"#FF0000\", dis:\"\"}}})\n           .collect()\n  return out\nend",
      "local_src": "/*\n*   Description :\n*     Given a site, returns a summary of the KPI results for the given\n*     chilled water system.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateTime               dates           - The dates to look at.\n*     Dict                   opts            - Optional Settings\n*\n*   Work :\n*     Who                    When        Change\n*     Thomas Kuhrke Limia    8/15/2024   - Initial Creation\n*     Thomas Kuhrke Limia    10/10/2024  - Added individual loop override support (Line 63)\n*/\n(site, dates, opts:{}) => do\n\n  // Retrieve Opts\n  hideEmptyValues   : opts.get(\"hideEmptyValues\") == true\n  valueSource       : if (opts.has(\"valueSource\")) opts.get(\"valueSource\").lower else \"auto\"\n  hhwsEquipOverride : if (opts.has(\"hhwsEquipOverride\")) opts.get(\"hhwsEquipOverride\") else null\n  returnDict        : opts.get(\"returnDict\") == true // TODO\n\n  // Retrieve HHWS\n  hhws: if (hhwsEquipOverride != \"\" and hhwsEquipOverride.isNonNull) try readById(hhwsEquipOverride)\n                                                                     catch return \"HHWS Override provided is invalid.\"\n        else logic_hhws_siteToHhws(site)\n\n  // Edge-case handling\n  if (hhws.isNull or hhws.isEmpty) return \"No Hot Water System on this Site\"\n  target: try hhws.toRec.get(\"id\") catch null\n\n  // Normalize Input\n  date: dates.toSpan\n\n  // KPI Funcs\n  funcs: [kpi_hws_percentTimeDPAtSetpoint,\n          //kpi_hhws_systemPeakElectricDemand,\n          //kpi_hhws_peakSystemEnergyUse,\n          kpi_hhws_totalSystemEnergyUse,\n          kpi_hhw_avgDeltaT,\n          kpi_hhw_percentTimeLowDeltaT,\n          //kpi_hhw_avgBypassValvePosition,\n          kpi_hhw_avgHHWReturnTemp,\n          kpi_hhw_avgHHWSupplySetpoint,\n          kpi_hhw_avgHHWSupplyTemp,\n          kpi_hhw_avgHHWSupplyFlow,\n          kpi_hhw_percentTimeSupTempAtSetpoint,\n          kpi_hws_percentTimeDPAtSetpoint,\n          //kpi_hhws_primaryLoop_percentRuntimeOnePump,\n          //kpi_hhws_primaryLoop_percentRuntimeMultiplePumps,\n          kpi_hhws_secondaryLoop_percentRuntimeOnePump,\n          kpi_hhws_secondaryLoop_percentRuntimeMultiplePumps,\n          //kpi_hhws_primaryLoop_percentRuntimeOneBlr,\n          //kpi_hhws_primaryLoop_percentRuntimeMultipleBlrs\n          ]\n\n  // Create Output (NOTE: Add support for retrieving \"min\" values by checking the funcs displayName)\n  out : (funcs.map() (curFunc) => do\n    cacheResSize    : try ruleKpis(target, dates, read(rule and ruleFunc == curFunc.get(\"name\")).colToList(\"date\")) catch null\n    incompleteCache : cacheResSize != date.numDays\n    engineRes: if (valueSource == \"auto\" or valueSource == \"kpi\") betterRuleKpis(curFunc, target, date) else null// Read rule cache\n    res : if (valueSource != \"kpi\" and (engineRes.isNull or valueSource == \"call\" or incompleteCache)) try call(curFunc, [{target: target, date: date, opts: opts}]).get(\"out\") catch null //ADDED try catch\n          else engineRes // Call the function regularly if the cache had no info.\n    val : if      (res.isNull or res.isEmpty)     \"missing\"\n          else if (res.isDict and res.has(\"max\")) ((res.get(\"max\")*10).round)/10\n          else                                    ((res.get(res.names.get(0))*10).round)/10\n    if   (incompleteCache and valueSource == \"kpi\" and val != \"missing\")  return {kpi: func(curFunc).get(\"dis\"), value : val.toStr + \" (Incomplete Cache)\"}\n    else                                                                  return {kpi: func(curFunc).get(\"dis\"), value : val}\n  end).toGrid\n\n  // Empty Values Opt\n  if (hideEmptyValues) out = out.findAll(r => not r.rowToList[1..-1].all(v => v==\"missing\"))\n\n  // Add Meta\n  out = out.stream\n           .addColMeta(\"kpi\", {dis: \"KPI\"})\n           .addColMeta(\"value\", {dis: \"System\", enum:{missing: {icon:\"x\", iconColor:\"#FF0000\", dis:\"\"}}})\n           .collect()\n  return out\nend",
      "diff": "@@ -36,24 +36,24 @@ \n   // KPI Funcs\n   funcs: [kpi_hws_percentTimeDPAtSetpoint,\n-          kpi_hhws_systemPeakElectricDemand,\n-          kpi_hhws_peakSystemEnergyUse,\n+          //kpi_hhws_systemPeakElectricDemand,\n+          //kpi_hhws_peakSystemEnergyUse,\n           kpi_hhws_totalSystemEnergyUse,\n           kpi_hhw_avgDeltaT,\n           kpi_hhw_percentTimeLowDeltaT,\n-          kpi_hhw_avgBypassValvePosition,\n+          //kpi_hhw_avgBypassValvePosition,\n           kpi_hhw_avgHHWReturnTemp,\n           kpi_hhw_avgHHWSupplySetpoint,\n           kpi_hhw_avgHHWSupplyTemp,\n           kpi_hhw_avgHHWSupplyFlow,\n           kpi_hhw_percentTimeSupTempAtSetpoint,\n           kpi_hws_percentTimeDPAtSetpoint,\n-          kpi_hhws_primaryLoop_percentRuntimeOnePump,\n-          kpi_hhws_primaryLoop_percentRuntimeMultiplePumps,\n+          //kpi_hhws_primaryLoop_percentRuntimeOnePump,\n+          //kpi_hhws_primaryLoop_percentRuntimeMultiplePumps,\n           kpi_hhws_secondaryLoop_percentRuntimeOnePump,\n           kpi_hhws_secondaryLoop_percentRuntimeMultiplePumps,\n-          kpi_hhws_primaryLoop_percentRuntimeOneBlr,\n-          kpi_hhws_primaryLoop_percentRuntimeMultipleBlrs\n+          //kpi_hhws_primaryLoop_percentRuntimeOneBlr,\n+          //kpi_hhws_primaryLoop_percentRuntimeMultipleBlrs\n           ]\n \n   // Create Output (NOTE: Add support for retrieving \"min\" values by checking the funcs displayName)\n@@ -61,7 +61,7 @@     cacheResSize    : try ruleKpis(target, dates, read(rule and ruleFunc == curFunc.get(\"name\")).colToList(\"date\")) catch null\n     incompleteCache : cacheResSize != date.numDays\n     engineRes: if (valueSource == \"auto\" or valueSource == \"kpi\") betterRuleKpis(curFunc, target, date) else null// Read rule cache\n-    res : if (valueSource != \"kpi\" and (engineRes.isNull or valueSource == \"call\" or incompleteCache)) call(curFunc, [{target: target, date: date, opts: opts}]).get(\"out\")\n+    res : if (valueSource != \"kpi\" and (engineRes.isNull or valueSource == \"call\" or incompleteCache)) try call(curFunc, [{target: target, date: date, opts: opts}]).get(\"out\") catch null //ADDED try catch\n           else engineRes // Call the function regularly if the cache had no info.\n     val : if      (res.isNull or res.isEmpty)     \"missing\"\n           else if (res.isDict and res.has(\"max\")) ((res.get(\"max\")*10).round)/10\n"
    },
    {
      "name": "logic_hisPoint_missingData",
      "pod": "kwLinkCoreExt",
      "pod_src": "(target, dates, opts: {}) => do\n  cipherText :\n    \"AxD:tnVmPw83PQLMOKwn-pkJeA::ckW6lCkR97Mf2mjlP2jqEEqYo9IYSPGYT5z6X0x9d0ttkHwpJ7j1\" +\n    \"Rd-BFOMN8fajur85oSps3v8-f5B14FJgQzh3u3NQsunH7zENgNuLkDqpDu29PlWFju5EVk2daOEQbUQz\" +\n    \"6tLidM3mcTfnWTUULp19ofGH_XpSq3vbFs6Nyywq1N-9B1hIVMBR3Ze1ex0tYbBCHVg1_Ix3lSCQGGXZ\" +\n    \"rXP8hyjKfhINXzaWrXsY-gdE4MQPb3TnHWqM2lvYrhsZYQ_itA1TPgRzdEAI_3q3rgieUv19XxwLoV7u\" +\n    \"9PftpgqpskBDN71X2dZL2CyAzwXBI1xyJoHr-Ou-WEQ4lRI5mDpT0kwzF3nD3tQepHuAc_VYu4O89V6Q\" +\n    \"Kpq0DmzGHV5pXq_YGDXWD6X6XfxfoO9t24KGfX6LrDMJX5hgxzq9b0c_YIXWyQA4fFdrwbM9MQAqI3VD\" +\n    \"j3jkTpGW0bakHSV5mvY4l65uFVCv3jqNSVBEq5JVXvOX3sZBl9XvRIrFknyBgj8wOpe4r07pKkP_eom3\" +\n    \"ewuHx7-Jr5GoxCc-H1XliBn36JDWTabYt3VUvjapAI3x4j9hhauwnaCXrPax-9IfVJdl1B4d_nupqhw3\" +\n    \"cGGr-eqY1EjPFtu4TGHiNPr_FyHTPpSVBe8aRyJFNCMold5RdWcSfwvWMSkZ4t1D5ACCrR9YpqzR59a9\" +\n    \"DbixgAQXmMpyJndvANPAIbrY3iAzsYhO_OHhmFfzEZSom54-pvPwd-juS4neMQmwIg6c8HEWgQkYcarA\" +\n    \"Ui2AoMoPhNgqpmu0aA\"\n  keyFunc : () => \"ddJNIRm2qio2Z7CXpKADiw\"\n  return afAxonEncryptorRt_callFunc(\"logic_hisPoint_missingData\", cipherText, [target, dates, opts], keyFunc)\nend",
      "local_src": "/*\n*   Description :\n*   Retrieves the periods in which the historized point is missing data.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    target          Historized Point\n*     DateSpan               dates           Dates to look at\n*     Dict                   opts            Additional Options\n*\n*   Output :\n*     Type                   Name            Description\n*     hisGrid                out            - Period Grid of missing periods, or null if no history found.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    08/22/2025      - Initial Creation\n*/\n(target, dates, opts: {}) => do\n    \n    // Normalize Inputs\n    targetRec: target.toRec\n    targetId: targetRec.get(\"id\")\n    dates = dates.toSpan\n    \n    // Retrieve his data, and calculate durations between timestamps\n    his: targetId.hisRead(dates, {-limit})\n    \n    // Edge-case (Empty His returns null)\n    if (his.hisClip.isEmpty) return null\n    \n    // Add the delta column of durations\n    deltaHis: his.addColDelta(\"ts\")\n    \n    // Determine Data Interval to use (prefer meta interval, and fallback on median)\n    dataInterval: deltaHis.foldCol(\"delta\", median)\n\n    // Find periods where the dataInterval doesnt match\n    periods: deltaHis.keepCols([\"ts\", \"delta\"])\n                     .hisFindPeriods(v => v.isNumber and v != dataInterval)\n                     .renameCol(\"delta\", \"dur\")\n    \n    // Combine all data for output\n    out: hisJoin([his, periods])\n    \n    // Return\n    return out\nend",
      "diff": "@@ -1,16 +1,48 @@+/*\n+*   Description :\n+*   Retrieves the periods in which the historized point is missing data.\n+*\n+*   Parameters :\n+*     Type                   Name            Description\n+*     Ref                    target          Historized Point\n+*     DateSpan               dates           Dates to look at\n+*     Dict                   opts            Additional Options\n+*\n+*   Output :\n+*     Type                   Name            Description\n+*     hisGrid                out            - Period Grid of missing periods, or null if no history found.\n+*\n+*   Work :\n+*     Who                    When            Change\n+*     Thomas Kuhrke Limia    08/22/2025      - Initial Creation\n+*/\n (target, dates, opts: {}) => do\n-  cipherText :\n-    \"AxD:tnVmPw83PQLMOKwn-pkJeA::ckW6lCkR97Mf2mjlP2jqEEqYo9IYSPGYT5z6X0x9d0ttkHwpJ7j1\" +\n-    \"Rd-BFOMN8fajur85oSps3v8-f5B14FJgQzh3u3NQsunH7zENgNuLkDqpDu29PlWFju5EVk2daOEQbUQz\" +\n-    \"6tLidM3mcTfnWTUULp19ofGH_XpSq3vbFs6Nyywq1N-9B1hIVMBR3Ze1ex0tYbBCHVg1_Ix3lSCQGGXZ\" +\n-    \"rXP8hyjKfhINXzaWrXsY-gdE4MQPb3TnHWqM2lvYrhsZYQ_itA1TPgRzdEAI_3q3rgieUv19XxwLoV7u\" +\n-    \"9PftpgqpskBDN71X2dZL2CyAzwXBI1xyJoHr-Ou-WEQ4lRI5mDpT0kwzF3nD3tQepHuAc_VYu4O89V6Q\" +\n-    \"Kpq0DmzGHV5pXq_YGDXWD6X6XfxfoO9t24KGfX6LrDMJX5hgxzq9b0c_YIXWyQA4fFdrwbM9MQAqI3VD\" +\n-    \"j3jkTpGW0bakHSV5mvY4l65uFVCv3jqNSVBEq5JVXvOX3sZBl9XvRIrFknyBgj8wOpe4r07pKkP_eom3\" +\n-    \"ewuHx7-Jr5GoxCc-H1XliBn36JDWTabYt3VUvjapAI3x4j9hhauwnaCXrPax-9IfVJdl1B4d_nupqhw3\" +\n-    \"cGGr-eqY1EjPFtu4TGHiNPr_FyHTPpSVBe8aRyJFNCMold5RdWcSfwvWMSkZ4t1D5ACCrR9YpqzR59a9\" +\n-    \"DbixgAQXmMpyJndvANPAIbrY3iAzsYhO_OHhmFfzEZSom54-pvPwd-juS4neMQmwIg6c8HEWgQkYcarA\" +\n-    \"Ui2AoMoPhNgqpmu0aA\"\n-  keyFunc : () => \"ddJNIRm2qio2Z7CXpKADiw\"\n-  return afAxonEncryptorRt_callFunc(\"logic_hisPoint_missingData\", cipherText, [target, dates, opts], keyFunc)\n+\n+    // Normalize Inputs\n+    targetRec: target.toRec\n+    targetId: targetRec.get(\"id\")\n+    dates = dates.toSpan\n+\n+    // Retrieve his data, and calculate durations between timestamps\n+    his: targetId.hisRead(dates, {-limit})\n+\n+    // Edge-case (Empty His returns null)\n+    if (his.hisClip.isEmpty) return null\n+\n+    // Add the delta column of durations\n+    deltaHis: his.addColDelta(\"ts\")\n+\n+    // Determine Data Interval to use (prefer meta interval, and fallback on median)\n+    dataInterval: deltaHis.foldCol(\"delta\", median)\n+\n+    // Find periods where the dataInterval doesnt match\n+    periods: deltaHis.keepCols([\"ts\", \"delta\"])\n+                     .hisFindPeriods(v => v.isNumber and v != dataInterval)\n+                     .renameCol(\"delta\", \"dur\")\n+\n+    // Combine all data for output\n+    out: hisJoin([his, periods])\n+\n+    // Return\n+    return out\n end"
    },
    {
      "name": "logic_kwModeling_anomalyDetection_simPreview_v2",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/* ============================================> TODO: Go through the entire pipeline and simplify drastically: get rid of all the unnecessary fats\n*   Description :\n*    Given a modelPoint Rec/Ref, the function retrieves the zinc file containing the confidence interval for the modelPoint predictions from the io/ directory.\n*\n*   Parameters :\n*     Type                   Name               Description\n*     Rec/Ref                meterPoint         - Meter point to get the predictions and anomalies for\n*     Rec/Ref                modelPoint         - Model point to get the predictions and anomalies for\n*     Date Span              dateSpan           - Date span to look for the anomalies\n*     Str                    sensitivity        - Tunes which of the many hisPeriodsGrids to display or hide. One of: \"high\" (absolute/any anomaly), \"low\" (large anomaly only) \"instantaneous\", \"sustained\", \"all\"\n*     Dict                   opts               - Optional Setting: NA for now\n*\n*\n*   Output :\n*     Type                   Name            Description\n*     Grid                   hisGrid         - A processed hisGrid containing data, predictions, deltas, and impacts\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         11/07/2024      - Initial Creation\n*/\n\n// Example of how to use/test\n// syntheticPoint1: (@p:kwLinkModelExt_apoorv:r:2eb2bce4-8a0fc1dd).toRec\n// syntheticPoint2: (@p:kwLinkModelExt_apoorv:r:2eb3ff8c-1641180e).toRec\n// syntheticPoint3: (@p:kwLinkModelExt_apoorv:r:2eb7d1aa-2cd4194e).toRec\n// syntheticPoint4: (@p:kwLinkModelExt_apoorv:r:2eaeb4cc-e571e327).toRec\n// syntheticPoint5: (@p:kwLinkModelExt_apoorv:r:2eb67096-ca45afaa).toRec\n// syntheticPoint6: (@p:kwLinkModelExt_apoorv:r:2eb6615b-968df8fb).toRec\n// syntheticPoint7: (@p:kwLinkModelExt_apoorv:r:2eb3ffca-747d46b7).toRec\n// meterPoint1: @p:kwLinkModelExt_apoorv:r:2eabf395-5091a9e2\n// meterPoint3: @p:kwLinkModelExt_apoorv:r:2eabf32d-5ad0d063\n// meterPoint5: @p:kwLinkModelExt_apoorv:r:2eabf3ae-7ce3a875\n// dateSpan: 2024-01-15..2024-05-25\n// sensitivity:\"high\"\n// logic_kwModeling_anomalyDetection_simPreview(syntheticPoint5, dateSpan, sensitivity)\n\n(modelPoint, dateSpan, sensitivity, opts:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  //check for null input, return help\n  if (modelPoint.isNull or (not (modelPoint.isRef or modelPoint.isDict))) return curFunc().get(\"help\")\n  if (modelPoint==@null) return \"No Model Point Selected\"\n\n  modelPoint  = modelPoint.toRec\n  meterPoint : modelPoint.get(\"pointRef\").toRec\n  dateSpan    = dateSpan.toSpan\n\n  //extendedSpan: (dateSpan.start - 1wk)..dateSpan.end // TODO: Why did we need an extended span?\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  debug: optNorm(opts, \"debug\", \"disable\").lower\n\n  // Extract tunable parameters from opts and check for proper units\n\n  minAnomalyDuration        : opts.optNorm(\"minAnomalyDuration\", 2hr)\n    if (not (minAnomalyDuration.unit.isNull or minAnomalyDuration.unit == \"hr\")) return \"Minimum anomaly duration must be in hours, not \" + minAnomalyDuration.unit\n    if (minAnomalyDuration.unit.isNull) minAnomalyDuration = minAnomalyDuration.as(\"hr\")\n\n  highAnomalyThreshold      : opts.optNorm(\"highAnomalyThreshold\", 10%)\n    if (not (highAnomalyThreshold.unit.isNull or highAnomalyThreshold.unit == \"%\")) return \"High anomaly threshold must be in \\\"%\\\", not \" + highAnomalyThreshold.unit\n    if (highAnomalyThreshold.unit.isNull) highAnomalyThreshold = highAnomalyThreshold.as(\"%\")\n\n  sustainedAnomalyThreshold : opts.optNorm(\"sustainedAnomalyThreshold\", 8hr)\n    if (not (sustainedAnomalyThreshold.unit.isNull or sustainedAnomalyThreshold.unit == \"hr\")) return \"Sustained anomaly threshold (minimum anomalous duration for sustained anomaly) must be in \\\"h\\\", not \" + sustainedAnomalyThreshold.unit\n    if (sustainedAnomalyThreshold.unit.isNull) sustainedAnomalyThreshold = sustainedAnomalyThreshold.as(\"hr\")\n\n  opts = opts.merge({minAnomalyDuration        : minAnomalyDuration,\n                     highAnomalyThreshold      : highAnomalyThreshold,\n                     sustainedAnomalyThreshold : sustainedAnomalyThreshold})\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n//////// Get relevant points and his data\n\n  points : [meterPoint, modelPoint]\n    // Debug points\n    if (debug==\"points\") return points\n\n  // ReadHis for meter and model points\n  data : points.hisRead(dateSpan, {-limit})\n               .hisRollup(avg, 1h)         // ",
      "local_src": "/* ============================================> TODO: Go through the entire pipeline and simplify drastically: get rid of all the unnecessary fats\n*   Description :\n*    Given a modelPoint Rec/Ref, the function retrieves the zinc file containing the confidence interval for the modelPoint predictions from the io/ directory.\n*\n*   Parameters :\n*     Type                   Name               Description\n*     Rec/Ref                meterPoint         - Meter point to get the predictions and anomalies for\n*     Rec/Ref                modelPoint         - Model point to get the predictions and anomalies for\n*     Date Span              dateSpan           - Date span to look for the anomalies\n*     Str                    sensitivity        - Tunes which of the many hisPeriodsGrids to display or hide. One of: \"high\" (absolute/any anomaly), \"low\" (large anomaly only) \"instantaneous\", \"sustained\", \"all\"\n*     Dict                   opts               - Optional Setting: NA for now\n*\n*\n*   Output :\n*     Type                   Name            Description\n*     Grid                   hisGrid         - A processed hisGrid containing data, predictions, deltas, and impacts\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         11/07/2024      - Initial Creation\n*/\n\n// Example of how to use/test\n// syntheticPoint1: (@p:kwLinkModelExt_apoorv:r:2eb2bce4-8a0fc1dd).toRec\n// syntheticPoint2: (@p:kwLinkModelExt_apoorv:r:2eb3ff8c-1641180e).toRec\n// syntheticPoint3: (@p:kwLinkModelExt_apoorv:r:2eb7d1aa-2cd4194e).toRec\n// syntheticPoint4: (@p:kwLinkModelExt_apoorv:r:2eaeb4cc-e571e327).toRec\n// syntheticPoint5: (@p:kwLinkModelExt_apoorv:r:2eb67096-ca45afaa).toRec\n// syntheticPoint6: (@p:kwLinkModelExt_apoorv:r:2eb6615b-968df8fb).toRec\n// syntheticPoint7: (@p:kwLinkModelExt_apoorv:r:2eb3ffca-747d46b7).toRec\n// meterPoint1: @p:kwLinkModelExt_apoorv:r:2eabf395-5091a9e2\n// meterPoint3: @p:kwLinkModelExt_apoorv:r:2eabf32d-5ad0d063\n// meterPoint5: @p:kwLinkModelExt_apoorv:r:2eabf3ae-7ce3a875\n// dateSpan: 2024-01-15..2024-05-25\n// sensitivity:\"high\"\n// logic_kwModeling_anomalyDetection_simPreview(syntheticPoint5, dateSpan, sensitivity)\n\n(modelPoint, dateSpan, sensitivity, opts:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  //check for null input, return help\n  if (modelPoint.isNull or (not (modelPoint.isRef or modelPoint.isDict))) return curFunc().get(\"help\")\n  if (modelPoint==@null) return \"No Model Point Selected\"\n\n  modelPoint  = modelPoint.toRec\n  meterPoint : modelPoint.get(\"pointRef\").toRec\n  dateSpan    = dateSpan.toSpan\n\n  //extendedSpan: (dateSpan.start - 1wk)..dateSpan.end // TODO: Why did we need an extended span?\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  debug: optNorm(opts, \"debug\", \"disable\").lower\n\n  // Extract tunable parameters from opts and check for proper units\n\n  minAnomalyDuration        : opts.optNorm(\"minAnomalyDuration\", 2hr)\n    if (not (minAnomalyDuration.unit.isNull or minAnomalyDuration.unit == \"hr\")) return \"Minimum anomaly duration must be in hours, not \" + minAnomalyDuration.unit\n    if (minAnomalyDuration.unit.isNull) minAnomalyDuration = minAnomalyDuration.as(\"hr\")\n\n  highAnomalyThreshold      : opts.optNorm(\"highAnomalyThreshold\", 10%)\n    if (not (highAnomalyThreshold.unit.isNull or highAnomalyThreshold.unit == \"%\")) return \"High anomaly threshold must be in \\\"%\\\", not \" + highAnomalyThreshold.unit\n    if (highAnomalyThreshold.unit.isNull) highAnomalyThreshold = highAnomalyThreshold.as(\"%\")\n\n  sustainedAnomalyThreshold : opts.optNorm(\"sustainedAnomalyThreshold\", 8hr)\n    if (not (sustainedAnomalyThreshold.unit.isNull or sustainedAnomalyThreshold.unit == \"hr\")) return \"Sustained anomaly threshold (minimum anomalous duration for sustained anomaly) must be in \\\"h\\\", not \" + sustainedAnomalyThreshold.unit\n    if (sustainedAnomalyThreshold.unit.isNull) sustainedAnomalyThreshold = sustainedAnomalyThreshold.as(\"hr\")\n\n  opts = opts.merge({minAnomalyDuration        : minAnomalyDuration,\n                     highAnomalyThreshold      : highAnomalyThreshold,\n                     sustainedAnomalyThreshold : sustainedAnomalyThreshold})\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n//////// Get relevant points and his data\n\n  points : [meterPoint, modelPoint]\n    // Debug points\n    if (debug==\"points\") return points\n\n  // ReadHis for meter and model points\n  data : points.hisRead(dateSpan, {-limit})\n               .hisRollup(avg, 1h)         // ------- Double check logic robustness for all units. Should we use hisRollupAuto (1h) instead? Can it handle 15 minute kW/kWh data both?\n               .hisClip\n//return data\n//////// Add checks: hisStart and hisEnd for both points against the extended span\n\n  requiredCols: [\"ts\", \"v0\", \"v1\"]\n  if (requiredCols.any(v => data.missing(v) == true)) return [].toGrid.addMeta({noData: \"Missing Data Error. Missing required column(s): \" + requiredCols.findAll(v=>his.missing(v) == true)})\n  // hisRead create a grid that has v1 col even if the col doesn't have any data. So the above condition doesn't fail.\n\n  if (not (data.col(\"v0\").meta.has(\"hisStart\") or data.col(\"v0\").meta.has(\"hisEnd\"))) return blankChart(\"Meter Point missing hisStart or hisEnd tags. Might be missing his data for the selected span.\")\n  if (not (data.col(\"v1\").meta.has(\"hisStart\") or data.col(\"v1\").meta.has(\"hisEnd\"))) return blankChart(\"Model Point missing hisStart or hisEnd tags. Might be missing his data for the selected span.\")\n\n  if (data.col(\"v0\").meta.get(\"hisStart\") > dateSpan.start) return \"Meter Point his data start date (\" + data.col(\"v0\").meta.get(\"hisStart\").date.toStr + \") is later than selected span start date. Select a span that begins a week after this date.\"\n  if (data.col(\"v1\").meta.get(\"hisStart\") > dateSpan.start) return \"Model Point his data start date (\" + data.col(\"v1\").meta.get(\"hisStart\").date.toStr + \") is later than selected span start date. Select a span that begins a week after this date.\"\n\n  if (data.col(\"v0\").meta.get(\"hisEnd\") < dateSpan.end) return \"Meter Point his data end date (\" + data.col(\"v0\").meta.get(\"hisEnd\").date.toStr + \") is before the selected span end date. Select a span that ends before this date.\"\n  if (data.col(\"v1\").meta.get(\"hisEnd\") < dateSpan.end) return \"Model Point his data end date (\" + data.col(\"v1\").meta.get(\"hisEnd\").date.toStr + \") is before the selected span end date. Select a span that ends before this date.\"\n\n\n//////// Get confidence interval data\n\n  // Get confidence intervals for the model point\n  confIntervals: logic_kwModeling_getModelPointResults(modelPoint).reorderKeepCols([\"ts\"])\n    if (confIntervals.isStr) return confIntervals\n    if (confIntervals.first.get(\"ts\") > dateSpan.start) return \"The results file his data start date (\" + confIntervals.first.get(\"ts\").date.toStr + \") is later than selected span start date. Select a span that begins a week after this date.\"\n\n  // Filter for extendedDateSpan and update meta based on extendedSpan (needed for processing results)\n  filteredConfIntervals: confIntervals.findAll() r=> (r.get(\"ts\") >= dateSpan.start and r.get(\"ts\") < dateSpan.end)\n  filteredConfIntervals= filteredConfIntervals.addMeta({hisStart:dateSpan.start, hisEnd: dateSpan.end})\n//return confIntervals\n\n  // hisJoin meter data, predictions and confidence intervals\n  out : hisJoin([data, filteredConfIntervals]).reorderKeepCols([\"ts\", \"v0\", \"v1\"])\n//return out\n//////// Find and flag different types of anomalies\n\n  // Process hisGrid to find anomalous periods and their durations Send to\n  out = out.logic_kwModeling_anomalyDetection_processSimResults_v2(sensitivity, opts)\n    if (out.isStr) return out\n\n  // Filter for selected dateSpan\n  out = out.findAll() r=> (r.get(\"ts\") >= dateSpan.start and r.get(\"ts\") < dateSpan.end)\n\n  \n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // FORMAT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // Update meta based on actual selected Span\n  out = out.addMeta({hisStart:dateSpan.start, hisEnd: dateSpan.end})\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  //RETURN RESULT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  return out\n\nend",
      "diff": "@@ -88,4 +88,63 @@ \n   // ReadHis for meter and model points\n   data : points.hisRead(dateSpan, {-limit})\n-               .hisRollup(avg, 1h)         //+               .hisRollup(avg, 1h)         // ------- Double check logic robustness for all units. Should we use hisRollupAuto (1h) instead? Can it handle 15 minute kW/kWh data both?\n+               .hisClip\n+//return data\n+//////// Add checks: hisStart and hisEnd for both points against the extended span\n+\n+  requiredCols: [\"ts\", \"v0\", \"v1\"]\n+  if (requiredCols.any(v => data.missing(v) == true)) return [].toGrid.addMeta({noData: \"Missing Data Error. Missing required column(s): \" + requiredCols.findAll(v=>his.missing(v) == true)})\n+  // hisRead create a grid that has v1 col even if the col doesn't have any data. So the above condition doesn't fail.\n+\n+  if (not (data.col(\"v0\").meta.has(\"hisStart\") or data.col(\"v0\").meta.has(\"hisEnd\"))) return blankChart(\"Meter Point missing hisStart or hisEnd tags. Might be missing his data for the selected span.\")\n+  if (not (data.col(\"v1\").meta.has(\"hisStart\") or data.col(\"v1\").meta.has(\"hisEnd\"))) return blankChart(\"Model Point missing hisStart or hisEnd tags. Might be missing his data for the selected span.\")\n+\n+  if (data.col(\"v0\").meta.get(\"hisStart\") > dateSpan.start) return \"Meter Point his data start date (\" + data.col(\"v0\").meta.get(\"hisStart\").date.toStr + \") is later than selected span start date. Select a span that begins a week after this date.\"\n+  if (data.col(\"v1\").meta.get(\"hisStart\") > dateSpan.start) return \"Model Point his data start date (\" + data.col(\"v1\").meta.get(\"hisStart\").date.toStr + \") is later than selected span start date. Select a span that begins a week after this date.\"\n+\n+  if (data.col(\"v0\").meta.get(\"hisEnd\") < dateSpan.end) return \"Meter Point his data end date (\" + data.col(\"v0\").meta.get(\"hisEnd\").date.toStr + \") is before the selected span end date. Select a span that ends before this date.\"\n+  if (data.col(\"v1\").meta.get(\"hisEnd\") < dateSpan.end) return \"Model Point his data end date (\" + data.col(\"v1\").meta.get(\"hisEnd\").date.toStr + \") is before the selected span end date. Select a span that ends before this date.\"\n+\n+\n+//////// Get confidence interval data\n+\n+  // Get confidence intervals for the model point\n+  confIntervals: logic_kwModeling_getModelPointResults(modelPoint).reorderKeepCols([\"ts\"])\n+    if (confIntervals.isStr) return confIntervals\n+    if (confIntervals.first.get(\"ts\") > dateSpan.start) return \"The results file his data start date (\" + confIntervals.first.get(\"ts\").date.toStr + \") is later than selected span start date. Select a span that begins a week after this date.\"\n+\n+  // Filter for extendedDateSpan and update meta based on extendedSpan (needed for processing results)\n+  filteredConfIntervals: confIntervals.findAll() r=> (r.get(\"ts\") >= dateSpan.start and r.get(\"ts\") < dateSpan.end)\n+  filteredConfIntervals= filteredConfIntervals.addMeta({hisStart:dateSpan.start, hisEnd: dateSpan.end})\n+//return confIntervals\n+\n+  // hisJoin meter data, predictions and confidence intervals\n+  out : hisJoin([data, filteredConfIntervals]).reorderKeepCols([\"ts\", \"v0\", \"v1\"])\n+//return out\n+//////// Find and flag different types of anomalies\n+\n+  // Process hisGrid to find anomalous periods and their durations Send to\n+  out = out.logic_kwModeling_anomalyDetection_processSimResults_v2(sensitivity, opts)\n+    if (out.isStr) return out\n+\n+  // Filter for selected dateSpan\n+  out = out.findAll() r=> (r.get(\"ts\") >= dateSpan.start and r.get(\"ts\") < dateSpan.end)\n+\n+\n+\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+  // FORMAT\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+\n+  // Update meta based on actual selected Span\n+  out = out.addMeta({hisStart:dateSpan.start, hisEnd: dateSpan.end})\n+\n+\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+  //RETURN RESULT\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+\n+  return out\n+\n+end"
    },
    {
      "name": "logic_kwModeling_autoCreateModelConfig",
      "pod": "kwLinkModelExt",
      "pod_src": "/*\n*   Description :\n*    Given a point, it creates a model config using opts to override the default values.\n*\n*   Parameters :\n*     Type                   Name               Description\n*     Ref                    point              - Point Ref to generate the config\n*     Dict                   opts               - Additional Options\n*\n*   Output :\n*     Type                   Name            Description\n*     Dict                   config          - The newly created config\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    02/14/2025      - Initial Creation\n*/\n\n(point, opts:{}) => do\n\n  // Normalize Inputs\n  pointRec: point.toRec\n  pointId: pointRec.get(\"id\")\n  siteRef: pointRec.get(\"siteRef\")\n\n  // Modeling Opts Retrieval\n  modelEngine: opts.get(\"modelEngine\")\n  modelMethod: opts.get(\"modelMethod\")\n  tempPointId: read(point and temp and weatherStationRef == pointRec.get(\"siteRef\").toRec.get(\"weatherStationRef\"), false).toRec.get(\"id\")\n\n  occPointId:  if (opts.has(\"occ\")) opts.get(\"occ\").toRec.get(\"id\")\n               else try read(wifi and occ and point and sensor and calculatedPercent and siteRef == siteRef).toRec.get(\"id\") catch null\n  additionalPoints: if (opts.has(\"additionalPoints\") and opts.get(\"additionalPoints\").isList and not opts.get(\"additionalPoints\").isEmpty) opts.get(\"additionalPoints\") else try readAll(indicatorVariable).colToList(\"id\") catch null\n\n  // PreProcess Configs Retrieval\n  zScoreOutlierExclusion: if (opts.get(\"zScoreOutlierExclusion\").isNonNull) opts.get(\"zScoreOutlierExclusion\")\n                          else                                              15\n  outlierLimits:          if (opts.get(\"outlierLimits\").isNonNull)          opts.get(\"outlierLimits\")\n                          else                                              {upperLimit:1000000, lowerLimit:0}\n  excludeDates:           if (opts.get(\"excludeDates\").isNonNull)           opts.get(\"excludeDates\")\n                          else                                              []\n  confInterval:           if (opts.get(\"confInterval\").isNonNull)           opts.get(\"confInterval\")\n                          else                                              95\n  includeTOW:             if (opts.get(\"includeTOW\").isNonNull)             opts.get(\"includeTOW\")\n                          else                                              true\n  imputationMethod:       if (opts.get(\"imputationMethod\").isNonNull)       opts.get(\"imputationMethod\")\n                          else                                              \"Linear Interpolation\"\n\n  // Retrieve/Normalize DateSpan\n  dates: if   (modelEngine==\"nmecpy\") ((pointRec.get(\"hisEnd\") - 1year)..(pointRec.get(\"hisEnd\"))).toSpan\n         else                         ((pointRec.get(\"hisEnd\") - 6mo)..(pointRec.get(\"hisEnd\"))).toSpan\n\n  // Generate dict to generate modelConfig\n  dict: if (modelEngine == \"nmecpy\") do\n          {\n           pointRef:        pointId,\n           siteRef:         siteRef,\n           modelEngine:     modelEngine,\n           modelParameters: {\n                             point:           pointId,\n                             temp:            tempPointId,\n                             occ:             occPointId,\n                             modelMethod:     modelMethod,\n                             additionalPoint: if (additionalPoints.isNonNull) additionalPoints.get(0) else null,\n                             trainDates:      ((dates.start-1wk)..(dates.end-1wk)).toSpan, // Shift train dates a week in the past.\n                             testDates:       ((dates.end-1wk)..(dates.end)).toSpan,        // Use the shift period as our test dates.\n                            },\n           preProcessConfig: {\n                               zScoreOutlierExclusion: zScoreOutlierExclusion,\n                               outlierLimits:          outlierLimits,\n                               excludeDates:           excludeDates\n                             }\n           }\n        else do\n          {\n           modelEngine:      modelEngine,\n           pointRef:         pointId,\n           siteRef:          siteRef,\n           modelParameters:  {\n                              point:              pointId,\n                              temp:               tempPointId,\n                              modelMethod:        modelMethod,\n                              span:               dates,\n                              confInterval:       confInterval,\n                              testTrainSplitDate: (dates.end-1wk),\n                              additionalPoint:    if   (additionalPoints.isNull) [] // Removed occ Point (occPointId)\n                                                  else                           [additionalPoints].flatten.findAll(v=>v.isNonNull),\n                             },\n           preProcessConfig: {\n                               includeTOW:       includeTOW,\n                               imputationMethod: imputationMethod\n                             }\n           }\n        end\n\n  // action_kwModeling_runModel returns a taskFuture, so we follow logic rather than call it itself.\n  modelConfigRec: try         logic_kwModeling_saveModelConfig_v2(dict, opts)\n                  catch (err) return {point: pointId, err: err}\n\n  updatedRec: diff(modelConfigRec, {\"modelRunState\": \"queued\"}, {update}).commit()\n\n  // NOTE: Change so after the callTask is done, we retrieve the future and check if done vs. done error\n  //try         callTask(logic_kwModeling_runModel, [updatedRec], getVal: true)\n  //catch (err) return {modelConfigRec: updatedRec, err: err}\n  // Edit 06/2025: Running the config is unnecessary. Just save config and activate point.\n\n  // Return the updated/created modelRec\n  return updatedRec\nend",
      "local_src": "/*\n*   Description :\n*    Given a point, it creates a model config using opts to override the default values.\n*\n*   Parameters :\n*     Type                   Name               Description\n*     Ref                    point              - Point Ref to generate the config\n*     Dict                   opts               - Additional Options\n*\n*   Output :\n*     Type                   Name            Description\n*     Dict                   config          - The newly created config\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    02/14/2025      - Initial Creation\n*/\n\n(point, opts:{}) => do\n\n  // Normalize Inputs\n  pointRec: point.toRec\n  pointId: pointRec.get(\"id\")\n  siteRef: pointRec.get(\"siteRef\")\n\n  // Modeling Opts Retrieval\n  modelEngine: opts.get(\"modelEngine\")\n  modelMethod: opts.get(\"modelMethod\")\n  tempPointId: read(point and temp and weatherStationRef == pointRec.get(\"siteRef\").toRec.get(\"weatherStationRef\"), false).toRec.get(\"id\")\n  logInfo(\"kwBulkModeling\", \"Temp Point selected: \" + tempPointId)\n  \n  occPointId:  if (opts.has(\"occ\")) opts.get(\"occ\").toRec.get(\"id\") \n               else try read(wifi and occ and point and sensor and calculatedPercent and siteRef == siteRef).toRec.get(\"id\") catch null\n  additionalPoints: if (opts.has(\"additionalPoints\") and opts.get(\"additionalPoints\").isList and not opts.get(\"additionalPoints\").isEmpty) opts.get(\"additionalPoints\").findAll(v=>v.isNonNull) else try readAll(indicatorVariable).colToList(\"id\") catch null\n\n  // PreProcess Configs Retrieval\n  zScoreOutlierExclusion: if (opts.get(\"zScoreOutlierExclusion\").isNonNull) opts.get(\"zScoreOutlierExclusion\")\n                          else                                              15\n  outlierLimits:          if (opts.get(\"outlierLimits\").isNonNull)          opts.get(\"outlierLimits\")\n                          else                                              {upperLimit:1000000, lowerLimit:0}\n  excludeDates:           if (opts.get(\"excludeDates\").isNonNull)           opts.get(\"excludeDates\")\n                          else                                              []\n  confInterval:           if (opts.get(\"confInterval\").isNonNull)           opts.get(\"confInterval\")\n                          else                                              95\n  includeTOW:             if (opts.get(\"includeTOW\").isNonNull)             opts.get(\"includeTOW\")\n                          else                                              true\n  imputationMethod:       if (opts.get(\"imputationMethod\").isNonNull)       opts.get(\"imputationMethod\")\n                          else                                              \"Linear Interpolation\"\n\n  // Retrieve/Normalize DateSpan\n  dates: if   (modelEngine==\"nmecpy\") ((pointRec.get(\"hisEnd\") - 1year)..(pointRec.get(\"hisEnd\"))).toSpan\n         else                         ((pointRec.get(\"hisEnd\") - 6mo)..(pointRec.get(\"hisEnd\"))).toSpan\n         \n  // Generate dict to generate modelConfig\n  dict: if (modelEngine == \"nmecpy\") do\n          {\n           pointRef:        pointId,\n           siteRef:         siteRef,\n           modelEngine:     modelEngine,\n           modelParameters: {\n                             point:           pointId,\n                             temp:            tempPointId,\n                             occ:             occPointId,\n                             modelMethod:     modelMethod,\n                             additionalPoint: if (additionalPoints.isNonNull) additionalPoints.get(0) else null,\n                             trainDates:      ((dates.start-1wk)..(dates.end-1wk)).toSpan, // Shift train dates a week in the past.\n                             testDates:       ((dates.end-1wk)..(dates.end)).toSpan,        // Use the shift period as our test dates.\n                            },\n           preProcessConfig: {\n                               zScoreOutlierExclusion: zScoreOutlierExclusion,\n                               outlierLimits:          outlierLimits,\n                               excludeDates:           excludeDates\n                             }\n           }\n        else do\n          {\n           modelEngine:      modelEngine,\n           pointRef:         pointId,\n           siteRef:          siteRef,\n           modelParameters:  {\n                              point:              pointId,\n                              temp:               tempPointId,\n                              modelMethod:        modelMethod,\n                              span:               dates,\n                              confInterval:       confInterval,\n                              testTrainSplitDate: (dates.end-1wk),\n                              additionalPoint:    if   (additionalPoints.isNull) [occPointId].flatten.findAll(v=>v.isNonNull) // Removed occ Point (occPointId)\n                                                  else                           [occPointId, additionalPoints].flatten.findAll(v=>v.isNonNull),\n                             },\n           preProcessConfig: {\n                               includeTOW:       includeTOW,\n                               imputationMethod: imputationMethod\n                             }\n           }\n        end\n\n  // action_kwModeling_runModel returns a taskFuture, so we follow logic rather than call it itself.\n  logInfo(\"kwBulkModeling\", \"Attempting to save ModelConfig\"+dict.toStr)\n\n  modelConfigRec: logic_kwModeling_saveModelConfig_v2(dict, opts) //try         \n                  //catch (err) return {point: pointId, err: err}\n\n  updatedRec: diff(modelConfigRec, {\"modelRunState\": \"queued\"}, {update}).commit()\n  logInfo(\"kwBulkModeling\", \"ModelConfig saved.\")\n  // Return the updated/created modelRec\n  return updatedRec\nend",
      "diff": "@@ -27,10 +27,11 @@   modelEngine: opts.get(\"modelEngine\")\n   modelMethod: opts.get(\"modelMethod\")\n   tempPointId: read(point and temp and weatherStationRef == pointRec.get(\"siteRef\").toRec.get(\"weatherStationRef\"), false).toRec.get(\"id\")\n+  logInfo(\"kwBulkModeling\", \"Temp Point selected: \" + tempPointId)\n \n   occPointId:  if (opts.has(\"occ\")) opts.get(\"occ\").toRec.get(\"id\")\n                else try read(wifi and occ and point and sensor and calculatedPercent and siteRef == siteRef).toRec.get(\"id\") catch null\n-  additionalPoints: if (opts.has(\"additionalPoints\") and opts.get(\"additionalPoints\").isList and not opts.get(\"additionalPoints\").isEmpty) opts.get(\"additionalPoints\") else try readAll(indicatorVariable).colToList(\"id\") catch null\n+  additionalPoints: if (opts.has(\"additionalPoints\") and opts.get(\"additionalPoints\").isList and not opts.get(\"additionalPoints\").isEmpty) opts.get(\"additionalPoints\").findAll(v=>v.isNonNull) else try readAll(indicatorVariable).colToList(\"id\") catch null\n \n   // PreProcess Configs Retrieval\n   zScoreOutlierExclusion: if (opts.get(\"zScoreOutlierExclusion\").isNonNull) opts.get(\"zScoreOutlierExclusion\")\n@@ -83,8 +84,8 @@                               span:               dates,\n                               confInterval:       confInterval,\n                               testTrainSplitDate: (dates.end-1wk),\n-                              additionalPoint:    if   (additionalPoints.isNull) [] // Removed occ Point (occPointId)\n-                                                  else                           [additionalPoints].flatten.findAll(v=>v.isNonNull),\n+                              additionalPoint:    if   (additionalPoints.isNull) [occPointId].flatten.findAll(v=>v.isNonNull) // Removed occ Point (occPointId)\n+                                                  else                           [occPointId, additionalPoints].flatten.findAll(v=>v.isNonNull),\n                              },\n            preProcessConfig: {\n                                includeTOW:       includeTOW,\n@@ -94,16 +95,13 @@         end\n \n   // action_kwModeling_runModel returns a taskFuture, so we follow logic rather than call it itself.\n-  modelConfigRec: try         logic_kwModeling_saveModelConfig_v2(dict, opts)\n-                  catch (err) return {point: pointId, err: err}\n+  logInfo(\"kwBulkModeling\", \"Attempting to save ModelConfig\"+dict.toStr)\n+\n+  modelConfigRec: logic_kwModeling_saveModelConfig_v2(dict, opts) //try\n+                  //catch (err) return {point: pointId, err: err}\n \n   updatedRec: diff(modelConfigRec, {\"modelRunState\": \"queued\"}, {update}).commit()\n-\n-  // NOTE: Change so after the callTask is done, we retrieve the future and check if done vs. done error\n-  //try         callTask(logic_kwModeling_runModel, [updatedRec], getVal: true)\n-  //catch (err) return {modelConfigRec: updatedRec, err: err}\n-  // Edit 06/2025: Running the config is unnecessary. Just save config and activate point.\n-\n+  logInfo(\"kwBulkModeling\", \"ModelConfig saved.\")\n   // Return the updated/created modelRec\n   return updatedRec\n end"
    },
    {
      "name": "logic_kwModeling_bulkGenerateModelPoints",
      "pod": "kwLinkModelExt",
      "pod_src": "/*\n*   Description :\n*    Given a list of point, it filters through them, and generates a model point for those points\n*    that have the requirements to do so.\n*\n*   Parameters :\n*     Type                   Name               Description\n*     Ref                    pointsList         - List of Point Refs to generate the model points\n*     Dict                   opts               - Additional Options\n*\n*   Output :\n*     Type                   Name            Description\n*     Str                    N/A             - Log that notifies through the log ext that the bulk modeling has been finalized\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    02/14/2025      - Initial Creation\n*     Thomas Kuhrke Limia    03/12/2025      - Finalized (Added proper multi-tasking)\n*     Apoorv Khanuja         07/02/2025      - Removed multi-tasking buckets. Activating one config at a time. Cleanup.\n*/\n(pointsList) => do\n  // Log the number of points selected\n  logInfo(\"kwBulkModeling\", pointsList.size + \" points selected for bulk modeling.\")\n\n  // Inform through logs that opts are being retrieved\n  logInfo(\"kwBulkModeling\", \"Attempting to retrieve opts...\")\n\n  // Retrieve bulk modeling opts\n  opts: read(bulkModelingOpts, false)\n\n  if (opts.isNull) do\n  // TODO: move the following code chunk to a separate logic func: logic_kwModeling_createDefaultBulkModelingOpts\n    logInfo(\"kwBulkModeling\", \"No Bulk Options found. Creating from scratch...\")\n    defaultOpts: {\n         // Modeling Opts\n         modelEngine: \"timegpt\", //nmec\n         modelMethod: \"TimeGPT\", //towt, 3pc, blah\n         //occ: read(kwStockSchedule and dis==\"Weekdays 6AM-7PM\", false).toRec.get(\"id\"),\n         spanOverride: null,\n         imputationMethod: \"Linear Interpolation\",\n         //modelActivationMode: \"Simulate\",\n\n         // Model Parameters/PreProcess Configs Opts\n         zScoreOutlierExclusion: 15,\n         outlierLimits: {upperLimit:1000000, lowerLimit:0},\n         excludeDates: [],\n         confInterval: 95,\n         includeTOW: true,\n\n         // Bulk Modeling Opts\n         pointPerTask: 1,\n         bulkMode: \"Single-Task\", //  Multi-Task\n         templateName: \"template_bulkModelingOptions\",\n         bulkModelingOpts: marker()\n    }\n\n    // Commit the opts into a rec\n    opts = diff(null, defaultOpts, {add}).commit()\n  end\n\n  // Inform that opts were retrieved successfully\n  logInfo(\"kwBulkModeling\", \"Bulk Options retrieved with id: \" + opts.get(\"id\"))\n\n  // Filter through grid to only contain valid points, and throw a message if none are valid.\n  validPoints: pointsList.map(r=>r.toRec).toGrid.findAll(r => logic_kwModeling_validatePointModeling(r.get(\"id\"), opts)).table\n  if (validPoints.isEmpty) throw \"None of the points provided are valid for modeling with the given settings.\"\n\n  logInfo(\"kwBulkModeling\", validPoints.size + \"/\" + pointsList.size + \" points valid for bulk modeling.\")\n\n  // Traverse the grid of valid points, and if they have a config, use that, otherwise create one   // ===========> TODO: use condition_modelConfigRecExists func instead. We want to be able to create multiple\n  // TODO: either add a better check that compares the modelParameters, or just create a new config every time\n  //modelConfigs: validPoints.map(r => if (read(modelConfig2 and pointRef==r.get(\"id\"), false).isNonNull) read(modelConfig2 and pointRef == r.get(\"id\"), false).toRec\n  //                                   else                                                               logic_kwModeling_autoCreateModelConfig(r.get(\"id\"), opts.merge({commitToRec: true})))\n  modelConfigs: validPoints.map(r => logic_kwModeling_autoCreateModelConfig(r.get(\"id\"), opts.merge({commitToRec: true})))\n\n  // modelConfigs grid waits until: 1. All configs are created and in a complete state\n  //                                2. Config had an error somewhere, which causes the row to have the err tag.\n\n  // Filter successful configs and non-successful configs\n  successfulConfigs: modelConfigs.findAll(r => r.isDict and r.missing(\"err\"))\n  failedConfigs:     modelConfigs.findAll(r => r.isDict and r.has(\"err\"))\n\n  // Report as a warning the configs that werent able to create a config.\n  //if (not failedConfigs.isEmpty) logWarn(\"kwBulkModeling\", failedConfigs.size + \" of valid points failed to create a config.\", failedConfigs)\n  // Commented out for now.\n  //Fails with: Func failed: logWarn(Obj tags,Str msg,Dict err); args: (Str,Str,GbGrid)\n  //  sys::ArgErr: java.lang.IllegalArgumentException: argument type mismatch\n\n//  // Point-Per-Task Opt\n//  pointPerTask: opts.get(\"pointPerTask\")\n//  bulkMode:     opts.get(\"bulkMode\")\n//\n//  // Reset pointPerTask to 1 if bulkMode is single-task\n//  if (pointPerTask < 1 or bulkMode==\"Single-Task\") pointPerTask = 1\n//\n//  // Split model configs into buckets for efficient/safe task processing\n//  buckets: []\n//  idx: 0\n//  successfulConfigs.each(r => do\n//    try do\n//      subGrid: buckets.get(idx)\n//      if   (subGrid.size < pointPerTask) buckets = buckets.set(idx, subGrid.add(r->id))\n//      else do\n//        idx = idx + 1\n//        throw \"Create new bucket\" // Dummy throw to jump into the catch\n//      end\n//    catch do\n//      buckets = buckets.add([r->id])\n//    end\n//  end)\n//\n//  // Log progress (buckets were created)\n//  logInfo(\"kwBulkModeling\", buckets.size + \" bucket(s) created. Initializing Bulk Modeling Activation\")\n//\n//  // Activate models using a taskRun/futureGet pair on a per-bucket basis (bucket size is dependant on pointPerTask Opt)\n//  buckets.each() (bucket) => do\n//    logInfo(\"kwBulkModeling\", \"Activating these models at the same time: \" + bucket.toStr)\n//    bucket.each() (modelConfigRef) => do\n//      if   (modelConfigRef != bucket.last) callTask(action_kwModeling_myModels_activateModel, [modelConfigRef], getVal:false)\n//      else                                 callTask(action_kwModeling_myModels_activateModel, [modelConfigRef], getVal:true)\n//    end\n//  end\n\n  // Activating one config at a time\n  successfulConfigs.each() r => do\n    modelConfigRef: r->id\n    logInfo(\"kwBulkModeling\", \"Activating the following model config: \" + modelConfigRef.toStr)\n    callTask(action_kwModeling_myModels_activateModel, [modelConfigRef], getVal:true)\n  end\n\n  // Log that the bulk modeling action has finished running\n  logInfo(\"kwBulkModeling\", \"Bulk Model Activation Finished.\")\nend",
      "local_src": "/*\n*   Description :\n*    Given a list of point, it filters through them, and generates a model point for those points\n*    that have the requirements to do so.\n*\n*   Parameters :\n*     Type                   Name               Description\n*     Ref                    pointsList         - List of Point Refs to generate the model points\n*     Dict                   opts               - Additional Options\n*\n*   Output :\n*     Type                   Name            Description\n*     Str                    N/A             - Log that notifies through the log ext that the bulk modeling has been finalized\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    02/14/2025      - Initial Creation\n*     Thomas Kuhrke Limia    03/12/2025      - Finalized (Added proper multi-tasking)\n*     Apoorv Khanuja         07/02/2025      - Removed multi-tasking buckets. Activating one config at a time. Cleanup.\n*/\n(pointsList) => do\n  // Log the number of points selected\n  logInfo(\"kwBulkModeling\", pointsList.size + \" points selected for bulk modeling.\")\n\n  // Inform through logs that opts are being retrieved\n  logInfo(\"kwBulkModeling\", \"Attempting to retrieve opts...\")\n\n  // Retrieve bulk modeling opts\n  opts: read(bulkModelingOpts, false)\n\n  if (opts.isNull) do\n  // TODO: move the following code chunk to a separate logic func: logic_kwModeling_createDefaultBulkModelingOpts\n    logInfo(\"kwBulkModeling\", \"No Bulk Options found. Creating from scratch...\")\n    defaultOpts: {\n         // Modeling Opts\n         modelEngine: \"timegpt\", //nmec\n         modelMethod: \"TimeGPT\", //towt, 3pc, blah\n         //occ: read(kwStockSchedule and dis==\"Weekdays 6AM-7PM\", false).toRec.get(\"id\"),\n         spanOverride: null,\n         imputationMethod: \"Linear Interpolation\",\n         //modelActivationMode: \"Simulate\",\n\n         // Model Parameters/PreProcess Configs Opts\n         zScoreOutlierExclusion: 15,\n         outlierLimits: {upperLimit:1000000, lowerLimit:0},\n         excludeDates: [],\n         confInterval: 95,\n         includeTOW: true,\n\n         // Bulk Modeling Opts\n         pointPerTask: 1,\n         bulkMode: \"Single-Task\", //  Multi-Task\n         templateName: \"template_bulkModelingOptions\",\n         bulkModelingOpts: marker()\n    }\n\n    // Commit the opts into a rec\n    opts = diff(null, defaultOpts, {add}).commit()\n  end\n\n  // Inform that opts were retrieved successfully\n  logInfo(\"kwBulkModeling\", \"Bulk Options retrieved with id: \" + opts.get(\"id\"))\n\n  // Filter through grid to only contain valid points, and throw a message if none are valid.\n  validPoints: pointsList.map(r=>r.toRec).toGrid.findAll(r => logic_kwModeling_validatePointModeling(r.get(\"id\"), opts)).table\n  if (validPoints.isEmpty) throw \"None of the points provided are valid for modeling with the given settings.\"\n\n  logInfo(\"kwBulkModeling\", validPoints.size + \"/\" + pointsList.size + \" points valid for bulk modeling.\")\n\n  // Traverse the grid of valid points, and if they have a config, use that, otherwise create one   // ===========> TODO: use condition_modelConfigRecExists func instead. We want to be able to create multiple\n  // TODO: either add a better check that compares the modelParameters, or just create a new config every time\n  //modelConfigs: validPoints.map(r => if (read(modelConfig2 and pointRef==r.get(\"id\"), false).isNonNull) read(modelConfig2 and pointRef == r.get(\"id\"), false).toRec\n  //                                   else                                                               logic_kwModeling_autoCreateModelConfig(r.get(\"id\"), opts.merge({commitToRec: true})))\n  modelConfigs: validPoints.map(r => logic_kwModeling_autoCreateModelConfig(r.get(\"id\"), opts.merge({commitToRec: true})))\n\n  // modelConfigs grid waits until: 1. All configs are created and in a complete state\n  //                                2. Config had an error somewhere, which causes the row to have the err tag.\n\n  // Filter successful configs and non-successful configs\n  successfulConfigs: modelConfigs.findAll(r => r.isDict and r.missing(\"err\"))\n  failedConfigs:     modelConfigs.findAll(r => r.isDict and r.has(\"err\"))\n\n\n  // Activating one config at a time\n  successfulConfigs.each() r => do\n    modelConfigRef: r->id\n    logInfo(\"kwBulkModeling\", \"Activating the following model config: \" + modelConfigRef.toStr)\n    callTask(action_kwModeling_myModels_activateModel, [modelConfigRef], getVal:true)\n  end  \n\n  // Log that the bulk modeling action has finished running\n  logInfo(\"kwBulkModeling\", \"Bulk Model Activation Finished.\")\nend",
      "diff": "@@ -80,46 +80,6 @@   successfulConfigs: modelConfigs.findAll(r => r.isDict and r.missing(\"err\"))\n   failedConfigs:     modelConfigs.findAll(r => r.isDict and r.has(\"err\"))\n \n-  // Report as a warning the configs that werent able to create a config.\n-  //if (not failedConfigs.isEmpty) logWarn(\"kwBulkModeling\", failedConfigs.size + \" of valid points failed to create a config.\", failedConfigs)\n-  // Commented out for now.\n-  //Fails with: Func failed: logWarn(Obj tags,Str msg,Dict err); args: (Str,Str,GbGrid)\n-  //  sys::ArgErr: java.lang.IllegalArgumentException: argument type mismatch\n-\n-//  // Point-Per-Task Opt\n-//  pointPerTask: opts.get(\"pointPerTask\")\n-//  bulkMode:     opts.get(\"bulkMode\")\n-//\n-//  // Reset pointPerTask to 1 if bulkMode is single-task\n-//  if (pointPerTask < 1 or bulkMode==\"Single-Task\") pointPerTask = 1\n-//\n-//  // Split model configs into buckets for efficient/safe task processing\n-//  buckets: []\n-//  idx: 0\n-//  successfulConfigs.each(r => do\n-//    try do\n-//      subGrid: buckets.get(idx)\n-//      if   (subGrid.size < pointPerTask) buckets = buckets.set(idx, subGrid.add(r->id))\n-//      else do\n-//        idx = idx + 1\n-//        throw \"Create new bucket\" // Dummy throw to jump into the catch\n-//      end\n-//    catch do\n-//      buckets = buckets.add([r->id])\n-//    end\n-//  end)\n-//\n-//  // Log progress (buckets were created)\n-//  logInfo(\"kwBulkModeling\", buckets.size + \" bucket(s) created. Initializing Bulk Modeling Activation\")\n-//\n-//  // Activate models using a taskRun/futureGet pair on a per-bucket basis (bucket size is dependant on pointPerTask Opt)\n-//  buckets.each() (bucket) => do\n-//    logInfo(\"kwBulkModeling\", \"Activating these models at the same time: \" + bucket.toStr)\n-//    bucket.each() (modelConfigRef) => do\n-//      if   (modelConfigRef != bucket.last) callTask(action_kwModeling_myModels_activateModel, [modelConfigRef], getVal:false)\n-//      else                                 callTask(action_kwModeling_myModels_activateModel, [modelConfigRef], getVal:true)\n-//    end\n-//  end\n \n   // Activating one config at a time\n   successfulConfigs.each() r => do\n"
    },
    {
      "name": "logic_kwModeling_createModelPointRec_v2",
      "pod": "kwLinkModelExt",
      "pod_src": "//\n//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   07 Oct 24   Apoorv Khanuja        - Creation.\n//   03 Mar 25   Apoorv Khanuja        - Created a new virtualBlueprint; included tags from synthetic ontology\n//\n// Description\n//   This logic func uses the modelConfig rec to create a modelPoint rec using a virtual blueprint and returns the modelPoint rec.\n//\n// Parameters\n//   Name                 Type(s)             DefVal\n//   modelConfigRec       rec/ref                 NA\n//\n// Returns\n//   Type           Description\n//   dict           - modelPoint rec.\n//\n\n// // Search for TODO to find leftover dev //\n\n(modelConfigRec) => do\n\n  // Normalize inputs\n  modelConfigRec = modelConfigRec.toRec\n\n  modelPointBlueprint : try read(virtualBlueprint and navName==\"GenAI Adaptive Baseline Model Point\") catch throw \"No modelPoint Blueprint found.\"\n\n  modelActivationConfig: if      (modelConfigRec.get(\"modelEngine\")==\"timegpt\") {trainLength: 6mo, forecastLength: 1week}\n                         else if (modelConfigRec.get(\"modelEngine\")==\"nmecpy\") {trainLength: 12mo, forecastLength: 1week}\n\n  meterHisStart : modelConfigRec.get(\"pointRef\").toRec.get(\"hisStart\")\n  meterHisEnd   : modelConfigRec.get(\"pointRef\").toRec.get(\"hisEnd\")\n  trainStartDateOverride : if (meterHisStart < meterHisEnd - 12mo) meterHisEnd - 12mo else meterHisStart\n\n  virtualPointTags: modelPointBlueprint.get(\"virtualPointTags\").listTagsToDict\n\n  modelType : try modelConfigRec.get(\"modelType\") catch null\n  mlIdentificationPeriod : try if (modelType == \"fixedBaseline\") modelConfigRec.get(\"mlIdentificationPeriod\") else null catch null\n\n  modelPointCreationInputs: {point                  : marker(),\n                             his                    : marker(),\n                             modelConfigRef         : modelConfigRec.get(\"id\"),\n                             pointRef               : modelConfigRec.get(\"pointRef\"),\n                             equipRef               : try modelConfigRec.get(\"pointRef\")->equipRef catch null,\n                             unit                   : try modelConfigRec.get(\"pointRef\")->unit     catch null,\n                             siteRef                : modelConfigRec.get(\"siteRef\"),\n                             trainStartDateOverride : trainStartDateOverride,\n                             tz                     : modelConfigRec.get(\"siteRef\").toRec.get(\"tz\"),\n                             modelType              : modelType,\n                             mlIdentificationPeriod : mlIdentificationPeriod,\n                             virtualPointOpts       : modelActivationConfig,\n                             virtualSyncFrequency   : 1week,                          // TODO: This should update based on modelActivationMode (futureOnly, histOnly and fullRange) or modelType (adaptiveBaseline, fixedBaseline)\n                             virtualPointCreator    : userCur().dis,\n                             virtualBlueprintRef    : modelPointBlueprint.get(\"id\"),\n                             disMacro               :\"\\$siteRef \\$pointRef \\$navName\",\n                             -virtualBlueprint,\n                             -virtualPointTags,\n                             -virtualPointOn,\n                             -virtualPointCategory}\n\n  modelPointRec: modelPointBlueprint.remove(\"id\").remove(\"mod\").merge(modelPointCreationInputs).merge(virtualPointTags)\n\n  modelPointRec = diff(null, modelPointRec, {add}).commit\n\n  // Update modelConfig to include modelPoint ref\n  modelPointRef: modelPointRec.get(\"id\")\n  modelConfigRec = diff(modelConfigRec, {\"modelPointRef\": modelPointRef}, {update}).commit\n\n  return modelPointRec\n\nend",
      "local_src": "//\n//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   07 Oct 24   Apoorv Khanuja        - Creation.\n//   03 Mar 25   Apoorv Khanuja        - Created a new virtualBlueprint; included tags from synthetic ontology\n//\n// Description\n//   This logic func uses the modelConfig rec to create a modelPoint rec using a virtual blueprint and returns the modelPoint rec.\n//\n// Parameters\n//   Name                 Type(s)             DefVal\n//   modelConfigRec       rec/ref                 NA\n//\n// Returns\n//   Type           Description\n//   dict           - modelPoint rec.\n//\n\n// // Search for TODO to find leftover dev //\n\n(modelConfigRec) => do\n\n  // Normalize inputs\n  modelConfigRec = modelConfigRec.toRec\n\n  modelPointBlueprint : try read(virtualBlueprint and navName==\"GenAI Adaptive Baseline Model Point\") catch throw \"No modelPoint Blueprint found.\"\n\n  modelActivationConfig: if      (modelConfigRec.get(\"modelEngine\")==\"timegpt\") {trainLength: 12mo, forecastLength: 1week}\n                         else if (modelConfigRec.get(\"modelEngine\")==\"nmecpy\") {trainLength: 12mo, forecastLength: 1week}\n\n  virtualPointTags: modelPointBlueprint.get(\"virtualPointTags\").listTagsToDict\n  \n  modelType : try modelConfigRec.get(\"modelType\") catch null\n  mlIdentificationPeriod : try if (modelType == \"fixedBaseline\") modelConfigRec.get(\"mlIdentificationPeriod\") else null catch null\n\n  modelPointCreationInputs: {point                  : marker(),\n                             his                    : marker(),\n                             modelConfigRef         : modelConfigRec.get(\"id\"),\n                             pointRef               : modelConfigRec.get(\"pointRef\"),\n                             equipRef               : try modelConfigRec.get(\"pointRef\")->equipRef catch null,\n                             unit                   : try modelConfigRec.get(\"pointRef\")->unit     catch null,\n                             siteRef                : modelConfigRec.get(\"siteRef\"),\n                             tz                     : modelConfigRec.get(\"siteRef\").toRec.get(\"tz\"),\n                             modelType              : modelType,\n                             mlIdentificationPeriod : mlIdentificationPeriod,\n                             virtualPointOpts       : modelActivationConfig,\n                             virtualSyncFrequency   : 1week,                          // TODO: This should update based on modelActivationMode (futureOnly, histOnly and fullRange) or modelType (adaptiveBaseline, fixedBaseline)\n                             virtualPointCreator    : userCur().dis,\n                             virtualBlueprintRef    : modelPointBlueprint.get(\"id\"),\n                             disMacro               :\"\\$siteRef \\$pointRef \\$navName\",\n                             -virtualBlueprint,\n                             -virtualPointTags,\n                             -virtualPointOn,\n                             -virtualPointCategory}\n\n  modelPointRec: modelPointBlueprint.remove(\"id\").remove(\"mod\").merge(modelPointCreationInputs).merge(virtualPointTags)\n\n  modelPointRec = diff(null, modelPointRec, {add}).commit\n\n  // Update modelConfig to include modelPoint ref\n  modelPointRef: modelPointRec.get(\"id\")\n  modelConfigRec = diff(modelConfigRec, {\"modelPointRef\": modelPointRef}, {update}).commit\n\n  return modelPointRec\n\nend",
      "diff": "@@ -29,12 +29,8 @@ \n   modelPointBlueprint : try read(virtualBlueprint and navName==\"GenAI Adaptive Baseline Model Point\") catch throw \"No modelPoint Blueprint found.\"\n \n-  modelActivationConfig: if      (modelConfigRec.get(\"modelEngine\")==\"timegpt\") {trainLength: 6mo, forecastLength: 1week}\n+  modelActivationConfig: if      (modelConfigRec.get(\"modelEngine\")==\"timegpt\") {trainLength: 12mo, forecastLength: 1week}\n                          else if (modelConfigRec.get(\"modelEngine\")==\"nmecpy\") {trainLength: 12mo, forecastLength: 1week}\n-\n-  meterHisStart : modelConfigRec.get(\"pointRef\").toRec.get(\"hisStart\")\n-  meterHisEnd   : modelConfigRec.get(\"pointRef\").toRec.get(\"hisEnd\")\n-  trainStartDateOverride : if (meterHisStart < meterHisEnd - 12mo) meterHisEnd - 12mo else meterHisStart\n \n   virtualPointTags: modelPointBlueprint.get(\"virtualPointTags\").listTagsToDict\n \n@@ -48,7 +44,6 @@                              equipRef               : try modelConfigRec.get(\"pointRef\")->equipRef catch null,\n                              unit                   : try modelConfigRec.get(\"pointRef\")->unit     catch null,\n                              siteRef                : modelConfigRec.get(\"siteRef\"),\n-                             trainStartDateOverride : trainStartDateOverride,\n                              tz                     : modelConfigRec.get(\"siteRef\").toRec.get(\"tz\"),\n                              modelType              : modelType,\n                              mlIdentificationPeriod : mlIdentificationPeriod,\n"
    },
    {
      "name": "logic_kwModeling_modelParameterWeightsPlot",
      "pod": "kwLinkModelExt",
      "pod_src": "//\n//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   30 Sep 24   Apoorv Khanuja        - Creation.\n//\n// Description\n//   This logic func takes the modelCache rec and returns the chart for the model parameter weights plot.\n//\n// Parameters\n//   Name                    Type(s)             DefVal\n//   modelCacheRec           dict                NA\n//\n// Returns\n//   Type                      Description\n//   grid/blankChart           - model parameter weights or model parameter information depending on the model\n//\n\n(modelCacheRec) => do\n\n  // Extract modelCoeffs\n  modelCoeffs: modelCacheRec.get(\"modelCoeffs\")\n\n////////////////////// If modelCoeffs is a number (In case of nmecpy>towt model)\n\n  if (modelCoeffs.debugType==\"haystack::Number\") do\n    outString : \"Total number of parameters for the selected ToWT model is \" + modelCoeffs.toStr\n    out       : blankChart(outString)\n  end\n\n\n////////////////////// If modelCoeffs is a dict (In case of other nmecpy models)\n\n  else if (modelCoeffs.debugType.contains(\"Dict\")) do\n    outString : \"Model Parameters for the selected model are: \" + modelCoeffs.toStr\n    out       : blankChart(outString)\n  end\n\n\n////////////////////// If modelCoeffs is a hisGrid (In case of timegpt model)\n\n\n//////// v1: regular grid with average model weights\n\n  else if (modelCoeffs.debugType==\"haystack::GbGrid\") do\n    outString: \"SHAP Value implemenation in progress.\"\n    out: blankChart(outString)/*modelCoeffs.stream\n                    .addMeta({title: \"Average weights of exogenous variable used for predictions\", chartType:\"bar\", chartLegend:\"hide\"})\n                    .addColMeta(\"timeOfWeek\", {dis: \"Time of week\"})\n                    .collect\n                    .chart*/\n  end\n\n\n//////// v2: hisGrid. Convert to regular grid with average model weights (absolute value -> sum)\n\n\n\n\n  else out: blankChart(\"Unsupported\")\n\n  return out\nend",
      "local_src": "//\n//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   30 Sep 24   Apoorv Khanuja        - Creation.\n//\n// Description\n//   This logic func takes the modelCache rec and returns the chart for the model parameter weights plot.\n//\n// Parameters\n//   Name                    Type(s)             DefVal\n//   modelCacheRec           dict                NA\n//\n// Returns\n//   Type                      Description\n//   grid/blankChart           - model parameter weights or model parameter information depending on the model\n//\n\n(modelCacheRec) => do\n\n  // Extract modelCoeffs\n  modelCoeffs: modelCacheRec.get(\"modelCoeffs\")\n  //return modelCoeffs.colNames\n  // Extract other colMeta information for better column names\n  modelConfigRec : modelCacheRec.get(\"modelConfigRef\").toRec\n  modelParameters: modelConfigRec.get(\"modelParameters\")\n  exoVarPoints: [modelParameters.get(\"temp\"), modelParameters.get(\"additionalPoint\")].flatten.findAll(v=>v.isNonNull)\n  \n  exoVarMeta: {}.toGrid\n  \n  exoVarPoints.each() (r,idx) => do\n    exoVarNum: idx + 1\n    exoVarLabel: \"v\"+exoVarNum.toStr\n    \n    curVarMeta: {exoVarLabel:exoVarLabel, curVarDis: r.toRec.get(\"navName\")}\n    exoVarMeta = exoVarMeta.addRow(curVarMeta)\n  end\n//return exoVarMeta.findAll(v => v.get(\"exoVarLabel\")==\"v1\").toRec.get(\"curVarDis\") //blankChart(exoVarMeta.size.toStr)\n\n  \n\n////////////////////// If modelCoeffs is a number (In case of nmecpy>towt model)\n\n  if (modelCoeffs.debugType==\"haystack::Number\") do\n    outString : \"Total number of parameters for the selected ToWT model is \" + modelCoeffs.toStr\n    out       : blankChart(outString)\n  end\n\n\n////////////////////// If modelCoeffs is a dict (In case of other nmecpy models)\n\n  else if (modelCoeffs.debugType.contains(\"Dict\")) do\n    outString : \"Model Parameters for the selected model are: \" + modelCoeffs.toStr\n    out       : blankChart(outString)\n  end\n\n\n////////////////////// If modelCoeffs is a hisGrid (In case of timegpt model)\n\n\n//////// v1: regular grid with average model weights\n\n  else if (modelCoeffs.debugType==\"haystack::GbGrid\") do\n    return {info: \"SHAP Value implemenation in progress.\"}.toGrid.addColMeta(\"info\", {dis:\"Information\"})\n    // Shows up as \"No Data\"\n    // akTODO: Finish writing this chunk with the new SHAP values grid\n    modelCoeffs = modelCoeffs.removeCols([\"unique_id\", \"ts\", \"timegpt\", \"base_value\"]).map() r => do\n      if (r.has(\"v1\")) r = r.set(\"v1\", r.get(\"v1\").abs)\n      if (r.has(\"v2\")) r = r.set(\"v2\", r.get(\"v2\").abs)\n      if (r.has(\"v3\")) r = r.set(\"v3\", r.get(\"v3\").abs)\n      if (r.has(\"v4\")) r = r.set(\"v4\", r.get(\"v4\").abs)\n      if (r.has(\"v5\")) r = r.set(\"v5\", r.get(\"v5\").abs) // Works for up to 5 exo variables\n      \n    end\n    //\n    return modelCoeffs//.colNames\n    out: modelCoeffs.stream\n                    .addMeta({title: \"Average weights of exogenous variable used for predictions\", chartType:\"bar\", chartLegend:\"hide\"})\n                    .addColMeta(\"timeOfWeek\", {dis: \"Time of week\"})\n                    .collect\n                    .chart\n  end\n\n\n//////// v2: hisGrid. Convert to regular grid with average model weights (absolute value -> sum)\n\n\n\n\n  else out: blankChart(\"Unsupported\")\n\n  return out\nend",
      "diff": "@@ -23,6 +23,24 @@ \n   // Extract modelCoeffs\n   modelCoeffs: modelCacheRec.get(\"modelCoeffs\")\n+  //return modelCoeffs.colNames\n+  // Extract other colMeta information for better column names\n+  modelConfigRec : modelCacheRec.get(\"modelConfigRef\").toRec\n+  modelParameters: modelConfigRec.get(\"modelParameters\")\n+  exoVarPoints: [modelParameters.get(\"temp\"), modelParameters.get(\"additionalPoint\")].flatten.findAll(v=>v.isNonNull)\n+\n+  exoVarMeta: {}.toGrid\n+\n+  exoVarPoints.each() (r,idx) => do\n+    exoVarNum: idx + 1\n+    exoVarLabel: \"v\"+exoVarNum.toStr\n+\n+    curVarMeta: {exoVarLabel:exoVarLabel, curVarDis: r.toRec.get(\"navName\")}\n+    exoVarMeta = exoVarMeta.addRow(curVarMeta)\n+  end\n+//return exoVarMeta.findAll(v => v.get(\"exoVarLabel\")==\"v1\").toRec.get(\"curVarDis\") //blankChart(exoVarMeta.size.toStr)\n+\n+\n \n ////////////////////// If modelCoeffs is a number (In case of nmecpy>towt model)\n \n@@ -46,12 +64,24 @@ //////// v1: regular grid with average model weights\n \n   else if (modelCoeffs.debugType==\"haystack::GbGrid\") do\n-    outString: \"SHAP Value implemenation in progress.\"\n-    out: blankChart(outString)/*modelCoeffs.stream\n+    return {info: \"SHAP Value implemenation in progress.\"}.toGrid.addColMeta(\"info\", {dis:\"Information\"})\n+    // Shows up as \"No Data\"\n+    // akTODO: Finish writing this chunk with the new SHAP values grid\n+    modelCoeffs = modelCoeffs.removeCols([\"unique_id\", \"ts\", \"timegpt\", \"base_value\"]).map() r => do\n+      if (r.has(\"v1\")) r = r.set(\"v1\", r.get(\"v1\").abs)\n+      if (r.has(\"v2\")) r = r.set(\"v2\", r.get(\"v2\").abs)\n+      if (r.has(\"v3\")) r = r.set(\"v3\", r.get(\"v3\").abs)\n+      if (r.has(\"v4\")) r = r.set(\"v4\", r.get(\"v4\").abs)\n+      if (r.has(\"v5\")) r = r.set(\"v5\", r.get(\"v5\").abs) // Works for up to 5 exo variables\n+\n+    end\n+    //\n+    return modelCoeffs//.colNames\n+    out: modelCoeffs.stream\n                     .addMeta({title: \"Average weights of exogenous variable used for predictions\", chartType:\"bar\", chartLegend:\"hide\"})\n                     .addColMeta(\"timeOfWeek\", {dis: \"Time of week\"})\n                     .collect\n-                    .chart*/\n+                    .chart\n   end\n \n \n"
    },
    {
      "name": "logic_kwModeling_validatePointModeling",
      "pod": "kwLinkModelExt",
      "pod_src": "/*\n*   Description :\n*    Given a point, it determines whether or not is valid for point modeling through (a lot of) checks.\n*\n*   Parameters :\n*     Type                   Name               Description\n*     hisPoint               point              - Historized Point.\n*     Dict                   opts               - Additional Options\n*\n*   Output :\n*     Type                   Name            Description\n*     Bool                   out             - True if we can model with that point, false otherwise\n*     GbGrid                 out             - If the opt \"reportWarnings\" is included in the call, returns\n*                                              the warnings and errors that prevent modeling through that point.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    02/12/2025      - Initial Creation\n*/\n// TODOs: Ctrl + F for \"NOTE\" for TODO items\n(point, opts:{}) => do\n\n  // NOTE: If date override is passed, use that for data (Weather Ref)\n\n  // Normalize Inputs\n  pointRec: point.toRec\n  pointId: pointRec.get(\"id\")\n  siteRec: pointRec.get(\"siteRef\").toRec\n  siteId: siteRec.get(\"id\")\n  modelEngine: opts.get(\"modelEngine\")\n  modelMethod: opts.get(\"modelMethod\")\n\n  // Create a list that contains the warnings, and one for errors\n  warnings: []\n  errors:   []\n//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n// START OF CHECKING LOGIC\n\n  // Traverse warning logic\n  // || Missing hisSize or hisSize==0 ||\n  // NOTE: Missing Checks for Complete data, intervals between temp point and main point, timezone checks, etc.\n  if (pointRec.missing(\"hisSize\"))                                                             errors = errors.add(\"Missing hisSize Tag.\")\n  if (pointRec.has(\"hisSize\") and pointRec.get(\"hisSize\") <= 0)                                errors = errors.add(\"No history data found (hisSize is Zero).\")\n\n  // || Missing hisEnd or hisEnd too Old ||\n  if (pointRec.missing(\"hisEnd\"))                                                              errors   = errors.add(\"Missing hisEnd Tag.\")\n  if (pointRec.has(\"hisSize\") and pointRec.get(\"hisEnd\") < now()-2year)                        warnings = warnings.add(\"Point's hisEnd is older than 2 years from today: \" + pointRec.get(\"hisEnd\") + \".\")\n\n  // || Multiple Model Config ||\n  // NOTE: Change for pointQuery. Include activationMode check (since we might want \"Simulate\" vs. \"Future Baseline\")\n  // TODO: update with better logic. Since we can have multiple modelConfigs (especially fixedBaseline type) for the same pointRef. Removed for now.\n  //configs: readAll(modelConfig2 and pointRef == pointId)\n  //if (configs.size > 1)                                                                        errors   = errors.add(\"Multiple Configs for this point exist.\")\n\n  // || Weather Ref ||\n  if (siteRec.missing(\"weatherStationRef\"))                                                    errors = errors.add(\"Site Missing weatherStationRef\")\n  else do\n    weatherStationRef: siteRec.get(\"weatherStationRef\")\n    weatherStationRec: read(point and temp and weatherStationRef==weatherStationRef, false)\n    if (weatherStationRec.isNull)                                                              errors = errors.add(\"Weather Station has no historized point.\")\n    else do\n      wsHisEnd: weatherStationRec.get(\"hisEnd\")\n      wsHisStart: weatherStationRec.get(\"hisStart\")\n      wsHisSize: weatherStationRec.get(\"hisSize\")\n      if (wsHisSize < 1)                                                                       errors = errors.add(\"Weather Station has no historized data.\")\n      if (wsHisEnd < now()-2year)                                                              warnings = warnings.add(\"Weather Station's hisEnd is older than 2 years from today: \" + pointRec.get(\"hisEnd\") + \".\")\n      // NOTE: Make this logic better?\n      if (pointRec.has(\"hisEnd\") and\n         ((pointRec.get(\"hisEnd\") - 1year) < wsHisStart) or\n          pointRec.get(\"hisEnd\") > wsHisEnd)                                                   errors = errors.add(\"Overlap data between Point and Temp is not complete\")\n     end\n  end\n\n  // NOTE: Change to pointQuery\n  //if (readAll(kwStockSchedule).isEmpty)                                                        errors = errors.add(\"No Occupancy Schedule (kwStockSchedule) in this Project.\")\n  // Commented out. Don't really need Stock schedule for EA.\n\n  // Miscellaneous Checks from current modeling app\n  if (modelEngine == \"nmecpy\" and (modelMethod != \"TOWT\" and modelMethod != \"HD-CD\"))          errors = errors.add(\"Additional Points unsupported by \" + modelMethod + \". Select a valid model method for additional points, or remove the additional point from this model method. Supported Model Methods for Additional Points: [\\\"TOWT\\\", \\\"HD-CD\\\"]\")\n\n\n//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n  // Return the warnings list if the opt is present\n  if (opts.isDict and opts.has(\"reportWarnings\")) return {warnings: warnings, errors: errors}\n\n  // Return true if the point is good to model, false otherwise\n  return errors.isEmpty()\nend",
      "local_src": "/*\n*   Description :\n*    Given a point, it determines whether or not is valid for point modeling through (a lot of) checks.\n*\n*   Parameters :\n*     Type                   Name               Description\n*     hisPoint               point              - Historized Point.\n*     Dict                   opts               - Additional Options\n*\n*   Output :\n*     Type                   Name            Description\n*     Bool                   out             - True if we can model with that point, false otherwise\n*     GbGrid                 out             - If the opt \"reportWarnings\" is included in the call, returns\n*                                              the warnings and errors that prevent modeling through that point.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    02/12/2025      - Initial Creation\n*/\n// TODOs: Ctrl + F for \"NOTE\" for TODO items\n(point, opts:{}) => do\n\n  // NOTE: If date override is passed, use that for data (Weather Ref)\n\n  // Normalize Inputs\n  pointRec: point.toRec\n  pointId: pointRec.get(\"id\")\n  siteRec: pointRec.get(\"siteRef\").toRec\n  siteId: siteRec.get(\"id\")\n  modelEngine: opts.get(\"modelEngine\")\n  modelMethod: opts.get(\"modelMethod\")\n\n  // Create a list that contains the warnings, and one for errors\n  warnings: []\n  errors:   []\n//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n// START OF CHECKING LOGIC\n\n  // Traverse warning logic\n  // || Missing hisSize or hisSize==0 ||\n  // NOTE: Missing Checks for Complete data, intervals between temp point and main point, timezone checks, etc.\n  if (pointRec.missing(\"hisSize\"))                                                             errors = errors.add(\"Missing hisSize Tag.\")\n  if (pointRec.has(\"hisSize\") and pointRec.get(\"hisSize\") <= 0)                                errors = errors.add(\"No history data found (hisSize is Zero).\")\n\n  // || Missing hisEnd or hisEnd too Old ||\n  if (pointRec.missing(\"hisEnd\"))                                                              errors   = errors.add(\"Missing hisEnd Tag.\")\n  if (pointRec.has(\"hisSize\") and pointRec.get(\"hisEnd\") < now()-2year)                        warnings = warnings.add(\"Point's hisEnd is older than 2 years from today: \" + pointRec.get(\"hisEnd\") + \".\")\n\n  // || Multiple Model Config ||\n  // NOTE: Change for pointQuery. Include activationMode check (since we might want \"Simulate\" vs. \"Future Baseline\")\n  // TODO: update with better logic. Since we can have multiple modelConfigs (especially fixedBaseline type) for the same pointRef. Removed for now.\n  //configs: readAll(modelConfig2 and pointRef == pointId)\n  //if (configs.size > 1)                                                                        errors   = errors.add(\"Multiple Configs for this point exist.\")\n\n  // || Weather Ref ||\n  if (siteRec.missing(\"weatherStationRef\"))                                                    errors = errors.add(\"Site Missing weatherStationRef\")\n  else do\n    weatherStationRef: siteRec.get(\"weatherStationRef\")\n    weatherStationRec: read(point and temp and weatherStationRef==weatherStationRef, false)\n    if (weatherStationRec.isNull)                                                              errors = errors.add(\"Weather Station has no historized point.\")\n    else do\n      wsHisEnd: weatherStationRec.get(\"hisEnd\")\n      wsHisStart: weatherStationRec.get(\"hisStart\")\n      wsHisSize: weatherStationRec.get(\"hisSize\")\n      if (wsHisSize < 1)                                                                       errors = errors.add(\"Weather Station has no historized data.\")\n      if (wsHisEnd < now()-2year)                                                              warnings = warnings.add(\"Weather Station's hisEnd is older than 2 years from today: \" + pointRec.get(\"hisEnd\") + \".\")\n      // NOTE: Make this logic better?\n      if (pointRec.has(\"hisEnd\") and ((pointRec.get(\"hisEnd\") - 1year) < wsHisStart) or pointRec.get(\"hisEnd\") > wsHisEnd) errors = errors.add(\"Overlap data between Point and Temp is not complete\")\n     end\n  end\n\n  // NOTE: Change to pointQuery\n  if (readAll(kwStockSchedule).isEmpty)                                                        errors = errors.add(\"No Occupancy Schedule (kwStockSchedule) in this Project.\")\n\n  // Miscellaneous Checks from current modeling app\n  if (modelEngine == \"nmecpy\" and (modelMethod != \"TOWT\" and modelMethod != \"HD-CD\"))          errors = errors.add(\"Additional Points unsupported by \" + modelMethod + \". Select a valid model method for additional points, or remove the additional point from this model method. Supported Model Methods for Additional Points: [\\\"TOWT\\\", \\\"HD-CD\\\"]\")\n\n\n//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n  // Return the warnings list if the opt is present\n  if (opts.isDict and opts.has(\"reportWarnings\")) return {warnings: warnings, errors: errors}\n\n  // Return true if the point is good to model, false otherwise\n  return errors.isEmpty()\nend",
      "diff": "@@ -65,15 +65,12 @@       if (wsHisSize < 1)                                                                       errors = errors.add(\"Weather Station has no historized data.\")\n       if (wsHisEnd < now()-2year)                                                              warnings = warnings.add(\"Weather Station's hisEnd is older than 2 years from today: \" + pointRec.get(\"hisEnd\") + \".\")\n       // NOTE: Make this logic better?\n-      if (pointRec.has(\"hisEnd\") and\n-         ((pointRec.get(\"hisEnd\") - 1year) < wsHisStart) or\n-          pointRec.get(\"hisEnd\") > wsHisEnd)                                                   errors = errors.add(\"Overlap data between Point and Temp is not complete\")\n+      if (pointRec.has(\"hisEnd\") and ((pointRec.get(\"hisEnd\") - 1year) < wsHisStart) or pointRec.get(\"hisEnd\") > wsHisEnd) errors = errors.add(\"Overlap data between Point and Temp is not complete\")\n      end\n   end\n \n   // NOTE: Change to pointQuery\n-  //if (readAll(kwStockSchedule).isEmpty)                                                        errors = errors.add(\"No Occupancy Schedule (kwStockSchedule) in this Project.\")\n-  // Commented out. Don't really need Stock schedule for EA.\n+  if (readAll(kwStockSchedule).isEmpty)                                                        errors = errors.add(\"No Occupancy Schedule (kwStockSchedule) in this Project.\")\n \n   // Miscellaneous Checks from current modeling app\n   if (modelEngine == \"nmecpy\" and (modelMethod != \"TOWT\" and modelMethod != \"HD-CD\"))          errors = errors.add(\"Additional Points unsupported by \" + modelMethod + \". Select a valid model method for additional points, or remove the additional point from this model method. Supported Model Methods for Additional Points: [\\\"TOWT\\\", \\\"HD-CD\\\"]\")\n"
    },
    {
      "name": "pointQueryLookup",
      "pod": "kwLinkCoreExt",
      "pod_src": "(target: null, opts: {}) => do\n  cipherText :\n    \"AxD:J7ik5GzASro65uWVIxMkAg::7cPS7lsTZSH5N2vG2V_-JnF7WnnW0YzEzAkoWuWN0t33cekbuST1\" +\n    \"jdwUtsnrnsWKnY2ZJ11FgjHRBw-4DchwEYWtNDzsPhtoG9qIeWzimhGiuRu1zA5WRrk0313ancLuQXcH\" +\n    \"zUsoSr9-_gDRNSvmhd8jJ8XaV0eozzR7scJJx801Sxs1lYKqn9-efJgMoGE5yUdio-ucRJMKwXCuww-J\" +\n    \"b8TqM0LRciBpX_qgy9sWW83-toyx1g1E6NXoPfuyE4EB_DXdpr3CmMSWscawuIAE3KH2FmPCMCU8ihr3\" +\n    \"tr_mcJm1MYsEo96AokjwJZusgH38mbnKrRoCf8NHPHr0pfCbgMSC-bF-D68vIgcs7pkROhkb8dHHy0Yb\" +\n    \"BGZHrQwecGLFxh3jego2SM4MSQxMksgcXyMChH17bY10l6VEqvIczPolCB66sNO2bVrCc5FWAFPGXxtr\" +\n    \"m_T7E__lMU78LEPWLfO1UMWE_FLJtlEAQbuv_xhB4jUy4kyqpWCjuwNLkCrYY7sWHzyE90-Ys6SJzJLF\" +\n    \"INz9kBTIPWsbbjDHx0satQxguftZqyzoAjtoNJvymRdqyVTMTH2kLe1zoF_fI7afvDstxdp_YJ1Snx8y\" +\n    \"b3DLsnyLYJmKX7M8z4og1VWfRq6MdK-OCyjnRkLCO8Yv2ieCKWXBBbt7b5H7sVq8aZe-QHMINJW9XJQa\" +\n    \"AnOn9ePqiMWZoCfUbSB_dpwe89a9xMwOB_Ndj9gW8AoXEiK2uUgBEN_p4zcjOmNOJemXpluU-yQDFqJ3\" +\n    \"bZKgktkwNLUwrhYAYsp8Rg12CkGeXqx5hEEhEeXaUZisO7OKUHC9iFpi0CXbKbIB7bQc5kmWXCQNOKpk\" +\n    \"WJX_EPBrGrlmKXXseMsPuT78bkNoPxGbYxGwE4cSIfDqE0wkZOpILvuYXWkc5_jIdwuNsHSNLMXtEG2W\" +\n    \"NZ2dCBXpdFS7pFE5KrdkkydqepBBHgcCia5uNWcm1Sm9MuqqhjSK3R2r8D8VxPAJNIP-B05znD5pcUt8\" +\n    \"H-XKlS1RIkPpOpT3_0o_ieo7Dk2A51Ta4aSMsV1dkCGA78aq6mSF6oBYWefmW2FGYsmJjl0_KhJ30cuM\" +\n    \"-VLV1Tm58wPWB4gk-VTLLCCYqcu2eiH2XOMA5h2A0Jqm8nAp0-VH8rIvsgcdXV7OyBp7RThiFhmXrczT\" +\n    \"9UjSq1fDGg5ss22R-nyAlL6Cy9lFNgAwJXhhZGyoeEmKAnTju_Eszm8fkclrd66V2OG8Oumqyo_uLyAT\" +\n    \"WaOvw-a_AV8IniBRcJsnpdMFFuoGLKQ_IeAojF82KpVG0sB7Q9SR5iOJOCgu1zOfjmbJJuRiKt_5lvlX\" +\n    \"vjjxxHEwjXF_f-PFNDclHEwLpjwPm0u3Vln7BaLW0nbWkRsC8_GugIUsI0JIOZXfv7gCowIcqWPqFl2k\" +\n    \"AQTXjulVkhATkjviOkhCRzRpJgBt_4vjZzIPZKnJZqEq1aIDDCk8TxIBlABWT2Secx_B7z6vXxM6sKJO\" +\n    \"nThHzuHjlsJYDXrMEq1NVYtIz6HOv86FEksDz8zR7cCtSfQE2z-PMEeIBL0g5g4k5lDduU0ltFQDamy8\" +\n    \"jGokPxsAKgmYeqtWlvQDFircTDaUEURdVC0t0aRmXAOHIlhqYslE5vMmXfaZyB8tY8XC-QO4RwKRNOsA\" +\n    \"Ii_lUujsAX-uRZib_Qj79SArzhQVKHalYEglFumjDyOfjftf0dAqW7Lm3RzLBPnSAP5drZMTfCqr_keo\" +\n    \"qCRPNnnsdgVhHpauC4oI3UwJJ3cnm6wK1qCQzw3LQTapoht77RJRHPZ0uepNmuxAENkXf0tm_ViUbsSj\" +\n    \"6EFCV5CDWQqH3aXWIdkiCAU4A0RkucCfFULcqINf2dfBkY3R9167QcyyhIIR335dIg-Jn2S3Ms2TuOiJ\" +\n    \"mVIx3ht7V091uCY6UWCmpIKebvMwnDubPnelivQ9tAwKnC2jR4eo_SppqT981sI61cG0tmud_TU6lwIW\" +\n    \"KoCXkm74hOIIw-JtB0zwfMXU1joh9m0_aPdw2FHyn4IhZVKpLeLYGlq8LtA_f723YpAqGzUM3VdjBqBq\" +\n    \"evHqr-YL5X8Rq5CdMndKEk5xKgy7a9tabPjmQINlDJvPOh69qnCXU0EjY9YJkJAfVBM0-QUw2ENrkk8X\" +\n    \"0Jnerfrdo-jFmA4UGKQlRooB3WU2zRbzNHpUOlUhdr1PGvwaKFeZBVgD8mKPorQ9M9fpbfvLv94j4u0a\" +\n    \"vGuJnHS8fIEIIttwkptkl6dZY5VJPirMexsQ8x9h7WlmoJc5KmkWxBQhDvM3Ct_MUx1rZJNrOLNi4Ie5\" +\n    \"Js_rUphBPRUBvsZoHHP6a7BkoGVK1Jkkz94oVE3S43QHcwxLRg7LerPwKMIfnnNynQDm7IE0n7FJ6I1J\" +\n    \"SrIJnQ0Lw2TrKHGF3LxRqJPpnzxYXyk__AlFTONjPxdh5D5GWbOXxupbq2XetLrtcbPlXiVhewejzhdo\" +\n    \"9AAHINpe-THmsGMM0YgSq6TGY-nbox6NZfQIV7lFpKfbhb4-vRv6CTcrSo2nPxcPPJklUYDX8AK7h4L2\" +\n    \"Hmc6n3T-BFv3rttrQnM2kmSPKZ2CzO3cMuLCOIdgoO12BVTnhT6Q46g5gwL6YSdsuOcRR1lpko7AMUmv\" +\n    \"KYDmI_qQ3gpToK2m2VtjzmpdQwQPsHEoGZ3KCv3FE37x64gVBjFjkcJuOj8PL87x2xJVivML5GYLopia\" +\n    \"u6glIJnSB-lRUd_NZZanFi05MKqaD37mr4thZHmNgUbyKKJceApVRwPQnH5JXZ-hIdOkErcKotzzFSXH\" +\n    \"QElnlHMJwPqlMH-_7FC2pKOgpjeIZTaAZtI5yubMlvRu6xEYYZMVWjko6C0w6JGAztBsbGGih-cp9UgZ\" +\n    \"vqVdxZpueTfDpepAdIPFNYl5GthaQfyzfldAD2a16HgTl7_RGNKkecRQEq_KUr6iX9Q1PyvswN4LSN6v\" +\n    \"AMgI8Uimcz0ZFZ2GT9AV8rWnSSLxTq-QIMjbJORY3LwDzKOG9IOs4T-5ZAuGoaDwmNklXTxUyPrCAGgT\" +\n    \"koFLAbO8hgV3OIuAMAjOFf77vsgFVzsQhSCAx9uDKsowXuxTnwwfkWn60jPoeZFW_D_IMw_SUil3b73h\" +\n    \"XvVU9TzYdYzAldUUSs8c9T32w_EqTx3dAFeq_UZenGz-Hpb0UvrdWaG4Nak31iEDMWgEaa6A5m7DVrCp\" +\n    \"Ep95i5hgSxW_A9piXVMf0yuX0nS2pUk9I156QvkqM_nqLi0KeHTdObLNxq40FWgvzE8yh-1xYXkUOS9C\" +\n    \"x2pkdEnzNWPzvKEIUPKyaNS6rDKHi4OW2Z20UHi9G08mrHfFWV-tv59bisy7pJA5Gaa2sB7GFqUqyh5X\" +\n    \"mLu9Vy98861yGdSVdKybEmwLrOgz20N63ie0ACuRk6UVaQaFgQX5ZZFBKSO19gsqiNxAGTPucd-9-K8y\" +\n    \"cBtQJ3bJgjboXjjQ7J-ehZxYvVJI-88Rsst_hnmgLUlmqwcZEtOVPuZEl7PjecfjB_2TzH0YQcW1BfWZ\" +\n    \"kr7fT7nwjf-Ow_za4spUGPe2BVMsG8wxDTvVwzzP8Qs-CrExhKiqxD7vImKOyel2BXU79lSJ80EFZJpV\" +\n    \"AmWma5DPsQHQGWxHVYCSe4MBvKBFvu0nE2P3YohFlxU4eR4FDC9ELCSuzy3O-FPmIDZNlb5H-CxKSDf-\" +\n    \"x7RL9sAPVduoRyKm30xnjUh61B2MOlORwB4buvAMVFIqURU86jNv6UqSUjHK_JigaLN9FDz-QJfUT72e\" +\n    \"XX10CJiRvdf9MNPOQ6UZ3scFY0OgFNW6g6BCs3u_wrRvljuvkFI5LYLENV1XGLKLjjmBufV3vfjSql3f\" +\n    \"ihfVGCGjXRLXgutD5lSZpGbSF05Clf1JxfSUJCq1rP4V_3GzXMwZD9AB7_Z3loOAkeRAmsjGndYKW833\" +\n    \"DbR_zGnFc6EoVlDvHScL_l1UYF-piWIb9IOUZ5WdGc0LDiOfSIj3oltosUnyC73nBmLcnRiBKbbKgIlm\" +\n    \"TnyGvqONb_vKNBsOM5f36dcW9AeFBrxLl0-huFK3XA81fp9shkvpybeJz06GmRhVIu9kzAWhqH-rRoE2\" +\n    \"7YuB8U4a6I-5VpHQGdMxAN7tIqpziaSDjsVjlqmvOPk0VQzES2d4Asf6IYRyiA7W-OfGi5pBiw3f2JNR\" +\n    \"leiFvUGd_nBA0Rc-6C9HGTzMLmMzIr_SXfvUxJvBvJRfoEQkWwfdb-FE5dkCupIiFrTR0Lu699B7ACEJ\" +\n    \"k1itx5Q-p3jUqdSEG7AeqdLT6JJ5CwBoEiC1wa15itVs4q2TlsYKVgVWfXDQ06ax7qGwWgYQyk_xwTfE\" +\n    \"p7erfra5P0wL3XDCtKE_oMX0HSpgVgqvKAzDF7GulOpzs8pFiGlNa2a0-6MeZ3cY3Id9vjlDSfdwtg_r\" +\n    \"5yPKDd7I10BFV9PSq6PtI6LVpZDGgTsThjMRpjnloYsrcik5Y_JN8HdTDe2UUZzLlQ\"\n  keyFunc : () => \"ddJNIRm2qio2Z7CXpKADiw\"\n  return afAxonEncryptorRt_callFunc(\"pointQueryLookup\", cipherText, [target, opts], keyFunc)\nend",
      "local_src": "(target:null, opts:{}) => do\n\n  //opts\n\n\n  colMeta: if(opts.has(\"colMeta\")) true else false\n  show: if(opts.has(\"show\")) true else false\n\n\n  queries: [\n    /////////////////////// AHUS /////////////////////////////////\n    //temps\n              {target:\"oat_sensor\",                  query:\"temp and air and point and outside and sensor\",                            colMeta:{color:\"#00b843\"} },\n              {target:\"dat_sensor\",                  query:\"temp and air and point and discharge and sensor and not leaving and not entering\", colMeta:{color:\"#0547fc\"} },\n              {target:\"dat_sp\",                      query:\"temp and air and point and discharge and sp\",                              colMeta:{color:\"#5cb3ff\"} },\n              {target:\"mat_sensor\",                  query:\"temp and air and point and mixed and sensor\",                              colMeta:{color:\"#FFD580\"} },\n              {target:\"mat_sp\",                      query:\"temp and air and point and mixed and sp\",                                  colMeta:{color:\"#FFA500\"} },\n              {target:\"eat_sensor\",                  query:\"temp and air and point and exhaust and sensor\",                            colMeta:{color:null} },\n              {target:\"rat_sensor\",                  query:\"temp and air and point and return and sensor\",                             colMeta:{color:\"#FF0000\"} },\n              {target:\"zntClg_sp\",                   query:\"temp and air and point and effective and zone and cooling and sp\",         colMeta:{color:\"#0547fc\"} },\n              {target:\"zntHtg_sp\",                   query:\"temp and air and point and effective and zone and heating and sp\",         colMeta:{color:\"#FF0000\"} },\n              {target:\"znt_sensor\",                  query:\"temp and air and point and zone and sensor\",                               colMeta:{color:\"#00b843\"} },\n              {target:\"znt_sp\",                      query:\"temp and air and point and zone and sp and not heating and not cooling\",   colMeta:{color:\"#71bd8d\"} },\n\n    //pressures\n              {target:\"dsp_sensor\",                  query:\"pressure and air and point and discharge and sensor and not filter\"       ,colMeta:{color:null} },\n              {target:\"dsp_sp\",                      query:\"pressure and air and point and discharge and sp and not filter\"           ,colMeta:{color:null} },\n              {target:\"bldg_press_sensor\",           query:\"air and building and point and pressure and sensor\"                       ,colMeta:{color:null} },\n              {target:\"bldg_press_sp\",               query:\"air and building and point and pressure and sp\"                           ,colMeta:{color:null} },\n    //Flow\n              {target:\"daFlow_sensor\",               query:\"flow and air and point and discharge and sensor\"                          ,colMeta:{color:null} },\n              {target:\"daFlow_sp\",                   query:\"flow and air and point and discharge and sp\"                              ,colMeta:{color:null} },\n              {target:\"eaFlow_sensor\",               query:\"flow and air and point and exhaust and sensor\"                            ,colMeta:{color:null} },\n              {target:\"eaFlow_sp\",                   query:\"flow and air and point and exhaust and sp\"                                ,colMeta:{color:null} },\n              {target:\"raFlow_sensor\",               query:\"flow and air and point and return and sensor\"                             ,colMeta:{color:null} },\n              {target:\"raFlow_sp\",                   query:\"flow and air and point and return and sp\"                                 ,colMeta:{color:null} },\n              {target:\"znflow_sp\",                   query:\"flow and air and point and discharge and sp\"                              ,colMeta:{color:null} },\n              {target:\"znflow_sensor\",               query:\"flow and air and point and discharge and sensor\"                          ,colMeta:{color:null} },\n              {target:\"tot_daFlow_sensor\",           query:\"flow and air and point and discharge and sensor and total\"                ,colMeta:{color:null} },\n              {target:\"tot_eaFlow_sensor\",           query:\"flow and air and point and exhaust and sensor and total\"                  ,colMeta:{color:null} },\n              {target:\"znDiffFlow_sensor\",           query:\"flow and air and point and delta and zone\"                                ,colMeta:{color:null} },\n    //valves\n              {target:\"htgValve_cmd\",                query:\"point and cmd and valve and water and (reheat or hot)\"                    ,colMeta:{color:null} },\n              {target:\"clgValve_cmd\",                query:\"point and water and (cool or chilled) and cmd and valve\"                  ,colMeta:{color:null} },\n              {target:\"phtValve_cmd\",                query:\"point and preheat and cmd and valve\"                                      ,colMeta:{color:null} },\n    //dampers\n              {target:\"oaDmpr_cmd\",                  query:\"damper and air and point and outside and cmd\"                             ,colMeta:{color:null} },\n              {target:\"maDmpr_cmd\",                  query:\"damper and air and point and mixed and cmd\"                               ,colMeta:{color:null} },\n              {target:\"eaDmpr_cmd\",                  query:\"damper and air and point and exhaust and cmd\"                             ,colMeta:{color:null} },\n              {target:\"raDmpr_cmd\",                  query:\"damper and air and point and return and cmd\"                              ,colMeta:{color:null} },\n              {target:\"rlfDmpr_cmd\",                 query:\"damper and air and point and relief and cmd\"                              ,colMeta:{color:null} },\n              {target:\"dmpr_cmd\",                    query:\"damper and air and point and cmd\"                                         ,colMeta:{color:null} },\n    //fans\n              {target:\"rtnFan_speed\",                query:\"return and point and fan and speed and cmd\"                               ,colMeta:{color:null} },\n              {target:\"rtnFan_cmd\",                  query:\"return and point and fan and run and cmd\"                                 ,colMeta:{color:null} },\n              {target:\"rlfFan_speed\",                query:\"relief and point and fan and speed and cmd\"                               ,colMeta:{color:null} },\n              {target:\"rlfFan_cmd\",                  query:\"relief and point and fan and run and cmd\"                                 ,colMeta:{color:null} },\n              {target:\"exhFan_speed\",                query:\"exhaust and point and fan and speed and cmd\"                              ,colMeta:{color:null} },\n              {target:\"daFan_speed\",                 query:\"discharge and point and fan and speed and cmd\"                            ,colMeta:{color:null} },\n              {target:\"daFan_run\",                   query:\"discharge and point and fan and run and cmd\"                              ,colMeta:{color:null} },\n\n    //humidity\n              {target:\"daHumid_sensor\",              query:\"point and sensor and discharge and air and humidity\"                      ,colMeta:{color:null} },\n              {target:\"humidifier_cmd\",              query:\"point and air and cmd and humidifier and not output\"                      ,colMeta:{color:null} },\n    //lab\n              {target:\"savFlow_sensor\",              query:\"(discharge or supply) and point and flow and sensor\"},\n    //other\n              {target:\"occ_cmd\",                     query:\"occupied and point and cmd\"},\n              {target:\"znOcc_cmd\",                   query:\"occupied and point and cmd\"},\n              {target:\"labOcc_cmd\",                  query:\"occupied and point and cmd and lab\"},\n              {target:\"fumeHoodOcc_cmd\",             query:\"occupied and point and cmd and fumeHood\"},\n              {target:\"siteOcc_cmd\",                 query:\"occupied and sitePoint\"},\n              {target:\"ahu_state\",                   query:\"point and (ahu or ahuState)\"},\n    //dual duct\n              {target:\"hotDeck_daFlow_sensor\",       query:\"hotDeck and flow and air and point and discharge and sensor\"},\n              {target:\"hotDeck_daFlow_sp\",           query:\"hotDeck and flow and air and point and discharge and sp\"},\n              {target:\"hotDeck_dmpr_cmd\",            query:\"hotDeck and damper and air and point and cmd\"},\n              {target:\"hotDeck_dmpr_reset\",          query:\"hotDeck and damper and air and point and cmd and reset\"},\n\n              {target:\"coldDeck_daFlow_sensor\",      query:\"coldDeck and flow and air and point and discharge and sensor\"},\n              {target:\"coldDeck_daFlow_sp\",          query:\"coldDeck and flow and air and point and discharge and sp\"},\n              {target:\"coldDeck_dmpr_cmd\",           query:\"coldDeck and damper and air and point and cmd\"},\n              {target:\"coldDeck_dmpr_reset\",         query:\"coldDeck and damper and air and point and cmd and reset\"},\n\n    //buildingHealth\n              {target:\"site_eui\",                     query:\"point and eui and equipRef->buildingHealth\"},\n              {target:\"other_eui\",                    query:\"point and other and equipRef->buildingHealth\"},\n              {target:\"chw_eui\",                      query:\"point and chilledWater and euiContributor and equipRef->buildingHealth\"},\n              {target:\"elec_eui\",                     query:\"point and elec and euiContributor and equipRef->buildingHealth\"},\n              {target:\"hw_eui\",                       query:\"point and hotWater and euiContributor and equipRef->buildingHealth\"},\n              {target:\"naturalGas_eui\",               query:\"point and naturalGas and euiContributor and equipRef->buildingHealth\"},\n              {target:\"natGas_eui\",                   query:\"point and naturalGas and euiContributor and equipRef->buildingHealth\"},\n              {target:\"site_eci\",                     query:\"point and eci and equipRef->buildingHealth\"},\n              {target:\"site_uncomfortableVAVs\",       query:\"point and vavScorePt and uncomfortable and equipRef->buildingHealth\"},\n              {target:\"site_comfortableVAVs\",         query:\"point and vavScorePt and comfortable and equipRef->buildingHealth\"},\n              {target:\"site_equipLife\",               query:\"point and equipmentLife and equipRef->buildingHealth\"},\n              {target:\"site_buildingComfort\",         query:\"point and buildingScore and comfort and equipRef->buildingHealth\"},\n              {target:\"site_buildingArea\",            query:\"point and buildingArea and equipRef->buildingHealth\"},\n    //elec meter\n              {target:\"elec_intervalPwr\",            query:\"elec and power and interval and equipRef->siteMeter and not cost and not modelPoint  and not syntheticPoint and not savings\"},\n              {target:\"elec_modelPwr\",               query:\"elec and power and calculated and modelHisFunction and modelPoint and interval and equipRef->siteMeter\"},\n              {target:\"elec_monthlyEnergy\",          query:\"elec and energy and monthly and not cost and not rateCost and equipRef->siteMeter and not modelPoint and not savings\"},\n              {target:\"elec_modelEnergy\",            query:\"elec and energy and monthly and equipRef->siteMeter and modelPoint\"},\n              {target:\"elec_monthlyCost\",            query:\"elec and energy and monthly and equipRef->siteMeter and cost\"},\n              {target:\"elec_intervalCost\",           query:\"elec and power and interval and equipRef->siteMeter and cost and not savings\"},\n              {target:\"elec_monthlyRateCost\",        query:\"elec and energy and monthly and equipRef->siteMeter and rateCost\"},\n              {target:\"elec_intervalSavingsCost\",    query:\"elec and power and interval and equipRef->siteMeter and savings and cost\"},\n              {target:\"elec_savingsPwr\",             query:\"elec and power and interval and equipRef->siteMeter and savings and not cost\"},\n              {target:\"elec_monthlySavingsCost\",     query:\"elec and energy and monthly and equipRef->siteMeter and savings and cost\"},\n              {target:\"elec_savingsEnergy\",          query:\"elec and energy and monthly and equipRef->siteMeter and savings and not cost\"},\n              {target:\"elec_genAIModelDemand\",       query:\"elec and power and syntheticPoint and mlPoint and interval and equipRef->siteMeter\"},\n    //natGas meter\n              {target:\"natGas_intervalCons\",         query:\"naturalGas and consumption and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings\"},\n              {target:\"natGas_intervalModelCons\",    query:\"naturalGas and consumption and calculated and modelHisFunction and modelPoint and interval and equipRef->siteMeter\"},\n              {target:\"natGas_monthlyCons\",          query:\"naturalGas and consumption and monthly and not cost and not rateCost and equipRef->siteMeter and not modelPoint and not savings\"},\n              {target:\"natGas_monthlyModelCons\",     query:\"naturalGas and consumption and monthly and equipRef->siteMeter and modelPoint\"},\n              {target:\"natGas_monthlyCost\",          query:\"naturalGas and consumption and monthly and equipRef->siteMeter and cost\"},\n              {target:\"natGas_intervalCost\",         query:\"naturalGas and consumption and interval and equipRef->siteMeter and cost and not savings\"},\n              {target:\"natGas_monthlyRateCost\",      query:\"naturalGas and consumption and monthly and equipRef->siteMeter and rateCost\"},\n              {target:\"natGas_intervalSavingsCost\",  query:\"naturalGas and consumption and interval and equipRef->siteMeter and savings and cost\"},\n              {target:\"natGas_intervalSavingsCons\",  query:\"naturalGas and consumption and interval and equipRef->siteMeter and savings and not cost\"},\n              {target:\"natGas_monthlySavingsCost\",   query:\"naturalGas and consumption and monthly and equipRef->siteMeter and savings and cost\"},\n              {target:\"natGas_monthlySavingsCons\",   query:\"naturalGas and consumption and monthly and equipRef->siteMeter and savings and not cost\"},\n              {target:\"natGas_genAIModelCons\",       query:\"naturalGas and consumption and interval and syntheticPoint and mlPoint and equipRef->siteMeter\"},\n    //hot water meter\n              {target:\"hotWater_intervalCons\",         query:\"hot and water and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings\"},\n              {target:\"hotWater_intervalModelCons\",    query:\"hot and water and calculated and modelHisFunction and modelPoint and interval and equipRef->siteMeter\"},\n              {target:\"hotWater_monthlyCons\",          query:\"hot and water and monthly and not cost and not rateCost and equipRef->siteMeter and not modelPoint and not savings\"},\n              {target:\"hotWater_monthlyModelCons\",     query:\"hot and water and monthly and equipRef->siteMeter and modelPoint\"},\n              {target:\"hotWater_monthlyCost\",          query:\"hot and water and monthly and equipRef->siteMeter and cost\"},\n              {target:\"hotWater_intervalCost\",         query:\"hot and water and interval and equipRef->siteMeter and not savings and cost\"},\n              {target:\"hotWater_monthlyRateCost\",      query:\"hot and water and monthly and equipRef->siteMeter and rateCost\"},\n              {target:\"hotWater_intervalSavingsCost\",  query:\"hot and water and interval and equipRef->siteMeter and savings and cost\"},\n              {target:\"hotWater_intervalSavingsCons\",  query:\"hot and water and interval and equipRef->siteMeter and savings and not cost\"},\n              {target:\"hotWater_monthlySavingsCost\",   query:\"hot and water and monthly and equipRef->siteMeter and savings and cost\"},\n              {target:\"hotWater_monthlySavingsCons\",   query:\"hot and water and monthly and equipRef->siteMeter and savings and not cost\"},\n              {target:\"hotWater_genAIModelCons\",       query:\"hot and water and interval and syntheticPoint and mlPoint and equipRef->siteMeter\"},\n    //chilled water meter\n              {target:\"chilledWater_intervalCons\",         query:\"chilled and water and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings\"},\n              {target:\"chilledWater_intervalModelCons\",    query:\"chilled and water and calculated and modelHisFunction and modelPoint and interval and equipRef->siteMeter\"},\n              {target:\"chilledWater_monthlyCons\",          query:\"chilled and water and monthly and not cost and not rateCost and equipRef->siteMeter and not modelPoint and not savings\"},\n              {target:\"chilledWater_monthlyModelCons\",     query:\"chilled and water and monthly and equipRef->siteMeter and modelPoint\"},\n              {target:\"chilledWater_monthlyCost\",          query:\"chilled and water and monthly and equipRef->siteMeter and cost\"},\n              {target:\"chilledWater_intervalCost\",         query:\"chilled and water and interval and equipRef->siteMeter and cost and not savings\"},\n              {target:\"chilledWater_monthlyRateCost\",      query:\"chilled and water and monthly and equipRef->siteMeter and rateCost\"},\n              {target:\"chilledWater_intervalSavingsCost\",  query:\"chilled and water and interval and equipRef->siteMeter and savings and cost\"},\n              {target:\"chilledWater_intervalSavingsCons\",  query:\"chilled and water and interval and equipRef->siteMeter and savings and not cost\"},\n              {target:\"chilledWater_monthlySavingsCost\",   query:\"chilled and water and monthly and equipRef->siteMeter and savings and cost\"},\n              {target:\"chilledWater_monthlySavingsCons\",   query:\"chilled and water and monthly and equipRef->siteMeter and savings and not cost\"},\n              {target:\"chilledWater_genAIModelCons\",       query:\"chilled and water and interval and syntheticPoint and mlPoint and equipRef->siteMeter\"},\n    //steam meter\n              {target:\"steam_intervalCons\",         query:\"steam and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings\"},\n              {target:\"steam_intervalModelCons\",    query:\"steam and calculated and modelHisFunction and modelPoint and interval and equipRef->siteMeter\"},\n              {target:\"steam_monthlyCons\",          query:\"steam and monthly and not cost and not rateCost and equipRef->siteMeter and not modelPoint and not savings\"},\n              {target:\"steam_monthlyModelCons\",     query:\"steam and monthly and equipRef->siteMeter and modelPoint\"},\n              {target:\"steam_monthlyCost\",          query:\"steam and monthly and equipRef->siteMeter and cost\"},\n              {target:\"steam_intervalCost\",         query:\"steam and interval and equipRef->siteMeter and cost and not savings\"},\n              {target:\"steam_monthlyRateCost\",      query:\"steam and monthly and equipRef->siteMeter and rateCost\"},\n              {target:\"steam_intervalSavingsCost\",  query:\"steam and interval and equipRef->siteMeter and savings and cost\"},\n              {target:\"steam_intervalSavingsCons\",  query:\"steam and interval and equipRef->siteMeter and savings and not cost\"},\n              {target:\"steam_monthlySavingsCost\",   query:\"steam and monthly and equipRef->siteMeter and savings and cost\"},\n              {target:\"steam_monthlySavingsCons\",   query:\"steam and monthly and equipRef->siteMeter and savings and not cost\"},\n              {target:\"steam_genAIModelCons\",       query:\"steam and interval and syntheticPoint and mlPoint and equipRef->siteMeter\"},\n    //water meter\n              {target:\"water_intervalCons\",         query:\"water and not chilled and not hot and consumption and interval and equipRef->siteMeter and not cost and not modelPoint and not savings\"},\n\n    //hot water\n              {target:\"hw_delta_press\",             query:\"hot and water and pressure and delta and sensor\"},\n              {target:\"hw_flow\",                    query:\"hot and water and flow and sensor\"},\n              {target:\"hw_pri_pump_speed\",          query:\"hot and water and pump and speed and cmd and primary\"},\n              {target:\"hw_pri_pump_status\",         query:\"hot and water and pump and run and cmd and primary\"},\n              {target:\"hw_sec_pump_speed\",          query:\"hot and water and pump and speed and cmd and secondary\"},\n              {target:\"hw_sec_pump_status\",         query:\"hot and water and pump and run and cmd and secondary\"},\n              {target:\"hw_pri_pump_power\",          query:\"hot and water and pump and power and sensor and primary\"},\n              {target:\"hw_sec_pump_power\",          query:\"hot and water and pump and power and sensor and secondary\"},\n              {target:\"hw_supply_temp\",             query:\"hot and water and supply and temp and sensor\"},\n              {target:\"hw_return_temp\",             query:\"hot and water and return and temp and sensor and not domestic\"},\n              {target:\"hw_supply_temp_sp\",          query:\"hot and water and supply and temp and sp\"},\n              {target:\"hw_delta_press_sp\",          query:\"hot and water and pressure and delta and sp\"},\n              {target:\"hw_delta_temp\",              query:\"hot and water and temp and delta and sensor\"},\n              {target:\"hw_sys_load\",                query:\"hot and water and load and energy\"},\n              {target:\"hw_bypass_valve\",            query:\"hot and water and bypass and valve and cmd\"},\n\n    //chilled water\n              {target:\"chw_delta_press\",             query:\"chilled and water and pressure and delta and sensor\"},\n              {target:\"chw_flow\",                    query:\"chilled and water and flow and sensor and not bypass\"},\n              {target:\"chw_pri_pump_speed\",          query:\"chilled and water and pump and speed and cmd and primary\"},\n              {target:\"chw_sec_pump_speed\",          query:\"chilled and water and pump and speed and cmd and secondary\"},\n              {target:\"chw_pri_pump_status\",         query:\"chilled and water and pump and run and cmd and primary\"},\n              {target:\"chw_sec_pump_status\",         query:\"chilled and water and pump and run and cmd and secondary\"},\n              {target:\"chw_pri_pump_power\",          query:\"chilled and water and pump and power and sensor and primary\"},\n              {target:\"chw_sec_pump_power\",          query:\"chilled and water and pump and power and sensor and secondary\"},\n            //{target:\"chw_pri_pump_status\",         query:\"chilled and water and pump and run and cmd and primary\"},\n            //{target:\"chw_sec_pump_status\",         query:\"chilled and water and pump and run and cmd and secondary\"},\n              {target:\"chw_supply_temp\",             query:\"chilled and water and supply and temp and sensor\"},\n              {target:\"chw_return_temp\",             query:\"chilled and water and return and temp and sensor\"},\n              {target:\"chw_supply_temp_sp\",          query:\"chilled and water and supply and temp and sp\"},\n              {target:\"chw_delta_press_sp\",          query:\"chilled and water and pressure and delta and sp\"},\n              {target:\"chw_delta_temp\",              query:\"chilled and water and temp and delta and sensor\"},\n              {target:\"chw_sys_load\",                query:\"chilled and water and load and energy\"},\n\n     //General Pump Points\n              {target:\"pri_pump_speed\",              query:\"water and pump and speed and cmd and primary\"},\n              {target:\"sec_pump_speed\",              query:\"water and pump and speed and cmd and secondary\"},\n              {target:\"pri_pump_status\",             query:\"water and pump and run and cmd and primary\"},\n              {target:\"sec_pump_status\",             query:\"water and pump and run and cmd and secondary\"},\n              {target:\"pri_pump_power\",              query:\"water and pump and power and sensor and primary\"},\n              {target:\"sec_pump_power\",              query:\"water and pump and power and sensor and secondary\"},\n\n     //chillers\n              {target:\"chlr_status\",                   query:\"chiller and cmd and run\"},\n              {target:\"chlr_power\",                    query:\"chiller and power and sensor\"},\n              {target:\"chlr_entering_temp\",            query:\"chiller and chilled and entering and temp and water and sensor and not condenser\"},\n              {target:\"chlr_leaving_temp\",             query:\"chiller and chilled and leaving and temp and water and sensor and not condenser\"},\n              {target:\"chlr_flow\",                     query:\"chiller and chilled and flow and water and sensor\"},\n              {target:\"chlr_fla\",                      query:\"chiller and fla and sensor\"},\n              {target:\"chlr_cw_entering_temp\",         query:\"chiller and condenser and entering and temp and water and sensor\"},\n              {target:\"chlr_cw_leaving_temp\",          query:\"chiller and condenser and leaving and temp and water and sensor\"},\n\n     //boilers\n              {target:\"blr_firing_rate\",              query:\"boiler and firingRate and sensor\"},\n              {target:\"blr_entering_temp\",            query:\"boiler and hot and (entering or return) and temp and water and sensor\"},\n              {target:\"blr_leaving_temp\",             query:\"boiler and hot and leaving and temp and water and sensor\"},\n              {target:\"blr_leaving_temp_sp\",          query:\"boiler and hot and leaving and temp and water and sp\"},\n              {target:\"blr_run_status\",               query:\"boiler and run and cmd\"},\n              {target:\"blr_enable\",                   query:\"boiler and cmd and enable\"},\n              {target:\"blr_gas_flow_rate\",            query:\"boiler and flow and rate and sensor and naturalGas\"},\n              {target:\"blr_flow\",                     query:\"boiler and notImplemented\"},\n              {target:\"blr_power\",                    query:\"boiler and power and sensor\"},\n\n\n     //cooling tower\n              {target:\"clgtwr_leaving_temp\",         query:\"coolingTower and temp and (leaving or supply) and water and sensor\"},\n              {target:\"clgtwr_entering_temp\",        query:\"coolingTower and temp and (entering or return) and water and sensor\"},\n              {target:\"clgtwr_fan_speed\",            query:\"coolingTower and fan and speed and air and cmd\"},\n              {target:\"clgtwr_power\",                query:\"coolingTower and power and sensor\"},\n              {target:\"clgtwr_fan_status\",           query:\"coolingTower and cmd and run\"},\n\n\n     //condenser water\n              {target:\"cws_supply_temp\",             query:\"condenser and water and supply and temp and sensor\"},\n              {target:\"cws_supply_temp_sp\",          query:\"condenser and water and supply and temp and sp\"},\n              {target:\"cws_return_temp\",             query:\"condenser and water and return and temp and sensor\"},\n\n     //miscellaneous\n              {target:\"wet_bulb\",                    query:\"temp and wetBulb\"},\n              {target:\"oah_sensor\",                  query:\"humidity and air and point and outside and sensor\"},\n              \n     // Heat Recovery Coil\n       // Discharge\n              {target:\"disHRC_ea_sensor\",            query:\"heatRecovery and coil and air and temp and sensor and discharge and entering\"},\n              {target:\"disHRC_la_sensor\",            query:\"heatRecovery and coil and air and temp and sensor and discharge and leaving\"},\n              {target:\"disHRC_flow_sensor\",          query:\"heatRecovery and coil and air and flow and sensor and discharge\"},\n              {target:\"disHRC_valve_cmd\",            query:\"heatRecovery and coil and water and valve and cmd and discharge\"},\n       // Exhaust       \n              {target:\"exHRC_ea_sensor\",             query:\"heatRecovery and coil and air and temp and sensor and exhaust and entering\"},\n              {target:\"exHRC_la_sensor\",             query:\"heatRecovery and coil and air and temp and sensor and exhaust and leaving\"},\n              {target:\"exHRC_flow_sensor\",           query:\"heatRecovery and coil and air and flow and sensor and exhaust\"},\n              {target:\"exHRC_valve_cmd\",             query:\"heatRecovery and coil and water and valve and cmd and exhaust\"},\n             \n     // Heat Recovery Wheel\n              {target:\"disHRW_ea_sensor\",            query:\"heatRecovery and wheel and air and temp and sensor and discharge and entering\"},\n              {target:\"disHRW_la_sensor\",            query:\"heatRecovery and wheel and air and temp and sensor and discharge and leaving\"},\n              {target:\"disHRW_flow_sensor\",          query:\"heatRecovery and wheel and air and flow and sensor and discharge\"},\n              \n              {target:\"exHRW_ea_sensor\",             query:\"heatRecovery and wheel and air and temp and sensor and exhaust and entering\"},\n              {target:\"exHRW_la_sensor\",             query:\"heatRecovery and wheel and air and temp and sensor and exhaust and leaving\"},\n              {target:\"exHRW_flow_sensor\",           query:\"heatRecovery and wheel and air and flow and sensor and exhaust\"},\n              \n              {target:\"hrw_status\",                  query:\"heatRecovery and wheel and sensor and run\"},\n              {target:\"hrw_cmd\",                     query:\"heatRecovery and wheel and cmd\"},\n              \n     // Lab Spaces\n              {target:\"ach\",                         query:\"ach and air and sensor\"},\n              {target:\"sash_cmd\",                    query:\"sash and cmd and fumeHood\"},\n  \n     //Coil Temps\n             //hot\n             {target:\"chw_entg_temp\",                query:\"chilled and water and valve\"},\n             {target:\"chw_lvg_temp\",                 query:\"humidity and air and point and outside and sensor\"},\n\n             //chilled\n             {target:\"hw_entg_temp\",                 query:\"temp and wetBulb\"},\n             {target:\"hw_lvg_temp\",                  query:\"humidity and air and point and outside and sensor\"}, \n             \n     //Baseline Search\n             {target:\"baseline_calc\",                query:\"point and calculated and modelPoint and not cost and not savings\"},\n     \n     //Utility Level Points for Dashboards\n             //Elec\n             {target:\"portfolioDashboard_elecUsage\",        query:\"portfolioDashboardPoint and elec and not cost and not savings\"},\n             {target:\"portfolioDashboard_elecCost\",         query:\"portfolioDashboardPoint and elec and cost and not savings\"},\n             {target:\"portfolioDashboard_elecSavingsCost\",  query:\"portfolioDashboardPoint and elec and cost and savings\"},\n             {target:\"portfolioDashboard_elecSavings\",      query:\"portfolioDashboardPoint and elec and usage and savings and not cost\"},\n             \n             //Steam\n             {target:\"portfolioDashboard_steamUsage\",        query:\"portfolioDashboardPoint and steam and not cost and not savings\"},\n             {target:\"portfolioDashboard_steamCost\",         query:\"portfolioDashboardPoint and steam and cost and not savings\"},\n             {target:\"portfolioDashboard_steamSavingsCost\",  query:\"portfolioDashboardPoint and steam and cost and savings\"},\n             {target:\"portfolioDashboard_steamSavings\",      query:\"portfolioDashboardPoint and steam and usage and savings and not cost\"},\n             \n             //Chilled Water\n             {target:\"portfolioDashboard_chwUsage\",        query:\"portfolioDashboardPoint and chw and not cost and not savings\"},\n             {target:\"portfolioDashboard_chwCost\",         query:\"portfolioDashboardPoint and chw and cost and not savings\"},\n             {target:\"portfolioDashboard_chwSavingsCost\",  query:\"portfolioDashboardPoint and chw and cost and savings\"},\n             {target:\"portfolioDashboard_chwSavings\",      query:\"portfolioDashboardPoint and chw and usage and savings and not cost\"},\n             \n             //Hot Water\n             {target:\"portfolioDashboard_hwUsage\",        query:\"portfolioDashboardPoint and hw and not cost and not savings\"},\n             {target:\"portfolioDashboard_hwCost\",         query:\"portfolioDashboardPoint and hw and cost and not savings\"},\n             {target:\"portfolioDashboard_hwSavingsCost\",  query:\"portfolioDashboardPoint and hw and cost and savings\"},\n             {target:\"portfolioDashboard_hwSavings\",      query:\"portfolioDashboardPoint and hw and usage and savings and not cost\"},\n           ].toGrid\n\n\n  if(show) return queries\n\n  match: queries.find(r => r.get(\"target\")==target)\n  if (match.isNull) throw \"Cannot find query: \"+target\n\n  out: if (colMeta) match.get(\"colMeta\")\n       else         match.get(\"query\")\n\n  return out\nend",
      "diff": "@@ -1,61 +1,325 @@-(target: null, opts: {}) => do\n-  cipherText :\n-    \"AxD:J7ik5GzASro65uWVIxMkAg::7cPS7lsTZSH5N2vG2V_-JnF7WnnW0YzEzAkoWuWN0t33cekbuST1\" +\n-    \"jdwUtsnrnsWKnY2ZJ11FgjHRBw-4DchwEYWtNDzsPhtoG9qIeWzimhGiuRu1zA5WRrk0313ancLuQXcH\" +\n-    \"zUsoSr9-_gDRNSvmhd8jJ8XaV0eozzR7scJJx801Sxs1lYKqn9-efJgMoGE5yUdio-ucRJMKwXCuww-J\" +\n-    \"b8TqM0LRciBpX_qgy9sWW83-toyx1g1E6NXoPfuyE4EB_DXdpr3CmMSWscawuIAE3KH2FmPCMCU8ihr3\" +\n-    \"tr_mcJm1MYsEo96AokjwJZusgH38mbnKrRoCf8NHPHr0pfCbgMSC-bF-D68vIgcs7pkROhkb8dHHy0Yb\" +\n-    \"BGZHrQwecGLFxh3jego2SM4MSQxMksgcXyMChH17bY10l6VEqvIczPolCB66sNO2bVrCc5FWAFPGXxtr\" +\n-    \"m_T7E__lMU78LEPWLfO1UMWE_FLJtlEAQbuv_xhB4jUy4kyqpWCjuwNLkCrYY7sWHzyE90-Ys6SJzJLF\" +\n-    \"INz9kBTIPWsbbjDHx0satQxguftZqyzoAjtoNJvymRdqyVTMTH2kLe1zoF_fI7afvDstxdp_YJ1Snx8y\" +\n-    \"b3DLsnyLYJmKX7M8z4og1VWfRq6MdK-OCyjnRkLCO8Yv2ieCKWXBBbt7b5H7sVq8aZe-QHMINJW9XJQa\" +\n-    \"AnOn9ePqiMWZoCfUbSB_dpwe89a9xMwOB_Ndj9gW8AoXEiK2uUgBEN_p4zcjOmNOJemXpluU-yQDFqJ3\" +\n-    \"bZKgktkwNLUwrhYAYsp8Rg12CkGeXqx5hEEhEeXaUZisO7OKUHC9iFpi0CXbKbIB7bQc5kmWXCQNOKpk\" +\n-    \"WJX_EPBrGrlmKXXseMsPuT78bkNoPxGbYxGwE4cSIfDqE0wkZOpILvuYXWkc5_jIdwuNsHSNLMXtEG2W\" +\n-    \"NZ2dCBXpdFS7pFE5KrdkkydqepBBHgcCia5uNWcm1Sm9MuqqhjSK3R2r8D8VxPAJNIP-B05znD5pcUt8\" +\n-    \"H-XKlS1RIkPpOpT3_0o_ieo7Dk2A51Ta4aSMsV1dkCGA78aq6mSF6oBYWefmW2FGYsmJjl0_KhJ30cuM\" +\n-    \"-VLV1Tm58wPWB4gk-VTLLCCYqcu2eiH2XOMA5h2A0Jqm8nAp0-VH8rIvsgcdXV7OyBp7RThiFhmXrczT\" +\n-    \"9UjSq1fDGg5ss22R-nyAlL6Cy9lFNgAwJXhhZGyoeEmKAnTju_Eszm8fkclrd66V2OG8Oumqyo_uLyAT\" +\n-    \"WaOvw-a_AV8IniBRcJsnpdMFFuoGLKQ_IeAojF82KpVG0sB7Q9SR5iOJOCgu1zOfjmbJJuRiKt_5lvlX\" +\n-    \"vjjxxHEwjXF_f-PFNDclHEwLpjwPm0u3Vln7BaLW0nbWkRsC8_GugIUsI0JIOZXfv7gCowIcqWPqFl2k\" +\n-    \"AQTXjulVkhATkjviOkhCRzRpJgBt_4vjZzIPZKnJZqEq1aIDDCk8TxIBlABWT2Secx_B7z6vXxM6sKJO\" +\n-    \"nThHzuHjlsJYDXrMEq1NVYtIz6HOv86FEksDz8zR7cCtSfQE2z-PMEeIBL0g5g4k5lDduU0ltFQDamy8\" +\n-    \"jGokPxsAKgmYeqtWlvQDFircTDaUEURdVC0t0aRmXAOHIlhqYslE5vMmXfaZyB8tY8XC-QO4RwKRNOsA\" +\n-    \"Ii_lUujsAX-uRZib_Qj79SArzhQVKHalYEglFumjDyOfjftf0dAqW7Lm3RzLBPnSAP5drZMTfCqr_keo\" +\n-    \"qCRPNnnsdgVhHpauC4oI3UwJJ3cnm6wK1qCQzw3LQTapoht77RJRHPZ0uepNmuxAENkXf0tm_ViUbsSj\" +\n-    \"6EFCV5CDWQqH3aXWIdkiCAU4A0RkucCfFULcqINf2dfBkY3R9167QcyyhIIR335dIg-Jn2S3Ms2TuOiJ\" +\n-    \"mVIx3ht7V091uCY6UWCmpIKebvMwnDubPnelivQ9tAwKnC2jR4eo_SppqT981sI61cG0tmud_TU6lwIW\" +\n-    \"KoCXkm74hOIIw-JtB0zwfMXU1joh9m0_aPdw2FHyn4IhZVKpLeLYGlq8LtA_f723YpAqGzUM3VdjBqBq\" +\n-    \"evHqr-YL5X8Rq5CdMndKEk5xKgy7a9tabPjmQINlDJvPOh69qnCXU0EjY9YJkJAfVBM0-QUw2ENrkk8X\" +\n-    \"0Jnerfrdo-jFmA4UGKQlRooB3WU2zRbzNHpUOlUhdr1PGvwaKFeZBVgD8mKPorQ9M9fpbfvLv94j4u0a\" +\n-    \"vGuJnHS8fIEIIttwkptkl6dZY5VJPirMexsQ8x9h7WlmoJc5KmkWxBQhDvM3Ct_MUx1rZJNrOLNi4Ie5\" +\n-    \"Js_rUphBPRUBvsZoHHP6a7BkoGVK1Jkkz94oVE3S43QHcwxLRg7LerPwKMIfnnNynQDm7IE0n7FJ6I1J\" +\n-    \"SrIJnQ0Lw2TrKHGF3LxRqJPpnzxYXyk__AlFTONjPxdh5D5GWbOXxupbq2XetLrtcbPlXiVhewejzhdo\" +\n-    \"9AAHINpe-THmsGMM0YgSq6TGY-nbox6NZfQIV7lFpKfbhb4-vRv6CTcrSo2nPxcPPJklUYDX8AK7h4L2\" +\n-    \"Hmc6n3T-BFv3rttrQnM2kmSPKZ2CzO3cMuLCOIdgoO12BVTnhT6Q46g5gwL6YSdsuOcRR1lpko7AMUmv\" +\n-    \"KYDmI_qQ3gpToK2m2VtjzmpdQwQPsHEoGZ3KCv3FE37x64gVBjFjkcJuOj8PL87x2xJVivML5GYLopia\" +\n-    \"u6glIJnSB-lRUd_NZZanFi05MKqaD37mr4thZHmNgUbyKKJceApVRwPQnH5JXZ-hIdOkErcKotzzFSXH\" +\n-    \"QElnlHMJwPqlMH-_7FC2pKOgpjeIZTaAZtI5yubMlvRu6xEYYZMVWjko6C0w6JGAztBsbGGih-cp9UgZ\" +\n-    \"vqVdxZpueTfDpepAdIPFNYl5GthaQfyzfldAD2a16HgTl7_RGNKkecRQEq_KUr6iX9Q1PyvswN4LSN6v\" +\n-    \"AMgI8Uimcz0ZFZ2GT9AV8rWnSSLxTq-QIMjbJORY3LwDzKOG9IOs4T-5ZAuGoaDwmNklXTxUyPrCAGgT\" +\n-    \"koFLAbO8hgV3OIuAMAjOFf77vsgFVzsQhSCAx9uDKsowXuxTnwwfkWn60jPoeZFW_D_IMw_SUil3b73h\" +\n-    \"XvVU9TzYdYzAldUUSs8c9T32w_EqTx3dAFeq_UZenGz-Hpb0UvrdWaG4Nak31iEDMWgEaa6A5m7DVrCp\" +\n-    \"Ep95i5hgSxW_A9piXVMf0yuX0nS2pUk9I156QvkqM_nqLi0KeHTdObLNxq40FWgvzE8yh-1xYXkUOS9C\" +\n-    \"x2pkdEnzNWPzvKEIUPKyaNS6rDKHi4OW2Z20UHi9G08mrHfFWV-tv59bisy7pJA5Gaa2sB7GFqUqyh5X\" +\n-    \"mLu9Vy98861yGdSVdKybEmwLrOgz20N63ie0ACuRk6UVaQaFgQX5ZZFBKSO19gsqiNxAGTPucd-9-K8y\" +\n-    \"cBtQJ3bJgjboXjjQ7J-ehZxYvVJI-88Rsst_hnmgLUlmqwcZEtOVPuZEl7PjecfjB_2TzH0YQcW1BfWZ\" +\n-    \"kr7fT7nwjf-Ow_za4spUGPe2BVMsG8wxDTvVwzzP8Qs-CrExhKiqxD7vImKOyel2BXU79lSJ80EFZJpV\" +\n-    \"AmWma5DPsQHQGWxHVYCSe4MBvKBFvu0nE2P3YohFlxU4eR4FDC9ELCSuzy3O-FPmIDZNlb5H-CxKSDf-\" +\n-    \"x7RL9sAPVduoRyKm30xnjUh61B2MOlORwB4buvAMVFIqURU86jNv6UqSUjHK_JigaLN9FDz-QJfUT72e\" +\n-    \"XX10CJiRvdf9MNPOQ6UZ3scFY0OgFNW6g6BCs3u_wrRvljuvkFI5LYLENV1XGLKLjjmBufV3vfjSql3f\" +\n-    \"ihfVGCGjXRLXgutD5lSZpGbSF05Clf1JxfSUJCq1rP4V_3GzXMwZD9AB7_Z3loOAkeRAmsjGndYKW833\" +\n-    \"DbR_zGnFc6EoVlDvHScL_l1UYF-piWIb9IOUZ5WdGc0LDiOfSIj3oltosUnyC73nBmLcnRiBKbbKgIlm\" +\n-    \"TnyGvqONb_vKNBsOM5f36dcW9AeFBrxLl0-huFK3XA81fp9shkvpybeJz06GmRhVIu9kzAWhqH-rRoE2\" +\n-    \"7YuB8U4a6I-5VpHQGdMxAN7tIqpziaSDjsVjlqmvOPk0VQzES2d4Asf6IYRyiA7W-OfGi5pBiw3f2JNR\" +\n-    \"leiFvUGd_nBA0Rc-6C9HGTzMLmMzIr_SXfvUxJvBvJRfoEQkWwfdb-FE5dkCupIiFrTR0Lu699B7ACEJ\" +\n-    \"k1itx5Q-p3jUqdSEG7AeqdLT6JJ5CwBoEiC1wa15itVs4q2TlsYKVgVWfXDQ06ax7qGwWgYQyk_xwTfE\" +\n-    \"p7erfra5P0wL3XDCtKE_oMX0HSpgVgqvKAzDF7GulOpzs8pFiGlNa2a0-6MeZ3cY3Id9vjlDSfdwtg_r\" +\n-    \"5yPKDd7I10BFV9PSq6PtI6LVpZDGgTsThjMRpjnloYsrcik5Y_JN8HdTDe2UUZzLlQ\"\n-  keyFunc : () => \"ddJNIRm2qio2Z7CXpKADiw\"\n-  return afAxonEncryptorRt_callFunc(\"pointQueryLookup\", cipherText, [target, opts], keyFunc)\n+(target:null, opts:{}) => do\n+\n+  //opts\n+\n+\n+  colMeta: if(opts.has(\"colMeta\")) true else false\n+  show: if(opts.has(\"show\")) true else false\n+\n+\n+  queries: [\n+    /////////////////////// AHUS /////////////////////////////////\n+    //temps\n+              {target:\"oat_sensor\",                  query:\"temp and air and point and outside and sensor\",                            colMeta:{color:\"#00b843\"} },\n+              {target:\"dat_sensor\",                  query:\"temp and air and point and discharge and sensor and not leaving and not entering\", colMeta:{color:\"#0547fc\"} },\n+              {target:\"dat_sp\",                      query:\"temp and air and point and discharge and sp\",                              colMeta:{color:\"#5cb3ff\"} },\n+              {target:\"mat_sensor\",                  query:\"temp and air and point and mixed and sensor\",                              colMeta:{color:\"#FFD580\"} },\n+              {target:\"mat_sp\",                      query:\"temp and air and point and mixed and sp\",                                  colMeta:{color:\"#FFA500\"} },\n+              {target:\"eat_sensor\",                  query:\"temp and air and point and exhaust and sensor\",                            colMeta:{color:null} },\n+              {target:\"rat_sensor\",                  query:\"temp and air and point and return and sensor\",                             colMeta:{color:\"#FF0000\"} },\n+              {target:\"zntClg_sp\",                   query:\"temp and air and point and effective and zone and cooling and sp\",         colMeta:{color:\"#0547fc\"} },\n+              {target:\"zntHtg_sp\",                   query:\"temp and air and point and effective and zone and heating and sp\",         colMeta:{color:\"#FF0000\"} },\n+              {target:\"znt_sensor\",                  query:\"temp and air and point and zone and sensor\",                               colMeta:{color:\"#00b843\"} },\n+              {target:\"znt_sp\",                      query:\"temp and air and point and zone and sp and not heating and not cooling\",   colMeta:{color:\"#71bd8d\"} },\n+\n+    //pressures\n+              {target:\"dsp_sensor\",                  query:\"pressure and air and point and discharge and sensor and not filter\"       ,colMeta:{color:null} },\n+              {target:\"dsp_sp\",                      query:\"pressure and air and point and discharge and sp and not filter\"           ,colMeta:{color:null} },\n+              {target:\"bldg_press_sensor\",           query:\"air and building and point and pressure and sensor\"                       ,colMeta:{color:null} },\n+              {target:\"bldg_press_sp\",               query:\"air and building and point and pressure and sp\"                           ,colMeta:{color:null} },\n+    //Flow\n+              {target:\"daFlow_sensor\",               query:\"flow and air and point and discharge and sensor\"                          ,colMeta:{color:null} },\n+              {target:\"daFlow_sp\",                   query:\"flow and air and point and discharge and sp\"                              ,colMeta:{color:null} },\n+              {target:\"eaFlow_sensor\",               query:\"flow and air and point and exhaust and sensor\"                            ,colMeta:{color:null} },\n+              {target:\"eaFlow_sp\",                   query:\"flow and air and point and exhaust and sp\"                                ,colMeta:{color:null} },\n+              {target:\"raFlow_sensor\",               query:\"flow and air and point and return and sensor\"                             ,colMeta:{color:null} },\n+              {target:\"raFlow_sp\",                   query:\"flow and air and point and return and sp\"                                 ,colMeta:{color:null} },\n+              {target:\"znflow_sp\",                   query:\"flow and air and point and discharge and sp\"                              ,colMeta:{color:null} },\n+              {target:\"znflow_sensor\",               query:\"flow and air and point and discharge and sensor\"                          ,colMeta:{color:null} },\n+              {target:\"tot_daFlow_sensor\",           query:\"flow and air and point and discharge and sensor and total\"                ,colMeta:{color:null} },\n+              {target:\"tot_eaFlow_sensor\",           query:\"flow and air and point and exhaust and sensor and total\"                  ,colMeta:{color:null} },\n+              {target:\"znDiffFlow_sensor\",           query:\"flow and air and point and delta and zone\"                                ,colMeta:{color:null} },\n+    //valves\n+              {target:\"htgValve_cmd\",                query:\"point and cmd and valve and water and (reheat or hot)\"                    ,colMeta:{color:null} },\n+              {target:\"clgValve_cmd\",                query:\"point and water and (cool or chilled) and cmd and valve\"                  ,colMeta:{color:null} },\n+              {target:\"phtValve_cmd\",                query:\"point and preheat and cmd and valve\"                                      ,colMeta:{color:null} },\n+    //dampers\n+              {target:\"oaDmpr_cmd\",                  query:\"damper and air and point and outside and cmd\"                             ,colMeta:{color:null} },\n+              {target:\"maDmpr_cmd\",                  query:\"damper and air and point and mixed and cmd\"                               ,colMeta:{color:null} },\n+              {target:\"eaDmpr_cmd\",                  query:\"damper and air and point and exhaust and cmd\"                             ,colMeta:{color:null} },\n+              {target:\"raDmpr_cmd\",                  query:\"damper and air and point and return and cmd\"                              ,colMeta:{color:null} },\n+              {target:\"rlfDmpr_cmd\",                 query:\"damper and air and point and relief and cmd\"                              ,colMeta:{color:null} },\n+              {target:\"dmpr_cmd\",                    query:\"damper and air and point and cmd\"                                         ,colMeta:{color:null} },\n+    //fans\n+              {target:\"rtnFan_speed\",                query:\"return and point and fan and speed and cmd\"                               ,colMeta:{color:null} },\n+              {target:\"rtnFan_cmd\",                  query:\"return and point and fan and run and cmd\"                                 ,colMeta:{color:null} },\n+              {target:\"rlfFan_speed\",                query:\"relief and point and fan and speed and cmd\"                               ,colMeta:{color:null} },\n+              {target:\"rlfFan_cmd\",                  query:\"relief and point and fan and run and cmd\"                                 ,colMeta:{color:null} },\n+              {target:\"exhFan_speed\",                query:\"exhaust and point and fan and speed and cmd\"                              ,colMeta:{color:null} },\n+              {target:\"daFan_speed\",                 query:\"discharge and point and fan and speed and cmd\"                            ,colMeta:{color:null} },\n+              {target:\"daFan_run\",                   query:\"discharge and point and fan and run and cmd\"                              ,colMeta:{color:null} },\n+\n+    //humidity\n+              {target:\"daHumid_sensor\",              query:\"point and sensor and discharge and air and humidity\"                      ,colMeta:{color:null} },\n+              {target:\"humidifier_cmd\",              query:\"point and air and cmd and humidifier and not output\"                      ,colMeta:{color:null} },\n+    //lab\n+              {target:\"savFlow_sensor\",              query:\"(discharge or supply) and point and flow and sensor\"},\n+    //other\n+              {target:\"occ_cmd\",                     query:\"occupied and point and cmd\"},\n+              {target:\"znOcc_cmd\",                   query:\"occupied and point and cmd\"},\n+              {target:\"labOcc_cmd\",                  query:\"occupied and point and cmd and lab\"},\n+              {target:\"fumeHoodOcc_cmd\",             query:\"occupied and point and cmd and fumeHood\"},\n+              {target:\"siteOcc_cmd\",                 query:\"occupied and sitePoint\"},\n+              {target:\"ahu_state\",                   query:\"point and (ahu or ahuState)\"},\n+    //dual duct\n+              {target:\"hotDeck_daFlow_sensor\",       query:\"hotDeck and flow and air and point and discharge and sensor\"},\n+              {target:\"hotDeck_daFlow_sp\",           query:\"hotDeck and flow and air and point and discharge and sp\"},\n+              {target:\"hotDeck_dmpr_cmd\",            query:\"hotDeck and damper and air and point and cmd\"},\n+              {target:\"hotDeck_dmpr_reset\",          query:\"hotDeck and damper and air and point and cmd and reset\"},\n+\n+              {target:\"coldDeck_daFlow_sensor\",      query:\"coldDeck and flow and air and point and discharge and sensor\"},\n+              {target:\"coldDeck_daFlow_sp\",          query:\"coldDeck and flow and air and point and discharge and sp\"},\n+              {target:\"coldDeck_dmpr_cmd\",           query:\"coldDeck and damper and air and point and cmd\"},\n+              {target:\"coldDeck_dmpr_reset\",         query:\"coldDeck and damper and air and point and cmd and reset\"},\n+\n+    //buildingHealth\n+              {target:\"site_eui\",                     query:\"point and eui and equipRef->buildingHealth\"},\n+              {target:\"other_eui\",                    query:\"point and other and equipRef->buildingHealth\"},\n+              {target:\"chw_eui\",                      query:\"point and chilledWater and euiContributor and equipRef->buildingHealth\"},\n+              {target:\"elec_eui\",                     query:\"point and elec and euiContributor and equipRef->buildingHealth\"},\n+              {target:\"hw_eui\",                       query:\"point and hotWater and euiContributor and equipRef->buildingHealth\"},\n+              {target:\"naturalGas_eui\",               query:\"point and naturalGas and euiContributor and equipRef->buildingHealth\"},\n+              {target:\"natGas_eui\",                   query:\"point and naturalGas and euiContributor and equipRef->buildingHealth\"},\n+              {target:\"site_eci\",                     query:\"point and eci and equipRef->buildingHealth\"},\n+              {target:\"site_uncomfortableVAVs\",       query:\"point and vavScorePt and uncomfortable and equipRef->buildingHealth\"},\n+              {target:\"site_comfortableVAVs\",         query:\"point and vavScorePt and comfortable and equipRef->buildingHealth\"},\n+              {target:\"site_equipLife\",               query:\"point and equipmentLife and equipRef->buildingHealth\"},\n+              {target:\"site_buildingComfort\",         query:\"point and buildingScore and comfort and equipRef->buildingHealth\"},\n+              {target:\"site_buildingArea\",            query:\"point and buildingArea and equipRef->buildingHealth\"},\n+    //elec meter\n+              {target:\"elec_intervalPwr\",            query:\"elec and power and interval and equipRef->siteMeter and not cost and not modelPoint  and not syntheticPoint and not savings\"},\n+              {target:\"elec_modelPwr\",               query:\"elec and power and calculated and modelHisFunction and modelPoint and interval and equipRef->siteMeter\"},\n+              {target:\"elec_monthlyEnergy\",          query:\"elec and energy and monthly and not cost and not rateCost and equipRef->siteMeter and not modelPoint and not savings\"},\n+              {target:\"elec_modelEnergy\",            query:\"elec and energy and monthly and equipRef->siteMeter and modelPoint\"},\n+              {target:\"elec_monthlyCost\",            query:\"elec and energy and monthly and equipRef->siteMeter and cost\"},\n+              {target:\"elec_intervalCost\",           query:\"elec and power and interval and equipRef->siteMeter and cost and not savings\"},\n+              {target:\"elec_monthlyRateCost\",        query:\"elec and energy and monthly and equipRef->siteMeter and rateCost\"},\n+              {target:\"elec_intervalSavingsCost\",    query:\"elec and power and interval and equipRef->siteMeter and savings and cost\"},\n+              {target:\"elec_savingsPwr\",             query:\"elec and power and interval and equipRef->siteMeter and savings and not cost\"},\n+              {target:\"elec_monthlySavingsCost\",     query:\"elec and energy and monthly and equipRef->siteMeter and savings and cost\"},\n+              {target:\"elec_savingsEnergy\",          query:\"elec and energy and monthly and equipRef->siteMeter and savings and not cost\"},\n+              {target:\"elec_genAIModelDemand\",       query:\"elec and power and syntheticPoint and mlPoint and interval and equipRef->siteMeter\"},\n+    //natGas meter\n+              {target:\"natGas_intervalCons\",         query:\"naturalGas and consumption and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings\"},\n+              {target:\"natGas_intervalModelCons\",    query:\"naturalGas and consumption and calculated and modelHisFunction and modelPoint and interval and equipRef->siteMeter\"},\n+              {target:\"natGas_monthlyCons\",          query:\"naturalGas and consumption and monthly and not cost and not rateCost and equipRef->siteMeter and not modelPoint and not savings\"},\n+              {target:\"natGas_monthlyModelCons\",     query:\"naturalGas and consumption and monthly and equipRef->siteMeter and modelPoint\"},\n+              {target:\"natGas_monthlyCost\",          query:\"naturalGas and consumption and monthly and equipRef->siteMeter and cost\"},\n+              {target:\"natGas_intervalCost\",         query:\"naturalGas and consumption and interval and equipRef->siteMeter and cost and not savings\"},\n+              {target:\"natGas_monthlyRateCost\",      query:\"naturalGas and consumption and monthly and equipRef->siteMeter and rateCost\"},\n+              {target:\"natGas_intervalSavingsCost\",  query:\"naturalGas and consumption and interval and equipRef->siteMeter and savings and cost\"},\n+              {target:\"natGas_intervalSavingsCons\",  query:\"naturalGas and consumption and interval and equipRef->siteMeter and savings and not cost\"},\n+              {target:\"natGas_monthlySavingsCost\",   query:\"naturalGas and consumption and monthly and equipRef->siteMeter and savings and cost\"},\n+              {target:\"natGas_monthlySavingsCons\",   query:\"naturalGas and consumption and monthly and equipRef->siteMeter and savings and not cost\"},\n+              {target:\"natGas_genAIModelCons\",       query:\"naturalGas and consumption and interval and syntheticPoint and mlPoint and equipRef->siteMeter\"},\n+    //hot water meter\n+              {target:\"hotWater_intervalCons\",         query:\"hot and water and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings\"},\n+              {target:\"hotWater_intervalModelCons\",    query:\"hot and water and calculated and modelHisFunction and modelPoint and interval and equipRef->siteMeter\"},\n+              {target:\"hotWater_monthlyCons\",          query:\"hot and water and monthly and not cost and not rateCost and equipRef->siteMeter and not modelPoint and not savings\"},\n+              {target:\"hotWater_monthlyModelCons\",     query:\"hot and water and monthly and equipRef->siteMeter and modelPoint\"},\n+              {target:\"hotWater_monthlyCost\",          query:\"hot and water and monthly and equipRef->siteMeter and cost\"},\n+              {target:\"hotWater_intervalCost\",         query:\"hot and water and interval and equipRef->siteMeter and not savings and cost\"},\n+              {target:\"hotWater_monthlyRateCost\",      query:\"hot and water and monthly and equipRef->siteMeter and rateCost\"},\n+              {target:\"hotWater_intervalSavingsCost\",  query:\"hot and water and interval and equipRef->siteMeter and savings and cost\"},\n+              {target:\"hotWater_intervalSavingsCons\",  query:\"hot and water and interval and equipRef->siteMeter and savings and not cost\"},\n+              {target:\"hotWater_monthlySavingsCost\",   query:\"hot and water and monthly and equipRef->siteMeter and savings and cost\"},\n+              {target:\"hotWater_monthlySavingsCons\",   query:\"hot and water and monthly and equipRef->siteMeter and savings and not cost\"},\n+              {target:\"hotWater_genAIModelCons\",       query:\"hot and water and interval and syntheticPoint and mlPoint and equipRef->siteMeter\"},\n+    //chilled water meter\n+              {target:\"chilledWater_intervalCons\",         query:\"chilled and water and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings\"},\n+              {target:\"chilledWater_intervalModelCons\",    query:\"chilled and water and calculated and modelHisFunction and modelPoint and interval and equipRef->siteMeter\"},\n+              {target:\"chilledWater_monthlyCons\",          query:\"chilled and water and monthly and not cost and not rateCost and equipRef->siteMeter and not modelPoint and not savings\"},\n+              {target:\"chilledWater_monthlyModelCons\",     query:\"chilled and water and monthly and equipRef->siteMeter and modelPoint\"},\n+              {target:\"chilledWater_monthlyCost\",          query:\"chilled and water and monthly and equipRef->siteMeter and cost\"},\n+              {target:\"chilledWater_intervalCost\",         query:\"chilled and water and interval and equipRef->siteMeter and cost and not savings\"},\n+              {target:\"chilledWater_monthlyRateCost\",      query:\"chilled and water and monthly and equipRef->siteMeter and rateCost\"},\n+              {target:\"chilledWater_intervalSavingsCost\",  query:\"chilled and water and interval and equipRef->siteMeter and savings and cost\"},\n+              {target:\"chilledWater_intervalSavingsCons\",  query:\"chilled and water and interval and equipRef->siteMeter and savings and not cost\"},\n+              {target:\"chilledWater_monthlySavingsCost\",   query:\"chilled and water and monthly and equipRef->siteMeter and savings and cost\"},\n+              {target:\"chilledWater_monthlySavingsCons\",   query:\"chilled and water and monthly and equipRef->siteMeter and savings and not cost\"},\n+              {target:\"chilledWater_genAIModelCons\",       query:\"chilled and water and interval and syntheticPoint and mlPoint and equipRef->siteMeter\"},\n+    //steam meter\n+              {target:\"steam_intervalCons\",         query:\"steam and interval and equipRef->siteMeter and not cost and not modelPoint and not syntheticPoint and not savings\"},\n+              {target:\"steam_intervalModelCons\",    query:\"steam and calculated and modelHisFunction and modelPoint and interval and equipRef->siteMeter\"},\n+              {target:\"steam_monthlyCons\",          query:\"steam and monthly and not cost and not rateCost and equipRef->siteMeter and not modelPoint and not savings\"},\n+              {target:\"steam_monthlyModelCons\",     query:\"steam and monthly and equipRef->siteMeter and modelPoint\"},\n+              {target:\"steam_monthlyCost\",          query:\"steam and monthly and equipRef->siteMeter and cost\"},\n+              {target:\"steam_intervalCost\",         query:\"steam and interval and equipRef->siteMeter and cost and not savings\"},\n+              {target:\"steam_monthlyRateCost\",      query:\"steam and monthly and equipRef->siteMeter and rateCost\"},\n+              {target:\"steam_intervalSavingsCost\",  query:\"steam and interval and equipRef->siteMeter and savings and cost\"},\n+              {target:\"steam_intervalSavingsCons\",  query:\"steam and interval and equipRef->siteMeter and savings and not cost\"},\n+              {target:\"steam_monthlySavingsCost\",   query:\"steam and monthly and equipRef->siteMeter and savings and cost\"},\n+              {target:\"steam_monthlySavingsCons\",   query:\"steam and monthly and equipRef->siteMeter and savings and not cost\"},\n+              {target:\"steam_genAIModelCons\",       query:\"steam and interval and syntheticPoint and mlPoint and equipRef->siteMeter\"},\n+    //water meter\n+              {target:\"water_intervalCons\",         query:\"water and not chilled and not hot and consumption and interval and equipRef->siteMeter and not cost and not modelPoint and not savings\"},\n+\n+    //hot water\n+              {target:\"hw_delta_press\",             query:\"hot and water and pressure and delta and sensor\"},\n+              {target:\"hw_flow\",                    query:\"hot and water and flow and sensor\"},\n+              {target:\"hw_pri_pump_speed\",          query:\"hot and water and pump and speed and cmd and primary\"},\n+              {target:\"hw_pri_pump_status\",         query:\"hot and water and pump and run and cmd and primary\"},\n+              {target:\"hw_sec_pump_speed\",          query:\"hot and water and pump and speed and cmd and secondary\"},\n+              {target:\"hw_sec_pump_status\",         query:\"hot and water and pump and run and cmd and secondary\"},\n+              {target:\"hw_pri_pump_power\",          query:\"hot and water and pump and power and sensor and primary\"},\n+              {target:\"hw_sec_pump_power\",          query:\"hot and water and pump and power and sensor and secondary\"},\n+              {target:\"hw_supply_temp\",             query:\"hot and water and supply and temp and sensor\"},\n+              {target:\"hw_return_temp\",             query:\"hot and water and return and temp and sensor and not domestic\"},\n+              {target:\"hw_supply_temp_sp\",          query:\"hot and water and supply and temp and sp\"},\n+              {target:\"hw_delta_press_sp\",          query:\"hot and water and pressure and delta and sp\"},\n+              {target:\"hw_delta_temp\",              query:\"hot and water and temp and delta and sensor\"},\n+              {target:\"hw_sys_load\",                query:\"hot and water and load and energy\"},\n+              {target:\"hw_bypass_valve\",            query:\"hot and water and bypass and valve and cmd\"},\n+\n+    //chilled water\n+              {target:\"chw_delta_press\",             query:\"chilled and water and pressure and delta and sensor\"},\n+              {target:\"chw_flow\",                    query:\"chilled and water and flow and sensor and not bypass\"},\n+              {target:\"chw_pri_pump_speed\",          query:\"chilled and water and pump and speed and cmd and primary\"},\n+              {target:\"chw_sec_pump_speed\",          query:\"chilled and water and pump and speed and cmd and secondary\"},\n+              {target:\"chw_pri_pump_status\",         query:\"chilled and water and pump and run and cmd and primary\"},\n+              {target:\"chw_sec_pump_status\",         query:\"chilled and water and pump and run and cmd and secondary\"},\n+              {target:\"chw_pri_pump_power\",          query:\"chilled and water and pump and power and sensor and primary\"},\n+              {target:\"chw_sec_pump_power\",          query:\"chilled and water and pump and power and sensor and secondary\"},\n+            //{target:\"chw_pri_pump_status\",         query:\"chilled and water and pump and run and cmd and primary\"},\n+            //{target:\"chw_sec_pump_status\",         query:\"chilled and water and pump and run and cmd and secondary\"},\n+              {target:\"chw_supply_temp\",             query:\"chilled and water and supply and temp and sensor\"},\n+              {target:\"chw_return_temp\",             query:\"chilled and water and return and temp and sensor\"},\n+              {target:\"chw_supply_temp_sp\",          query:\"chilled and water and supply and temp and sp\"},\n+              {target:\"chw_delta_press_sp\",          query:\"chilled and water and pressure and delta and sp\"},\n+              {target:\"chw_delta_temp\",              query:\"chilled and water and temp and delta and sensor\"},\n+              {target:\"chw_sys_load\",                query:\"chilled and water and load and energy\"},\n+\n+     //General Pump Points\n+              {target:\"pri_pump_speed\",              query:\"water and pump and speed and cmd and primary\"},\n+              {target:\"sec_pump_speed\",              query:\"water and pump and speed and cmd and secondary\"},\n+              {target:\"pri_pump_status\",             query:\"water and pump and run and cmd and primary\"},\n+              {target:\"sec_pump_status\",             query:\"water and pump and run and cmd and secondary\"},\n+              {target:\"pri_pump_power\",              query:\"water and pump and power and sensor and primary\"},\n+              {target:\"sec_pump_power\",              query:\"water and pump and power and sensor and secondary\"},\n+\n+     //chillers\n+              {target:\"chlr_status\",                   query:\"chiller and cmd and run\"},\n+              {target:\"chlr_power\",                    query:\"chiller and power and sensor\"},\n+              {target:\"chlr_entering_temp\",            query:\"chiller and chilled and entering and temp and water and sensor and not condenser\"},\n+              {target:\"chlr_leaving_temp\",             query:\"chiller and chilled and leaving and temp and water and sensor and not condenser\"},\n+              {target:\"chlr_flow\",                     query:\"chiller and chilled and flow and water and sensor\"},\n+              {target:\"chlr_fla\",                      query:\"chiller and fla and sensor\"},\n+              {target:\"chlr_cw_entering_temp\",         query:\"chiller and condenser and entering and temp and water and sensor\"},\n+              {target:\"chlr_cw_leaving_temp\",          query:\"chiller and condenser and leaving and temp and water and sensor\"},\n+\n+     //boilers\n+              {target:\"blr_firing_rate\",              query:\"boiler and firingRate and sensor\"},\n+              {target:\"blr_entering_temp\",            query:\"boiler and hot and (entering or return) and temp and water and sensor\"},\n+              {target:\"blr_leaving_temp\",             query:\"boiler and hot and leaving and temp and water and sensor\"},\n+              {target:\"blr_leaving_temp_sp\",          query:\"boiler and hot and leaving and temp and water and sp\"},\n+              {target:\"blr_run_status\",               query:\"boiler and run and cmd\"},\n+              {target:\"blr_enable\",                   query:\"boiler and cmd and enable\"},\n+              {target:\"blr_gas_flow_rate\",            query:\"boiler and flow and rate and sensor and naturalGas\"},\n+              {target:\"blr_flow\",                     query:\"boiler and notImplemented\"},\n+              {target:\"blr_power\",                    query:\"boiler and power and sensor\"},\n+\n+\n+     //cooling tower\n+              {target:\"clgtwr_leaving_temp\",         query:\"coolingTower and temp and (leaving or supply) and water and sensor\"},\n+              {target:\"clgtwr_entering_temp\",        query:\"coolingTower and temp and (entering or return) and water and sensor\"},\n+              {target:\"clgtwr_fan_speed\",            query:\"coolingTower and fan and speed and air and cmd\"},\n+              {target:\"clgtwr_power\",                query:\"coolingTower and power and sensor\"},\n+              {target:\"clgtwr_fan_status\",           query:\"coolingTower and cmd and run\"},\n+\n+\n+     //condenser water\n+              {target:\"cws_supply_temp\",             query:\"condenser and water and supply and temp and sensor\"},\n+              {target:\"cws_supply_temp_sp\",          query:\"condenser and water and supply and temp and sp\"},\n+              {target:\"cws_return_temp\",             query:\"condenser and water and return and temp and sensor\"},\n+\n+     //miscellaneous\n+              {target:\"wet_bulb\",                    query:\"temp and wetBulb\"},\n+              {target:\"oah_sensor\",                  query:\"humidity and air and point and outside and sensor\"},\n+\n+     // Heat Recovery Coil\n+       // Discharge\n+              {target:\"disHRC_ea_sensor\",            query:\"heatRecovery and coil and air and temp and sensor and discharge and entering\"},\n+              {target:\"disHRC_la_sensor\",            query:\"heatRecovery and coil and air and temp and sensor and discharge and leaving\"},\n+              {target:\"disHRC_flow_sensor\",          query:\"heatRecovery and coil and air and flow and sensor and discharge\"},\n+              {target:\"disHRC_valve_cmd\",            query:\"heatRecovery and coil and water and valve and cmd and discharge\"},\n+       // Exhaust\n+              {target:\"exHRC_ea_sensor\",             query:\"heatRecovery and coil and air and temp and sensor and exhaust and entering\"},\n+              {target:\"exHRC_la_sensor\",             query:\"heatRecovery and coil and air and temp and sensor and exhaust and leaving\"},\n+              {target:\"exHRC_flow_sensor\",           query:\"heatRecovery and coil and air and flow and sensor and exhaust\"},\n+              {target:\"exHRC_valve_cmd\",             query:\"heatRecovery and coil and water and valve and cmd and exhaust\"},\n+\n+     // Heat Recovery Wheel\n+              {target:\"disHRW_ea_sensor\",            query:\"heatRecovery and wheel and air and temp and sensor and discharge and entering\"},\n+              {target:\"disHRW_la_sensor\",            query:\"heatRecovery and wheel and air and temp and sensor and discharge and leaving\"},\n+              {target:\"disHRW_flow_sensor\",          query:\"heatRecovery and wheel and air and flow and sensor and discharge\"},\n+\n+              {target:\"exHRW_ea_sensor\",             query:\"heatRecovery and wheel and air and temp and sensor and exhaust and entering\"},\n+              {target:\"exHRW_la_sensor\",             query:\"heatRecovery and wheel and air and temp and sensor and exhaust and leaving\"},\n+              {target:\"exHRW_flow_sensor\",           query:\"heatRecovery and wheel and air and flow and sensor and exhaust\"},\n+\n+              {target:\"hrw_status\",                  query:\"heatRecovery and wheel and sensor and run\"},\n+              {target:\"hrw_cmd\",                     query:\"heatRecovery and wheel and cmd\"},\n+\n+     // Lab Spaces\n+              {target:\"ach\",                         query:\"ach and air and sensor\"},\n+              {target:\"sash_cmd\",                    query:\"sash and cmd and fumeHood\"},\n+\n+     //Coil Temps\n+             //hot\n+             {target:\"chw_entg_temp\",                query:\"chilled and water and valve\"},\n+             {target:\"chw_lvg_temp\",                 query:\"humidity and air and point and outside and sensor\"},\n+\n+             //chilled\n+             {target:\"hw_entg_temp\",                 query:\"temp and wetBulb\"},\n+             {target:\"hw_lvg_temp\",                  query:\"humidity and air and point and outside and sensor\"},\n+\n+     //Baseline Search\n+             {target:\"baseline_calc\",                query:\"point and calculated and modelPoint and not cost and not savings\"},\n+\n+     //Utility Level Points for Dashboards\n+             //Elec\n+             {target:\"portfolioDashboard_elecUsage\",        query:\"portfolioDashboardPoint and elec and not cost and not savings\"},\n+             {target:\"portfolioDashboard_elecCost\",         query:\"portfolioDashboardPoint and elec and cost and not savings\"},\n+             {target:\"portfolioDashboard_elecSavingsCost\",  query:\"portfolioDashboardPoint and elec and cost and savings\"},\n+             {target:\"portfolioDashboard_elecSavings\",      query:\"portfolioDashboardPoint and elec and usage and savings and not cost\"},\n+\n+             //Steam\n+             {target:\"portfolioDashboard_steamUsage\",        query:\"portfolioDashboardPoint and steam and not cost and not savings\"},\n+             {target:\"portfolioDashboard_steamCost\",         query:\"portfolioDashboardPoint and steam and cost and not savings\"},\n+             {target:\"portfolioDashboard_steamSavingsCost\",  query:\"portfolioDashboardPoint and steam and cost and savings\"},\n+             {target:\"portfolioDashboard_steamSavings\",      query:\"portfolioDashboardPoint and steam and usage and savings and not cost\"},\n+\n+             //Chilled Water\n+             {target:\"portfolioDashboard_chwUsage\",        query:\"portfolioDashboardPoint and chw and not cost and not savings\"},\n+             {target:\"portfolioDashboard_chwCost\",         query:\"portfolioDashboardPoint and chw and cost and not savings\"},\n+             {target:\"portfolioDashboard_chwSavingsCost\",  query:\"portfolioDashboardPoint and chw and cost and savings\"},\n+             {target:\"portfolioDashboard_chwSavings\",      query:\"portfolioDashboardPoint and chw and usage and savings and not cost\"},\n+\n+             //Hot Water\n+             {target:\"portfolioDashboard_hwUsage\",        query:\"portfolioDashboardPoint and hw and not cost and not savings\"},\n+             {target:\"portfolioDashboard_hwCost\",         query:\"portfolioDashboardPoint and hw and cost and not savings\"},\n+             {target:\"portfolioDashboard_hwSavingsCost\",  query:\"portfolioDashboardPoint and hw and cost and savings\"},\n+             {target:\"portfolioDashboard_hwSavings\",      query:\"portfolioDashboardPoint and hw and usage and savings and not cost\"},\n+           ].toGrid\n+\n+\n+  if(show) return queries\n+\n+  match: queries.find(r => r.get(\"target\")==target)\n+  if (match.isNull) throw \"Cannot find query: \"+target\n+\n+  out: if (colMeta) match.get(\"colMeta\")\n+       else         match.get(\"query\")\n+\n+  return out\n end"
    },
    {
      "name": "setIgnoredTags",
      "pod": "kwLinkegratorExt",
      "pod_src": "() => do\n  cipherText :\n  \"\"\"AxD:Yxba0aIY9Y5BKvFonyFhAQ::BtgrCvguDM6BZW0ICtmF0cD4evFw471bebhtHhIy1K8RTeLq6tL0\n     SJr_hQfimBgLDmfLzNoOdBZ2-SVOsFjM9oz5iYgXoxAPfxoLWDlTRgS-bigO-p1P0hSeVxJ1yEoyVvx3\n     NnLH1R6QO4AtEgHgk6T6GGRbTwPwBPoAcOJ233Fdj_wKuiOFsuCLCG5YJ_e6A6uAhre_POD25IjZpNej\n     3WMNzKuTOr6oamZQs4aS4yqXPS7bddsCzncgbn0YJaOR6mIBLdT8PbY1BnSoV0LIjwa8IVemqeVLF89v\n     bkfHFw0h9HD-j3lOxjpE_0voXJzI\"\"\"\n  keyFunc : () => \"uF_n7EQ34krJ1dsJbt7y5w\"\n  return afAxonEncryptorRt_callFunc(\"setIgnoredTags\", cipherText, [], keyFunc)\nend",
      "local_src": "() => do\n  [\"clarificationNeeded\",\"manualReview\",\"clarificationComment\",\"clarificationComment_kw\",\"notDuplicate\",\"g36DashReady\",\"emcsPoint\"]\nend",
      "diff": "@@ -1,10 +1,3 @@ () => do\n-  cipherText :\n-  \"\"\"AxD:Yxba0aIY9Y5BKvFonyFhAQ::BtgrCvguDM6BZW0ICtmF0cD4evFw471bebhtHhIy1K8RTeLq6tL0\n-     SJr_hQfimBgLDmfLzNoOdBZ2-SVOsFjM9oz5iYgXoxAPfxoLWDlTRgS-bigO-p1P0hSeVxJ1yEoyVvx3\n-     NnLH1R6QO4AtEgHgk6T6GGRbTwPwBPoAcOJ233Fdj_wKuiOFsuCLCG5YJ_e6A6uAhre_POD25IjZpNej\n-     3WMNzKuTOr6oamZQs4aS4yqXPS7bddsCzncgbn0YJaOR6mIBLdT8PbY1BnSoV0LIjwa8IVemqeVLF89v\n-     bkfHFw0h9HD-j3lOxjpE_0voXJzI\"\"\"\n-  keyFunc : () => \"uF_n7EQ34krJ1dsJbt7y5w\"\n-  return afAxonEncryptorRt_callFunc(\"setIgnoredTags\", cipherText, [], keyFunc)\n+  [\"clarificationNeeded\",\"manualReview\",\"clarificationComment\",\"clarificationComment_kw\",\"notDuplicate\",\"g36DashReady\",\"emcsPoint\"]\n end"
    },
    {
      "name": "siteMeter_retrieveRequiredVirtualPoints",
      "pod": "kwLinkVirtualExt",
      "pod_src": "/*\n*    Description :\n*     This function retrieves the point tags used for siteMeter virtualPoint creation\n*     filtering based on scope and utility type.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    siteMeter       - The siteMeter selected to generate points.\n*     Dict                   opts            - Options to return valid/missing/extra/tags points\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    1/10/2025       - Initial Creation\n*/\n(siteMeter, opts: {pointType: \"tags\"}) => do\n\n  // Normalize Inputs\n  smRec: siteMeter.toRec\n  smId: smRec.get(\"id\")\n\n  // Retrieve Data from Rec\n  meterScope : smRec.get(\"scope\")\n  type       : if      (smRec.has(\"elec\"))                                                   \"elec\"\n               else if (smRec.has(\"gas\") or smRec.has(\"naturalGas\") or smRec.has(\"natGas\"))  \"gas\"\n               else                                                                          \"invalid\"\n\n  // Retrieve Required pointQueryTarget Tags\n  tags: if      (meterScope == \"interval\" and type==\"elec\") [\"elec_modelPwr\", \"elec_monthlyEnergy\", \"elec_monthlyCost\", \"elec_monthlyRateCost\", \"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n        else if (meterScope == \"interval\" and type==\"gas\")  [\"natGas_intervalModelCons\", \"natGas_monthlyCons\", \"natGas_monthlyCost\", \"natGas_monthlyRateCost\", \"natGas_intervalModelCons\", \"natGas_intervalCost\", \"natGas_intervalSavingsCost\", \"natGas_intervalSavingsCons\", \"natGas_intervalCons\"]\n        else if (meterScope == \"monthly\"  and type==\"elec\") [\"elec_modelEnergy\", \"elec_monthlyEnergy\", \"elec_monthlyCost\", \"elec_monthlyRateCost\", \"elec_modelEnergy\", \"elec_savingsEnergy\", \"elec_monthlySavingsCost\"]\n        else if (meterScope == \"monthly\"  and type==\"gas\")  [\"natGas_monthlyModelCons\", \"natGas_monthlyCons\", \"natGas_monthlyCost\", \"natGas_monthlyRateCost\", \"natGas_monthlyModelCons\", \"natGas_monthlySavingsCost\", \"natGas_monthlySavingsCons\"]\n        else                                                []\n\n  // Determine return value based on pointType opt.\n  if (opts.get(\"pointType\") == \"tags\")    return tags\n  if (opts.get(\"pointType\") == \"valid\")   return tags.map(point => if (pointQuery(point, {equipRef: smId}).isNonNull) pointQuery(point, {equipRef: smId}) else null).findAll(v => v.isNonNull).toGrid\n  if (opts.get(\"pointType\") == \"missing\") do\n    // Find Missing Points through the blueprint list and pointQueryLookups\n    missing: getVirtualBlueprintList().findAll(r => tags.contains(r.get(\"pointQueryTarget\")) and pointQuery(r.get(\"pointQueryTarget\"), {equipRef: smId}).isNull)\n                   .map(r=> r.set(\"tz\", smRec.get(\"siteRef\").toRec.get(\"tz\")))\n\n    // Add Model Point Missing Message\n    if      (meterScope == \"interval\" and type==\"elec\" and pointQuery(\"elec_modelPwr\", {equipRef: smId}).isNull) missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    else if (meterScope == \"interval\" and type==\"gas\" and pointQuery(\"natGas_intervalModelCons\", {equipRef: smId}).isNull)  missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    else if (meterScope == \"monthly\"  and type==\"elec\" and pointQuery(\"elec_modelEnergy\", {equipRef: smId}).isNull) missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    else if (meterScope == \"monthly\"  and type==\"gas\" and pointQuery(\"natGas_monthlyModelCons\", {equipRef: smId}).isNull)  missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    return missing.sortDis()\n  end\n  if (opts.get(\"pointType\") == \"extra\") return readAll(point and equipRef == smId).findAll(r => r.missing(\"pointQueryTarget\") or (r.has(\"pointQueryTarget\") and pointQuery(r.get(\"pointQueryTarget\"), {equipRef: smId}).isNull)).toGrid\n\n  // If none of the opts is called, return the opt key name and the valid values for that key.\n  return {info: \"Accepted \\\"pointType\\\" Opts: [tags, valid, missing, extra]\"}.toGrid.addColMeta(\"info\", {dis: \"Information\"})\nend",
      "local_src": "/*    \n*    Description :\n*     This function retrieves the point tags used for siteMeter virtualPoint creation\n*     filtering based on scope and utility type.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    siteMeter       - The siteMeter selected to generate points.\n*     Dict                   opts            - Options to return valid/missing/extra/tags points\n*\n*   Work :\n*     Who                    When            Change \n*     Thomas Kuhrke Limia    1/10/2025       - Initial Creation\n*/\n(siteMeter, opts: {pointType: \"tags\"}) => do\n  \n  // Normalize Inputs\n  smRec: siteMeter.toRec\n  smId: smRec.get(\"id\")\n  \n  // Retrieve Data from Rec\n  meterScope : smRec.get(\"scope\")\n  type       : if      (smRec.has(\"elec\"))                                                   \"elec\"\n               else if (smRec.has(\"gas\") or smRec.has(\"naturalGas\") or smRec.has(\"natGas\"))  \"gas\"\n               else if (smRec.has(\"hot\") and smRec.has(\"water\"))                             \"hotWater\"\n               else if (smRec.has(\"chilled\") and smRec.has(\"water\"))                         \"chilledWater\"\n               else if (smRec.has(\"steam\"))                                                  \"steam\"\n               else                                                                          \"invalid\"\n  \n  // Retrieve Required pointQueryTarget Tags \n  tags: if (meterScope == \"interval\" and type==\"elec\")               [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n        else  if (meterScope == \"interval\" and type==\"hotWater\")     [\"hotWater_intervalCons\", \"hotWater_intervalModelCons\", \"hotWater_intervalSavingsCons\", \"hotWater_intervalSavingsCost\", \"hotWater_intervalCost\"]\n        else  if (meterScope == \"interval\" and type==\"chilledWater\") [\"chilledWater_intervalCons\", \"chilledWater_intervalModelCons\", \"chilledWater_intervalSavingsCons\", \"chilledWater_intervalSavingsCost\", \"chilledWater_intervalCost\"]\n        else  if (meterScope == \"interval\" and type==\"steam\")        [\"steam_intervalCons\", \"steam_intervalModelCons\", \"steam_intervalSavingsCons\", \"steam_intervalSavingsCost\", \"steam_intervalCost\"]\n        else  if (meterScope == \"interval\" and type==\"gas\")          [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n        else  if (meterScope == \"monthly\"  and type==\"elec\")         [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n        else  if (meterScope == \"monthly\"  and type==\"hotWater\")     [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n        else  if (meterScope == \"monthly\"  and type==\"chilledWater\") [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n        else  if (meterScope == \"monthly\"  and type==\"steam\")        [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n        else  if (meterScope == \"monthly\"  and type==\"gas\")          [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n        else                                                         []\n\n  // Determine return value based on pointType opt.\n  if (opts.get(\"pointType\") == \"tags\")  return tags\n  if (opts.get(\"pointType\") == \"valid\") do\n    validRecs: tags.map(point => if (pointQuery(point, {equipRef: smId}).isNonNull) pointQuery(point, {equipRef: smId}) else null).findAll(v => v.isNonNull).toGrid\n    if(validRecs.isNonNull and validRecs.has(\"id\"))  validRecs = validRecs.unique(\"id\")\n    return validRecs\n  end\n  if (opts.get(\"pointType\") == \"missing\") do\n    // Find Missing Points through the blueprint list and pointQueryLookups\n    missing: getVirtualBlueprintList().findAll(r => tags.contains(r.get(\"pointQueryTarget\")) and pointQuery(r.get(\"pointQueryTarget\"), {equipRef: smId}).isNull)\n                   .map(r=> r.set(\"tz\", smRec.get(\"siteRef\").toRec.get(\"tz\")))\n    \n    // Add Model Point Missing Message\n    if (meterScope == \"interval\" and type==\"elec\" and pointQuery(\"elec_modelPwr\", {equipRef: smId}).isNull)                          missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    if (meterScope == \"interval\" and type==\"hotWater\" and pointQuery(\"hotWater_intervalModelCons\", {equipRef: smId}).isNull)         missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    if (meterScope == \"interval\" and type==\"chilledWater\" and pointQuery(\"chilledWater_intervalModelCons\", {equipRef: smId}).isNull) missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    if (meterScope == \"interval\" and type==\"steam\" and pointQuery(\"steam_intervalModelCons\", {equipRef: smId}).isNull)               missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    if (meterScope == \"interval\" and type==\"gas\" and pointQuery(\"natGas_intervalModelCons\", {equipRef: smId}).isNull)                missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    if (meterScope == \"monthly\"  and type==\"elec\" and pointQuery(\"elec_modelEnergy\", {equipRef: smId}).isNull)                       missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    if (meterScope == \"monthly\"  and type==\"hotWater\" and pointQuery(\"hotWater_monthlyModelCons\", {equipRef: smId}).isNull)          missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    if (meterScope == \"monthly\"  and type==\"chilledWater\" and pointQuery(\"chilledWater_monthlyModelCons\", {equipRef: smId}).isNull)  missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    if (meterScope == \"monthly\"  and type==\"steam\" and pointQuery(\"steam_monthlyModelCons\", {equipRef: smId}).isNull)                missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    if (meterScope == \"monthly\"  and type==\"gas\" and pointQuery(\"natGas_monthlyModelCons\", {equipRef: smId}).isNull)                 missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n    return missing.sortDis()\n  end\n  if (opts.get(\"pointType\") == \"extra\") do\n    validRecs: tags.map(point => if (pointQuery(point, {equipRef: smId}).isNonNull) pointQuery(point, {equipRef: smId}) else null).findAll(v => v.isNonNull).toGrid\n    extras: readAll(point and equipRef == smId).findAll(r => r.missing(\"pointQueryTarget\") or (r.has(\"pointQueryTarget\") and pointQuery(r.get(\"pointQueryTarget\"), {equipRef: smId}).isNull)).toGrid\n    if(validRecs.isNull or validRecs.isEmpty) return extras\n    extras = extras.findAll(r => not validRecs.colToList(\"id\").contains(r.get(\"id\")))\n    return extras\n  end\n  // If none of the opts is called, return the opt key name and the valid values for that key.\n  return {info: \"Accepted \\\"pointType\\\" Opts: [tags, valid, missing, extra]\"}.toGrid.addColMeta(\"info\", {dis: \"Information\"})\nend",
      "diff": "@@ -22,32 +22,56 @@   meterScope : smRec.get(\"scope\")\n   type       : if      (smRec.has(\"elec\"))                                                   \"elec\"\n                else if (smRec.has(\"gas\") or smRec.has(\"naturalGas\") or smRec.has(\"natGas\"))  \"gas\"\n+               else if (smRec.has(\"hot\") and smRec.has(\"water\"))                             \"hotWater\"\n+               else if (smRec.has(\"chilled\") and smRec.has(\"water\"))                         \"chilledWater\"\n+               else if (smRec.has(\"steam\"))                                                  \"steam\"\n                else                                                                          \"invalid\"\n \n   // Retrieve Required pointQueryTarget Tags\n-  tags: if      (meterScope == \"interval\" and type==\"elec\") [\"elec_modelPwr\", \"elec_monthlyEnergy\", \"elec_monthlyCost\", \"elec_monthlyRateCost\", \"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n-        else if (meterScope == \"interval\" and type==\"gas\")  [\"natGas_intervalModelCons\", \"natGas_monthlyCons\", \"natGas_monthlyCost\", \"natGas_monthlyRateCost\", \"natGas_intervalModelCons\", \"natGas_intervalCost\", \"natGas_intervalSavingsCost\", \"natGas_intervalSavingsCons\", \"natGas_intervalCons\"]\n-        else if (meterScope == \"monthly\"  and type==\"elec\") [\"elec_modelEnergy\", \"elec_monthlyEnergy\", \"elec_monthlyCost\", \"elec_monthlyRateCost\", \"elec_modelEnergy\", \"elec_savingsEnergy\", \"elec_monthlySavingsCost\"]\n-        else if (meterScope == \"monthly\"  and type==\"gas\")  [\"natGas_monthlyModelCons\", \"natGas_monthlyCons\", \"natGas_monthlyCost\", \"natGas_monthlyRateCost\", \"natGas_monthlyModelCons\", \"natGas_monthlySavingsCost\", \"natGas_monthlySavingsCons\"]\n-        else                                                []\n+  tags: if (meterScope == \"interval\" and type==\"elec\")               [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n+        else  if (meterScope == \"interval\" and type==\"hotWater\")     [\"hotWater_intervalCons\", \"hotWater_intervalModelCons\", \"hotWater_intervalSavingsCons\", \"hotWater_intervalSavingsCost\", \"hotWater_intervalCost\"]\n+        else  if (meterScope == \"interval\" and type==\"chilledWater\") [\"chilledWater_intervalCons\", \"chilledWater_intervalModelCons\", \"chilledWater_intervalSavingsCons\", \"chilledWater_intervalSavingsCost\", \"chilledWater_intervalCost\"]\n+        else  if (meterScope == \"interval\" and type==\"steam\")        [\"steam_intervalCons\", \"steam_intervalModelCons\", \"steam_intervalSavingsCons\", \"steam_intervalSavingsCost\", \"steam_intervalCost\"]\n+        else  if (meterScope == \"interval\" and type==\"gas\")          [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n+        else  if (meterScope == \"monthly\"  and type==\"elec\")         [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n+        else  if (meterScope == \"monthly\"  and type==\"hotWater\")     [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n+        else  if (meterScope == \"monthly\"  and type==\"chilledWater\") [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n+        else  if (meterScope == \"monthly\"  and type==\"steam\")        [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n+        else  if (meterScope == \"monthly\"  and type==\"gas\")          [\"elec_intervalPwr\", \"elec_modelPwr\", \"elec_savingsPwr\", \"elec_intervalSavingsCost\", \"elec_intervalCost\"]\n+        else                                                         []\n \n   // Determine return value based on pointType opt.\n-  if (opts.get(\"pointType\") == \"tags\")    return tags\n-  if (opts.get(\"pointType\") == \"valid\")   return tags.map(point => if (pointQuery(point, {equipRef: smId}).isNonNull) pointQuery(point, {equipRef: smId}) else null).findAll(v => v.isNonNull).toGrid\n+  if (opts.get(\"pointType\") == \"tags\")  return tags\n+  if (opts.get(\"pointType\") == \"valid\") do\n+    validRecs: tags.map(point => if (pointQuery(point, {equipRef: smId}).isNonNull) pointQuery(point, {equipRef: smId}) else null).findAll(v => v.isNonNull).toGrid\n+    if(validRecs.isNonNull and validRecs.has(\"id\"))  validRecs = validRecs.unique(\"id\")\n+    return validRecs\n+  end\n   if (opts.get(\"pointType\") == \"missing\") do\n     // Find Missing Points through the blueprint list and pointQueryLookups\n     missing: getVirtualBlueprintList().findAll(r => tags.contains(r.get(\"pointQueryTarget\")) and pointQuery(r.get(\"pointQueryTarget\"), {equipRef: smId}).isNull)\n                    .map(r=> r.set(\"tz\", smRec.get(\"siteRef\").toRec.get(\"tz\")))\n \n     // Add Model Point Missing Message\n-    if      (meterScope == \"interval\" and type==\"elec\" and pointQuery(\"elec_modelPwr\", {equipRef: smId}).isNull) missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n-    else if (meterScope == \"interval\" and type==\"gas\" and pointQuery(\"natGas_intervalModelCons\", {equipRef: smId}).isNull)  missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n-    else if (meterScope == \"monthly\"  and type==\"elec\" and pointQuery(\"elec_modelEnergy\", {equipRef: smId}).isNull) missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n-    else if (meterScope == \"monthly\"  and type==\"gas\" and pointQuery(\"natGas_monthlyModelCons\", {equipRef: smId}).isNull)  missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n+    if (meterScope == \"interval\" and type==\"elec\" and pointQuery(\"elec_modelPwr\", {equipRef: smId}).isNull)                          missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n+    if (meterScope == \"interval\" and type==\"hotWater\" and pointQuery(\"hotWater_intervalModelCons\", {equipRef: smId}).isNull)         missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n+    if (meterScope == \"interval\" and type==\"chilledWater\" and pointQuery(\"chilledWater_intervalModelCons\", {equipRef: smId}).isNull) missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n+    if (meterScope == \"interval\" and type==\"steam\" and pointQuery(\"steam_intervalModelCons\", {equipRef: smId}).isNull)               missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n+    if (meterScope == \"interval\" and type==\"gas\" and pointQuery(\"natGas_intervalModelCons\", {equipRef: smId}).isNull)                missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n+    if (meterScope == \"monthly\"  and type==\"elec\" and pointQuery(\"elec_modelEnergy\", {equipRef: smId}).isNull)                       missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n+    if (meterScope == \"monthly\"  and type==\"hotWater\" and pointQuery(\"hotWater_monthlyModelCons\", {equipRef: smId}).isNull)          missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n+    if (meterScope == \"monthly\"  and type==\"chilledWater\" and pointQuery(\"chilledWater_monthlyModelCons\", {equipRef: smId}).isNull)  missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n+    if (meterScope == \"monthly\"  and type==\"steam\" and pointQuery(\"steam_monthlyModelCons\", {equipRef: smId}).isNull)                missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n+    if (meterScope == \"monthly\"  and type==\"gas\" and pointQuery(\"natGas_monthlyModelCons\", {equipRef: smId}).isNull)                 missing = missing.addRow({virtualPointCategory: \"Missing Model Point\"})\n     return missing.sortDis()\n   end\n-  if (opts.get(\"pointType\") == \"extra\") return readAll(point and equipRef == smId).findAll(r => r.missing(\"pointQueryTarget\") or (r.has(\"pointQueryTarget\") and pointQuery(r.get(\"pointQueryTarget\"), {equipRef: smId}).isNull)).toGrid\n-\n+  if (opts.get(\"pointType\") == \"extra\") do\n+    validRecs: tags.map(point => if (pointQuery(point, {equipRef: smId}).isNonNull) pointQuery(point, {equipRef: smId}) else null).findAll(v => v.isNonNull).toGrid\n+    extras: readAll(point and equipRef == smId).findAll(r => r.missing(\"pointQueryTarget\") or (r.has(\"pointQueryTarget\") and pointQuery(r.get(\"pointQueryTarget\"), {equipRef: smId}).isNull)).toGrid\n+    if(validRecs.isNull or validRecs.isEmpty) return extras\n+    extras = extras.findAll(r => not validRecs.colToList(\"id\").contains(r.get(\"id\")))\n+    return extras\n+  end\n   // If none of the opts is called, return the opt key name and the valid values for that key.\n   return {info: \"Accepted \\\"pointType\\\" Opts: [tags, valid, missing, extra]\"}.toGrid.addColMeta(\"info\", {dis: \"Information\"})\n end"
    },
    {
      "name": "stackedAreaChart",
      "pod": "kwLinkCoreExt",
      "pod_src": "(data, opts: {}) => do\n  cipherText :\n    \"AxD:38dDwRQ-pgdqgJiurXebHQ::mhXfpADGbIC7GxoTqRSqdV464RPnMp4CLuc657aoHaKd9_HM1D4a\" +\n    \"HIskOslaBmvn9jSqOhTYxcOCs_3woqg599Elx2Fcyc1JBY-NqdgSwE8XsMz7_c1Wz8fyLkddRGT1pzE_\" +\n    \"kw5hFRL3Sr_Q8-KUBzP8fTjHvzOitZVKQDCcmThHuvxjhdgAtGmeIwmHXrzAEOxrOicz38xgiAcerjFi\" +\n    \"CpxfehBlJt-8borMQX1bT-eBm-mTZqCbijfc1guvAfAQscQLYCYuTD5cpMps-6wkk2689l5Z7eTAehHB\" +\n    \"v6mxCJS_95jACQ_Kez_os1YNCoNMO45G-fkjaKq-Jhu8BfrxOMxAm4GKBGVQUE-hmd9ktMBgmzjx_k_R\" +\n    \"csMl5bYyW8CsJ38vK4TOIit_9K8vKk4BK2aWwqOeca1VB16VFgt3_K53nqDujbk6zLct1BSPZVrUL9_B\" +\n    \"RTqmKKAIiHUR57r7R84k1J5C57XfI_Bk-3EcqyUVrrfHZPdxiYUMEsMXgiODwQ8HMdO4pz7lO9Vjuti8\" +\n    \"nqXRwMZm3o3x7DKDAQdRHSaawNBUrJnBpM2VKmA-ZcEud5xZZQl177mVpVtxMFRxllIJxBRJ864BVXxD\" +\n    \"rzwWf-UyEcxauEhCcTZYqJSgIWzuTydLATJlBjQLceRRGcNo0TKcxwbZ-4H1bOa5pK-bmQKz7AI7FBNB\" +\n    \"5IcHwyPZXydqmnIQ44Y93vyeA1GzOyyVL_IvI2_16095Rklz96bRvj654lLJaKwTCcKpcZX7hMwovOTj\" +\n    \"J_utAqV0DN2mr2U-t1ImzPNCNF2umqB799KwX1AZkHikHM-9HVfliUKCndI51prCfo2Axd7aERcdkOxv\" +\n    \"Xz5g3wgMzTBskd5UdgYG9YQ4kMEQauanpxvKe_ySHg7vy3_zWq1TXZJSjRKt4lKUJRSfbwuB909fW26j\" +\n    \"-j9CRykqL9T-ArZqZ02B455KDjUyAkFw8XtYRxjLHkYyNII5c7EmNvL6Kl8_trUYcbiA8iA\"\n  keyFunc : () => \"ddJNIRm2qio2Z7CXpKADiw\"\n  return afAxonEncryptorRt_callFunc(\"stackedAreaChart\", cipherText, [data, opts], keyFunc)\nend",
      "local_src": "(data, opts:{}) => do\n  \n  //STACK FUNCTION\n  stack: (data) => do\n    // map column names to their index\n    colNames: data.colNames\n    colNameToIndex: {}\n    colNames[1..-1].each((n,i) => do\n      colNameToIndex = colNameToIndex.set(n, i+1)\n    end)\n\n    // map the data cells to the sum of values to the left\n    stream: data.stream.map(r => do\n      r.map((v, n) => do\n        if (n == \"ts\" or n==\"time\") return v\n        sum: 0\n        index: colNameToIndex[n]\n        (1..index).each(i => sum = sum + r[colNames[i]])\n        return sum\n      end)\n    end)\n   \n    // add area mode tag to value columes\n    colNames[1..-1].each((c,i) => do\n      mode: if (i == 0) \"zero\" else \"prevSeries\"\n      clmMeta: if (opts.has(\"removeColor\")) {chartAreaMode:mode, -color}\n               else {chartAreaMode:mode}\n      stream = stream.addColMeta(c,clmMeta)\n    end)\n\n    // ensure column order of input data\n    return stream.addMeta({chartLegendNoSort}).reorderCols(colNames).collect(toGrid)\n  end\n\n  //check incoming grid for empty columns\n  //filter empty colms\n   if (data.cols.any(colm => data.colToList(colm).all(v=>v==null))) do\n     nonEmptyCols: data.cols.findAll(colm => not data.colToList(colm).all(v=>v==null))\n     data = data.keepCols(nonEmptyCols)\n     end\n\n  //check how many unique units\n  units: data.cols.map(r => r.meta.get(\"unit\")).findAll(v=>v!=null).unique\n  unitCount: units.size\n\n  if (unitCount<2) return data.stack()\n  else do\n    out: []\n    units.each u => do\n      unitCols: data.cols.findAll(c=>c.meta.get(\"unit\")==u)\n      unitData: data.keepCols([\"ts\"].addAll(unitCols))\n      out=out.add(unitData.stack())\n    end\n    return out.hisJoin\n  end\n\nend",
      "diff": "@@ -1,18 +1,57 @@-(data, opts: {}) => do\n-  cipherText :\n-    \"AxD:38dDwRQ-pgdqgJiurXebHQ::mhXfpADGbIC7GxoTqRSqdV464RPnMp4CLuc657aoHaKd9_HM1D4a\" +\n-    \"HIskOslaBmvn9jSqOhTYxcOCs_3woqg599Elx2Fcyc1JBY-NqdgSwE8XsMz7_c1Wz8fyLkddRGT1pzE_\" +\n-    \"kw5hFRL3Sr_Q8-KUBzP8fTjHvzOitZVKQDCcmThHuvxjhdgAtGmeIwmHXrzAEOxrOicz38xgiAcerjFi\" +\n-    \"CpxfehBlJt-8borMQX1bT-eBm-mTZqCbijfc1guvAfAQscQLYCYuTD5cpMps-6wkk2689l5Z7eTAehHB\" +\n-    \"v6mxCJS_95jACQ_Kez_os1YNCoNMO45G-fkjaKq-Jhu8BfrxOMxAm4GKBGVQUE-hmd9ktMBgmzjx_k_R\" +\n-    \"csMl5bYyW8CsJ38vK4TOIit_9K8vKk4BK2aWwqOeca1VB16VFgt3_K53nqDujbk6zLct1BSPZVrUL9_B\" +\n-    \"RTqmKKAIiHUR57r7R84k1J5C57XfI_Bk-3EcqyUVrrfHZPdxiYUMEsMXgiODwQ8HMdO4pz7lO9Vjuti8\" +\n-    \"nqXRwMZm3o3x7DKDAQdRHSaawNBUrJnBpM2VKmA-ZcEud5xZZQl177mVpVtxMFRxllIJxBRJ864BVXxD\" +\n-    \"rzwWf-UyEcxauEhCcTZYqJSgIWzuTydLATJlBjQLceRRGcNo0TKcxwbZ-4H1bOa5pK-bmQKz7AI7FBNB\" +\n-    \"5IcHwyPZXydqmnIQ44Y93vyeA1GzOyyVL_IvI2_16095Rklz96bRvj654lLJaKwTCcKpcZX7hMwovOTj\" +\n-    \"J_utAqV0DN2mr2U-t1ImzPNCNF2umqB799KwX1AZkHikHM-9HVfliUKCndI51prCfo2Axd7aERcdkOxv\" +\n-    \"Xz5g3wgMzTBskd5UdgYG9YQ4kMEQauanpxvKe_ySHg7vy3_zWq1TXZJSjRKt4lKUJRSfbwuB909fW26j\" +\n-    \"-j9CRykqL9T-ArZqZ02B455KDjUyAkFw8XtYRxjLHkYyNII5c7EmNvL6Kl8_trUYcbiA8iA\"\n-  keyFunc : () => \"ddJNIRm2qio2Z7CXpKADiw\"\n-  return afAxonEncryptorRt_callFunc(\"stackedAreaChart\", cipherText, [data, opts], keyFunc)\n+(data, opts:{}) => do\n+\n+  //STACK FUNCTION\n+  stack: (data) => do\n+    // map column names to their index\n+    colNames: data.colNames\n+    colNameToIndex: {}\n+    colNames[1..-1].each((n,i) => do\n+      colNameToIndex = colNameToIndex.set(n, i+1)\n+    end)\n+\n+    // map the data cells to the sum of values to the left\n+    stream: data.stream.map(r => do\n+      r.map((v, n) => do\n+        if (n == \"ts\" or n==\"time\") return v\n+        sum: 0\n+        index: colNameToIndex[n]\n+        (1..index).each(i => sum = sum + r[colNames[i]])\n+        return sum\n+      end)\n+    end)\n+\n+    // add area mode tag to value columes\n+    colNames[1..-1].each((c,i) => do\n+      mode: if (i == 0) \"zero\" else \"prevSeries\"\n+      clmMeta: if (opts.has(\"removeColor\")) {chartAreaMode:mode, -color}\n+               else {chartAreaMode:mode}\n+      stream = stream.addColMeta(c,clmMeta)\n+    end)\n+\n+    // ensure column order of input data\n+    return stream.addMeta({chartLegendNoSort}).reorderCols(colNames).collect(toGrid)\n+  end\n+\n+  //check incoming grid for empty columns\n+  //filter empty colms\n+   if (data.cols.any(colm => data.colToList(colm).all(v=>v==null))) do\n+     nonEmptyCols: data.cols.findAll(colm => not data.colToList(colm).all(v=>v==null))\n+     data = data.keepCols(nonEmptyCols)\n+     end\n+\n+  //check how many unique units\n+  units: data.cols.map(r => r.meta.get(\"unit\")).findAll(v=>v!=null).unique\n+  unitCount: units.size\n+\n+  if (unitCount<2) return data.stack()\n+  else do\n+    out: []\n+    units.each u => do\n+      unitCols: data.cols.findAll(c=>c.meta.get(\"unit\")==u)\n+      unitData: data.keepCols([\"ts\"].addAll(unitCols))\n+      out=out.add(unitData.stack())\n+    end\n+    return out.hisJoin\n+  end\n+\n end"
    },
    {
      "name": "task_energyAgent_anomalyArcOrchestrator",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "(opts: {syncDuration: 1wk, testMode:false}) => do\n\n  // Sync all model points\n  callTask(\"task_energyAgent_syncGenAIModels\", [], true)\n\n  // Delete old anomaly recs\n  readAll(anomaly).colToList(\"id\").recDelete\n\n  // Find triggers and anomalies\n  callTask(\"task_energyAgent_syncDataAndFindAnomalyTriggers\", [opts], true)\n  callTask(\"task_energyAgent_createFixedModelsAndAnomalyRecs\", [], true)\n\n  //Recompute the EA-related rules to refresh all KPI numbers in the dashboards.\n  ruleList: readAll(rule and kpiRule).findAll(v=>v.get(\"ruleFunc\").contains(\"kpi_anomaly\"))\n  ruleRecompute(null, null, ruleList)\nend",
      "local_src": "(opts: {syncDuration: 1wk, testMode:false}) => do\n  \n  // Sync all model points\n  //callTask(\"task_energyAgent_syncGenAIModels\", [], true)\n  \n  // Delete old anomaly recs\n  try readAll(anomaly or anomalyTrigger).colToList(\"id\").recDelete catch null\n  \n  // Find triggers and anomalies\n  callTask(\"task_energyAgent_syncDataAndFindAnomalyTriggers\", [opts], true)\n  callTask(\"task_energyAgent_createFixedModelsAndAnomalyRecs\", [], true)\n  \n  //Recompute the EA-related rules to refresh all KPI numbers in the dashboards.\n  ruleList: readAll(rule and kpiRule).findAll(v=>v.get(\"ruleFunc\").contains(\"kpi_anomaly\"))\n  ruleRecompute(null, null, ruleList)\nend",
      "diff": "@@ -1,10 +1,10 @@ (opts: {syncDuration: 1wk, testMode:false}) => do\n \n   // Sync all model points\n-  callTask(\"task_energyAgent_syncGenAIModels\", [], true)\n+  //callTask(\"task_energyAgent_syncGenAIModels\", [], true)\n \n   // Delete old anomaly recs\n-  readAll(anomaly).colToList(\"id\").recDelete\n+  try readAll(anomaly or anomalyTrigger).colToList(\"id\").recDelete catch null\n \n   // Find triggers and anomalies\n   callTask(\"task_energyAgent_syncDataAndFindAnomalyTriggers\", [opts], true)\n"
    },
    {
      "name": "task_energyAgent_syncDataAndFindAnomalyTriggers",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "(opts: {syncDuration: 1wk, testMode:false}) => do\n\n  syncDuration: opts.optNorm(\"syncDuration\", 1wk)\n\n  modelPoints: readAll(syntheticPoint and modelType==\"adaptiveBaseline\" and hisSize)\n\n  modelPoints.each() mp => do\n    //Get Meter Point\n    meterPointRef : mp.get(\"pointRef\")\n    meterPoint    : readById(meterPointRef)\n    meterHisEnd   : meterPoint.get(\"hisEnd\")\n    logInfo(\"energyAgentTask\", \"EnergyAgent Evaluating model for \"+meterPoint.dis)\n\n    // TODO?: Add check for modelPoint hisEnd, compare to old/new hisEnds. Include the following code chunk in a conditional as well?\n    // modelPoint => virtualSyncHis\n    modelPointRef: mp.get(\"id\")\n    modelEndDate: mp.get(\"hisEnd\")\n\n    // Add check for anomalyArc status, only find new triggers if no open arcs exist.\n    // akTODO: Also check if triggers exist. Figure out the unique conditional\n    // Otherwise need to delete triggers before each run\n    openArcs: readAll(anomalyArc and arcOn==meterPointRef).findAll(v=>v.get(\"anomalyArcState\")!=\"resolved\")\n    // Only map through the meterPoints that got updated?\n    //if(newHisEnd.date > oldHisEnd.date and openArcs.size == 0) do\n\n    if(openArcs.size == 0) do\n      //Find and commit anomalyTriggers\n      //dateSpan: (oldHisEnd..newHisEnd).toSpan\n      modelPointHisEnd: mp.get(\"hisEnd\")\n      targetPointHisEnd: mp.get(\"pointRef\")->hisEnd\n      modelEnd: if(modelPointHisEnd < targetPointHisEnd) modelPointHisEnd else targetPointHisEnd\n\n      dateSpan: (modelEnd-3mo..modelEnd).toSpan // (Only finds triggers/anomalies for the past 3 months. Anchor a start date instead? akTODO: potentially YES, different for different projects)\n      sensitivityOpts: {minAnomalyDuration:24h, highAnomalyThreshold:10%, sustainedAnomalyThreshold:24h}\n\n      try anomalyTrigger: logic_kwModeling_anomalyDetection_findAndCommitAnomalyTriggers(mp, dateSpan, sensitivity:\"high\", sensitivityOpts, opts:{commitToRec:true})\n      catch(err) return diff(mp, {anomalyTriggerCreationErr: err.dis}, {transient}).commit()\n\n    end\n    //end\n  end\nend",
      "local_src": "(opts: {syncDuration: 1wk, testMode:false}) => do\n\n  syncDuration: opts.optNorm(\"syncDuration\", 1wk)\n  \n  modelPoints: readAll(syntheticPoint and modelType==\"adaptiveBaseline\" and hisSize).toRecList\n  \n  modelPoints.each() mp => do\n    //Get Meter Point\n    meterPointRef : mp.get(\"pointRef\")\n    meterPoint    : readById(meterPointRef)\n    meterHisEnd   : meterPoint.get(\"hisEnd\")\n    logInfo(\"energyAgentTask\", \"EnergyAgent Evaluating model for \"+meterPoint.dis)\n    \n    modelPointRef : mp.get(\"id\")\n    modelHisEnd   : mp.get(\"hisEnd\")\n    \n    siteTz: meterPoint.get(\"siteRef\").toRec.get(\"tz\")\n    // Overwritten date on Cornell. Might just default to past month for the pod\n    anomalyFindingStartDate: parseDateTime(\"2025-08-01 00:00:00\", \"YYYY-MM-DD hh:mm:SS\", siteTz)\n    // Add check for anomalyArc status, only find new triggers if no open arcs exist. \n    //========================================================> Also check if triggers exist. Figure out the unique conditional\n    // Otherwise need to delete triggers before each run\n    //openArcs: readAll(anomalyArc and arcOn==meterPointRef).findAll(v=>v.get(\"anomalyArcState\")!=\"resolved\") \n    // No more auto creating arcs (might want to fit this in again once we finalize the MBCx pipeline anomaly arc creation\n    // Only map through the meterPoints that got updated? //if(newHisEnd.date > oldHisEnd.date and openArcs.size == 0) do\n    \n    if(modelHisEnd>anomalyFindingStartDate and meterHisEnd>anomalyFindingStartDate) do \n      //Find and commit anomalyTriggers\n      \n      modelEnd: if(modelHisEnd < meterHisEnd) modelHisEnd else meterHisEnd\n      //logInfo(\"energyAgentTask\", \"Task 1: Checkpoint 2\")\n      dateSpan: (anomalyFindingStartDate..modelEnd).toSpan \n      sensitivityOpts: {minAnomalyDuration:24hr, highAnomalyThreshold:10%, sustainedAnomalyThreshold:24hr}\n      try anomalyTrigger: logic_kwModeling_anomalyDetection_findAndCommitAnomalyTriggers(mp, dateSpan, sensitivity:\"high\", sensitivityOpts, opts:{commitToRec:true})\n      catch(err) return diff(mp, {anomalyTriggerCreationErr: err.dis}, {transient}).commit()\n      //logInfo(\"energyAgentTask\", \"Task 1: Anomaly trigger found: \"+anomalyTrigger.get(\"id\").toStr)\n    end\n    //end  \n  end \nend",
      "diff": "@@ -2,7 +2,7 @@ \n   syncDuration: opts.optNorm(\"syncDuration\", 1wk)\n \n-  modelPoints: readAll(syntheticPoint and modelType==\"adaptiveBaseline\" and hisSize)\n+  modelPoints: readAll(syntheticPoint and modelType==\"adaptiveBaseline\" and hisSize).toRecList\n \n   modelPoints.each() mp => do\n     //Get Meter Point\n@@ -11,31 +11,29 @@     meterHisEnd   : meterPoint.get(\"hisEnd\")\n     logInfo(\"energyAgentTask\", \"EnergyAgent Evaluating model for \"+meterPoint.dis)\n \n-    // TODO?: Add check for modelPoint hisEnd, compare to old/new hisEnds. Include the following code chunk in a conditional as well?\n-    // modelPoint => virtualSyncHis\n-    modelPointRef: mp.get(\"id\")\n-    modelEndDate: mp.get(\"hisEnd\")\n+    modelPointRef : mp.get(\"id\")\n+    modelHisEnd   : mp.get(\"hisEnd\")\n \n+    siteTz: meterPoint.get(\"siteRef\").toRec.get(\"tz\")\n+    // Overwritten date on Cornell. Might just default to past month for the pod\n+    anomalyFindingStartDate: parseDateTime(\"2025-08-01 00:00:00\", \"YYYY-MM-DD hh:mm:SS\", siteTz)\n     // Add check for anomalyArc status, only find new triggers if no open arcs exist.\n-    // akTODO: Also check if triggers exist. Figure out the unique conditional\n+    //========================================================> Also check if triggers exist. Figure out the unique conditional\n     // Otherwise need to delete triggers before each run\n-    openArcs: readAll(anomalyArc and arcOn==meterPointRef).findAll(v=>v.get(\"anomalyArcState\")!=\"resolved\")\n-    // Only map through the meterPoints that got updated?\n-    //if(newHisEnd.date > oldHisEnd.date and openArcs.size == 0) do\n+    //openArcs: readAll(anomalyArc and arcOn==meterPointRef).findAll(v=>v.get(\"anomalyArcState\")!=\"resolved\")\n+    // No more auto creating arcs (might want to fit this in again once we finalize the MBCx pipeline anomaly arc creation\n+    // Only map through the meterPoints that got updated? //if(newHisEnd.date > oldHisEnd.date and openArcs.size == 0) do\n \n-    if(openArcs.size == 0) do\n+    if(modelHisEnd>anomalyFindingStartDate and meterHisEnd>anomalyFindingStartDate) do\n       //Find and commit anomalyTriggers\n-      //dateSpan: (oldHisEnd..newHisEnd).toSpan\n-      modelPointHisEnd: mp.get(\"hisEnd\")\n-      targetPointHisEnd: mp.get(\"pointRef\")->hisEnd\n-      modelEnd: if(modelPointHisEnd < targetPointHisEnd) modelPointHisEnd else targetPointHisEnd\n \n-      dateSpan: (modelEnd-3mo..modelEnd).toSpan // (Only finds triggers/anomalies for the past 3 months. Anchor a start date instead? akTODO: potentially YES, different for different projects)\n-      sensitivityOpts: {minAnomalyDuration:24h, highAnomalyThreshold:10%, sustainedAnomalyThreshold:24h}\n-\n+      modelEnd: if(modelHisEnd < meterHisEnd) modelHisEnd else meterHisEnd\n+      //logInfo(\"energyAgentTask\", \"Task 1: Checkpoint 2\")\n+      dateSpan: (anomalyFindingStartDate..modelEnd).toSpan\n+      sensitivityOpts: {minAnomalyDuration:24hr, highAnomalyThreshold:10%, sustainedAnomalyThreshold:24hr}\n       try anomalyTrigger: logic_kwModeling_anomalyDetection_findAndCommitAnomalyTriggers(mp, dateSpan, sensitivity:\"high\", sensitivityOpts, opts:{commitToRec:true})\n       catch(err) return diff(mp, {anomalyTriggerCreationErr: err.dis}, {transient}).commit()\n-\n+      //logInfo(\"energyAgentTask\", \"Task 1: Anomaly trigger found: \"+anomalyTrigger.get(\"id\").toStr)\n     end\n     //end\n   end\n"
    },
    {
      "name": "task_energyAgent_syncGenAIModels",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "() => do\n  syncDuration: 1wk\n  modelPoints: readAll(syntheticPoint and modelType==\"adaptiveBaseline\" and hisSize)\n  modelPoints.each() mp => do\n    //Get Meter Point\n    meterPointRef : mp.get(\"pointRef\")\n    meterPoint    : readById(meterPointRef)\n    meterHisEnd   : meterPoint.get(\"hisEnd\")\n    logInfo(\"energyAgentTask\", \"Task 1: Evaluating model for \"+meterPoint.dis)\n\n    modelPointRef : mp.get(\"id\")\n    modelHisEnd   : mp.get(\"hisEnd\")\n\n    // Sync model if new meter data is available\n    if (modelHisEnd <= meterHisEnd - syncDuration) do\n      syncSpan: (modelHisEnd..meterHisEnd).toSpan\n      logInfo(\"energyAgentTask\", \"Syncing model point \" + mp.dis + \" for span \" + syncSpan.toStr)\n      callTask(\"virtualSyncHis\", [modelPointRef, syncSpan], getVal: true)\n    end\n  end\nend",
      "local_src": "() => do\n  syncDuration: 7day\n  modelPoints: readAll(syntheticPoint and modelType==\"adaptiveBaseline\" and hisSize)\n  modelPoints.each() mp => do\n    //Get Meter Point\n    meterPointRef : mp.get(\"pointRef\")\n    meterPoint    : readById(meterPointRef)\n    meterHisEnd   : meterPoint.get(\"hisEnd\")\n    logInfo(\"energyAgentTask\", \"Task 1: Evaluating model for \"+meterPoint.dis)\n\n    modelPointRef : mp.get(\"id\")\n    modelHisEnd   : mp.get(\"hisEnd\")\n\n    // Sync model if new meter data is available\n    if (modelHisEnd <= meterHisEnd - syncDuration) do\n      syncSpan: (modelHisEnd..meterHisEnd).toSpan\n      logInfo(\"energyAgentTask\", \"Syncing model point \" + mp.dis + \" for span \" + syncSpan.toStr)\n      callTask(\"virtualSyncHis\", [modelPointRef, syncSpan], getVal: true)\n    end\n  end\nend",
      "diff": "@@ -1,5 +1,5 @@ () => do\n-  syncDuration: 1wk\n+  syncDuration: 7day\n   modelPoints: readAll(syntheticPoint and modelType==\"adaptiveBaseline\" and hisSize)\n   modelPoints.each() mp => do\n     //Get Meter Point\n"
    },
    {
      "name": "view_calculateSavings",
      "pod": "kwLinkModelExt",
      "pod_src": "//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   DD MMM 22   Dylan Bardin          - Creation.\n//   09 Aug 24   Thomas Kuhrke Limia   - Refactorization and code documentation.\n//   18 Feb 25   Apoorv Khanuja        - Added @null check at the top (so the view isn't full of errors when you first click on it)\n//   15 Jul 25   Apoorv Khanuja        - Error catching, null removal, dynamic unit conversion\n//\n// Description\n//   This function represents the view of the calculated savings tab.\n//\n// Parameters\n//   Name                 Type(s)\n//   tile                  Str\n//   modelPoint            Ref\n//   dates                 DateTime\n//   rollup                Number\n//   temp                  Number\n//\n// Returns\n//   Type           Description\n//   1. TODO        - TODO\n//\n// NOTES\n// - Re-organize and refactor view.\n//\n(tile, modelPoint, dates, rollup:null, temp: 50\u00b0F) => do\n\n  // Check for when the point is null\n  if (modelPoint.isNull or modelPoint==@null) return {info: \"Select inputs before proceeding.\"}.toGrid.addMeta({noData: \"Select inputs before proceeding.\"}).addColMeta(\"info\", {dis: \"Information\"})\n\n  modelPoint = readById(modelPoint)\n  equip: modelPoint->equipRef\n  basePt: readById(modelPoint->modelRef)->pointRef\n  interval: modelPoint.get(\"hisRollupInterval\")\n  weather: readAll(weatherStationRef==equip->siteRef->weatherStationRef and temp)\n  weatherHis: weather.hisRead(dates, {-limit}).renameCol(\"v0\",\"oat\").setColMeta(\"oat\",{chartGroup:\"a\", color:\"gray\", opacity:0.1, strokeDasharray:\"2,2\", dis:\"Outside Air Temp\"})\n  siteDis: readById(modelPoint->siteRef).dis\n  modelDis: modelPoint.dis\n  days: (dates.end - dates.start).to(1day)\n  modelUnit: modelPoint.get(\"unit\") // typically power unit\n  usageUnit: logic_dynamicallyConvertUnits(modelUnit, \"powerToUsage\")\n\n  try do\n    h1: hisRead(basePt, dates, {-limit}).renameCol(\"v0\",\"meas\").findAll(v => v.get(\"meas\").toStr!=\"NaN\")\n    h2: hisRead(modelPoint, dates, {-limit}).renameCol(\"v0\",\"model\").findAll(v => v.get(\"model\").toStr!=\"NaN\")\n    h: hisJoin([h1,h2, weatherHis])\n    nam: h.colNames\n    h= h.hisRollup(avg, interval)\n              .hisClip\n              .hisInterpolate\n\n    h = h .findAll(r=> r.has(nam[1]) and r.has(nam[2]))\n          .addCol(\"delta\",  r => r.get(\"model\")-r.get(\"meas\"))\n\n  end catch (ex) do\n    return (\"Press the \\\"Sync Calculated Point\\\" button\").toGrid.addMeta({\"view\": \"table\"})\n  end\n\n  // Converting between usage and power // TODO: Make it dynamic, AND keep the actual hisGrid numbers in power units, but the final rolled up numbers in usage units.\n  //if (modelPoint.get(\"unit\")==\"kW\" and interval==1hr) h=h.hisMap(v => if (v.unit==\"kW\") v.as(\"kWh\") else v)\n\n  h = h.findAll(r => r.has(\"model\") and r.has(\"meas\"))\n\n  if (h.size<1)\n    if (tile==\"comparison\") return [{ts:now(), warning:0}].toGrid.addMeta({title:\"Data Unavailable - No his data found for selected point\", subtitle:\"Make sure data is synced\"})\n    else return [].toGrid.addMeta({noData:\"No his data\"})\n\n  mod: h.colToList(\"model\").fold(sum)\n  meas: h.colToList(\"meas\").fold(sum)\n  delta: h.colToList(\"delta\").fold(sum)\n  pct: ((delta / mod)*100).as(\"%\")\n  coolingSav: h.findAll(row => row.get(\"oat\")>temp).colToList(\"delta\").fold(sum)\n  heatingSav: h.findAll(row => row.get(\"oat\")<=temp).colToList(\"delta\").fold(sum)\n  unit_: mod.unit\n  if (heatingSav==null) heatingSav=0.00.as(unit_)\n  if (coolingSav==null) coolingSav=0.00.as(unit_)\n\n  if (rollup==\"Hourly\") do\n    title:\"Hourly Comparison\"\n    intervalRollup:1hr\n  end\n  else if (rollup==\"Monthly\") do\n    title:\"Monthly Comparison\"\n    intervalRollup:1mo\n  end\n  else if (rollup==\"Daily\") do\n    title:\"Daily Comparison\"\n    intervalRollup:1day\n  end\n  else do\n    title:\"\"\n    intervalRollup:1hr\n  end\n\n  //overrides\n  if (intervalRollup < interval) intervalRollup = interval\n\n  dailyData: h.keepCols([\"ts\",\"delta\"]).hisMap(v => v.as(usageUnit))\n                .hisRollup(sum, 1day)\n  incr: dailyData.findAll(r=>r.get(\"delta\")<0)\n  incrDays: incr.size\n  incrSum: try incr.foldCol(\"delta\",sum).abs.round catch 0\n  decr: dailyData.findAll(r=>r.get(\"delta\")>0)\n  decrDays: decr.size\n  decrSum: try decr.foldCol(\"delta\",sum).abs.round catch 0\n\n  datas: h.hisRollup(avg, intervalRollup)\n          .addCol(\"up\",   r => if (r.get(\"delta\")<0) r.get(\"delta\") else 0)\n          .addCol(\"down\", r => if (r.get(\"delta\")>0) r.get(\"delta\") else 0)\n\n  out: datas.addMeta({\"title\" : title, \"chartLegendNoSort\"})\n            .addColMeta(\"meas\",       {order: 1, dis: readById(basePt).dis, chartGroup: \"a\"})\n            .addColMeta(\"model\",      {order: 2, dis:\"Modeled\",             chartGroup: \"a\", color: \"orange\", chartType:\"area\", chartAreaMode:\"nextSeries\", strokeDasharray: \"6,4\"})\n            .addColMeta(\"up\",         {order: 3, dis: \"Increased Usage\",    chartGroup: \"b\", color: \"red\",    chartType:\"area\", chartAreaMode:\"zero\"})\n            .addColMeta(\"down\",       {order: 3, dis: \"Savings\",            chartGroup: \"b\", color: \"green\",  chartType:\"area\", chartAreaMode:\"zero\", title:\"Delta\", subtitle:\"Overall, energy decreased by \"+decrSum+\", and increased by \"+incrSum})\n            .removeCols([\"delta\"])\n\n  out = out.hisRollup(avg, intervalRollup)\n\n  if (tile == \"comparison\") return out\n\n  else if(tile == \"meas\") do\n    card: {title: \"Modeled vs Measured - \"+modelDis,//siteDis\n           subtitle:pct.round.toStr+ \" Savings (\" + dates.start.date.toStr + \" to \" +dates.end.date.toStr + \")\",\n           mod: mod.as(usageUnit),\n           meas: meas.abs.as(usageUnit),\n           delta: delta.abs.as(usageUnit)}\n\n      try out2: card.toGrid\n              .addMeta({chartLegendNoSort})\n              .addColMeta(\"subtitle\",       {                          color: if (pct>0) \"green\" else \"red\",      viz:\"barCell\"})\n              .addColMeta(\"mod\",       {dis:\"Modeled Usage\",           color:\"orange\",      viz:\"barCell\"})\n              .addColMeta(\"meas\",      {dis:\"Measured Usage\",          color:\"#3498DB\",     viz:\"barCell\"})\n              .addColMeta(\"delta\",     {dis:if (delta<0) \"Increased Usage\" else \"Savings\",     color:if (delta>0) \"green\" else \"red\",      viz:\"barCell\"})\n              .betterReorderCols([\"mod\", \"meas\",\"delta\",\"pct\"])\n     catch return \"try pressing the Sync Calculated Point Button\"\n\n\n    return out2.addMeta({vizByUnit})\n\n  end else if(tile == \"tempSavings\") do\n    suffix1: if (heatingSav<0) \"an increased usage of \"\n             else              \"savings of\"\n    suffix2: if (coolingSav<0) \"an increased usage of\"\n             else              \"savings of\"\n    card: {title:\"Cooling vs Heating\",\n           subtitle:\"How does outside air temp correlate with savings? [\"+temp+\" changepoint]\",\n           heating:\"In heating season, we saw \"+suffix1,\n           below:heatingSav.abs.as(usageUnit),\n           cooling:\"In cooling season, we saw \"+suffix2,\n           above:coolingSav.abs.as(usageUnit),\n           }.toGrid\n            .addColMeta(\"below\", {dis:\"Heating\", viz:\"barCell\", color:\"red\"})\n            .addColMeta(\"heating\", {dis:\"\", viz:\"\", color:\"red\"})\n            .addColMeta(\"above\", {dis:\"Cooling\", viz:\"barCell\", color:\"blue\"})\n            .addColMeta(\"cooling\", {dis:\"\", viz:\"\", color:\"blue\"})\n            .addMeta({vizByUnit})\n    return card\n\n  end else if(tile == \"pct\") do\n\n    increasedDays:    if (incrDays!=null) incrDays else 0\n    savingsDays:     if (decrDays!=null) decrDays else 0\n\n    pie:  {days:days,\n           increasedDays:    increasedDays,\n           savingsDays:     savingsDays,\n           }.toGrid\n            .addColMeta(\"increasedDays\",  {dis:\"Increased Usage [\"+increasedDays+\"]\", color:\"red\",      viz:\"barCell\"})\n            .addColMeta(\"savingsDays\",    {dis:\"Savings [\"+savingsDays+\"]\",      color:\"green\",      viz:\"barCell\"})\n            .addMeta({title:\"# Days With\", chartType:\"pie\"})\n    return pie\n\n  end\nend",
      "local_src": "//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   DD MMM 22   Dylan Bardin          - Creation.\n//   09 Aug 24   Thomas Kuhrke Limia   - Refactorization and code documentation.\n//   18 Feb 25   Apoorv Khanuja        - Added @null check at the top (so the view isn't full of errors when you first click on it)\n//   15 Jul 25   Apoorv Khanuja        - Error catching, null removal, dynamic unit conversion\n//\n// Description\n//   This function represents the view of the calculated savings tab.\n//\n// Parameters\n//   Name                 Type(s)\n//   tile                  Str\n//   modelPoint            Ref\n//   dates                 DateTime\n//   rollup                Number\n//   temp                  Number\n//\n// Returns\n//   Type           Description\n//   1. TODO        - TODO\n//\n// NOTES\n// - Re-organize and refactor view.\n//\n(tile, modelPoint, dates, rollup:null, temp: 50\u00b0F) => do\n\n  // Check for when the point is null\n  if (modelPoint.isNull or modelPoint==@null) return {info: \"Select inputs before proceeding.\"}.toGrid.addMeta({noData: \"Select inputs before proceeding.\"}).addColMeta(\"info\", {dis: \"Information\"})\n\n  modelPoint = readById(modelPoint)\n  equip: modelPoint->equipRef\n  basePt: readById(modelPoint->modelRef)->pointRef\n  interval: modelPoint.get(\"hisRollupInterval\")\n  weather: readById(readById(modelPoint->modelRef)->modelConfig->viewInputs->temp)//readAll(weatherStationRef==equip->siteRef->weatherStationRef and temp and not enthalpy) //Added filter to remove enthalpy as an accidentally considered point here\n  weatherUnit: weather.get(\"unit\")\n  if(weatherUnit == \"\u00b0F\") temp = 50\u00b0F else if (weatherUnit == \"btu/lb_dry\") temp = 28.as(\"btu/lb_dry\")\n  weatherHis: weather.hisRead(dates, {-limit}).renameCol(\"v0\",\"oat\").setColMeta(\"oat\",{chartGroup:\"a\", color:\"gray\", opacity:0.1, strokeDasharray:\"2,2\", dis:\"Outside Air Temp\"})\n  siteDis: readById(modelPoint->siteRef).dis\n  modelDis: modelPoint.dis\n  days: (dates.end - dates.start).to(1day)\n  modelUnit: modelPoint.get(\"unit\") // typically power unit\n  usageUnit: logic_dynamicallyConvertUnits(modelUnit, \"powerToUsage\")\n\n  try do\n    h1: hisRead(basePt, dates, {-limit}).renameCol(\"v0\",\"meas\").findAll(v => v.get(\"meas\").toStr!=\"NaN\")\n    h2: hisRead(modelPoint, dates, {-limit}).renameCol(\"v0\",\"model\").findAll(v => v.get(\"model\").toStr!=\"NaN\")\n    h: hisJoin([h1,h2, weatherHis])\n    nam: h.colNames\n    h= h.hisRollup(avg, interval)\n              .hisClip\n              .hisInterpolate\n\n    h = h .findAll(r=> r.has(nam[1]) and r.has(nam[2]))\n          .addCol(\"delta\",  r => r.get(\"model\")-r.get(\"meas\"))\n\n  end catch (ex) do\n    return (\"Press the \\\"Sync Calculated Point\\\" button\").toGrid.addMeta({\"view\": \"table\"})\n  end\n\n  // Converting between usage and power // TODO: Make it dynamic, AND keep the actual hisGrid numbers in power units, but the final rolled up numbers in usage units.\n  //if (modelPoint.get(\"unit\")==\"kW\" and interval==1hr) h=h.hisMap(v => if (v.unit==\"kW\") v.as(\"kWh\") else v)\n\n  h = h.findAll(r => r.has(\"model\") and r.has(\"meas\"))\n\n  if (h.size<1)\n    if (tile==\"comparison\") return [{ts:now(), warning:0}].toGrid.addMeta({title:\"Data Unavailable - No his data found for selected point\", subtitle:\"Make sure data is synced\"})\n    else return [].toGrid.addMeta({noData:\"No his data\"})\n\n  mod: h.colToList(\"model\").fold(sum)\n  meas: h.colToList(\"meas\").fold(sum)\n  delta: h.colToList(\"delta\").fold(sum)\n  pct: ((delta / mod)*100).as(\"%\")\n  coolingSav: h.findAll(row => row.get(\"oat\")>temp).colToList(\"delta\").fold(sum)\n  heatingSav: h.findAll(row => row.get(\"oat\")<=temp).colToList(\"delta\").fold(sum)\n  unit_: mod.unit\n  if (heatingSav==null) heatingSav=0.00.as(unit_)\n  if (coolingSav==null) coolingSav=0.00.as(unit_)\n\n  if (rollup==\"Hourly\") do\n    title:\"Hourly Comparison\"\n    intervalRollup:1hr\n  end\n  else if (rollup==\"Monthly\") do\n    title:\"Monthly Comparison\"\n    intervalRollup:1mo\n  end\n  else if (rollup==\"Daily\") do\n    title:\"Daily Comparison\"\n    intervalRollup:1day\n  end\n  else do\n    title:\"\"\n    intervalRollup:1hr\n  end\n\n  //overrides\n  if (intervalRollup < interval) intervalRollup = interval\n\n  dailyData: h.keepCols([\"ts\",\"delta\"]).hisMap(v => v.as(usageUnit))\n                .hisRollup(sum, 1day)\n  incr: dailyData.findAll(r=>r.get(\"delta\")<0)\n  incrDays: incr.size\n  incrSum: try incr.foldCol(\"delta\",sum).abs.round catch 0\n  decr: dailyData.findAll(r=>r.get(\"delta\")>0)\n  decrDays: decr.size\n  decrSum: try decr.foldCol(\"delta\",sum).abs.round catch 0\n\n  datas: h.hisRollup(avg, intervalRollup)\n          .addCol(\"up\",   r => if (r.get(\"delta\")<0) r.get(\"delta\") else 0)\n          .addCol(\"down\", r => if (r.get(\"delta\")>0) r.get(\"delta\") else 0)\n\n  out: datas.addMeta({\"title\" : title, \"chartLegendNoSort\"})\n            .addColMeta(\"meas\",       {order: 1, dis: readById(basePt).dis, chartGroup: \"a\"})\n            .addColMeta(\"model\",      {order: 2, dis:\"Modeled\",             chartGroup: \"a\", color: \"orange\", chartType:\"area\", chartAreaMode:\"nextSeries\", strokeDasharray: \"6,4\"})\n            .addColMeta(\"up\",         {order: 3, dis: \"Increased Usage\",    chartGroup: \"b\", color: \"red\",    chartType:\"area\", chartAreaMode:\"zero\"})\n            .addColMeta(\"down\",       {order: 3, dis: \"Savings\",            chartGroup: \"b\", color: \"green\",  chartType:\"area\", chartAreaMode:\"zero\", title:\"Delta\", subtitle:\"Overall, energy decreased by \"+decrSum+\", and increased by \"+incrSum})\n            .removeCols([\"delta\"])\n\n  out = out.hisRollup(avg, intervalRollup)\n\n  if (tile == \"comparison\") return out\n\n  else if(tile == \"meas\") do\n    card: {title: \"Modeled vs Measured - \"+modelDis,//siteDis\n           subtitle:pct.round.toStr+ \" Savings (\" + dates.start.date.toStr + \" to \" +dates.end.date.toStr + \")\",\n           mod: mod.as(usageUnit),\n           meas: meas.abs.as(usageUnit),\n           delta: delta.abs.as(usageUnit)}\n\n      try out2: card.toGrid\n              .addMeta({chartLegendNoSort})\n              .addColMeta(\"subtitle\",       {                          color: if (pct>0) \"green\" else \"red\",      viz:\"barCell\"})\n              .addColMeta(\"mod\",       {dis:\"Modeled Usage\",           color:\"orange\",      viz:\"barCell\"})\n              .addColMeta(\"meas\",      {dis:\"Measured Usage\",          color:\"#3498DB\",     viz:\"barCell\"})\n              .addColMeta(\"delta\",     {dis:if (delta<0) \"Increased Usage\" else \"Savings\",     color:if (delta>0) \"green\" else \"red\",      viz:\"barCell\"})\n              .betterReorderCols([\"mod\", \"meas\",\"delta\",\"pct\"])\n     catch return \"try pressing the Sync Calculated Point Button\"\n\n\n    return out2.addMeta({vizByUnit})\n\n  end else if(tile == \"tempSavings\") do\n    suffix1: if (heatingSav<0) \"an increased usage of \"\n             else              \"savings of\"\n    suffix2: if (coolingSav<0) \"an increased usage of\"\n             else              \"savings of\"\n    card: {title:\"Cooling vs Heating\",\n           subtitle:\"How does outside air temp correlate with savings? [\"+temp+\" changepoint]\",\n           heating:\"In heating season, we saw \"+suffix1,\n           below:heatingSav.abs.as(usageUnit),\n           cooling:\"In cooling season, we saw \"+suffix2,\n           above:coolingSav.abs.as(usageUnit),\n           }.toGrid\n            .addColMeta(\"below\", {dis:\"Heating\", viz:\"barCell\", color:\"red\"})\n            .addColMeta(\"heating\", {dis:\"\", viz:\"\", color:\"red\"})\n            .addColMeta(\"above\", {dis:\"Cooling\", viz:\"barCell\", color:\"blue\"})\n            .addColMeta(\"cooling\", {dis:\"\", viz:\"\", color:\"blue\"})\n            .addMeta({vizByUnit})\n    return card\n\n  end else if(tile == \"pct\") do\n\n    increasedDays:    if (incrDays!=null) incrDays else 0\n    savingsDays:     if (decrDays!=null) decrDays else 0\n\n    pie:  {days:days,\n           increasedDays:    increasedDays,\n           savingsDays:     savingsDays,\n           }.toGrid\n            .addColMeta(\"increasedDays\",  {dis:\"Increased Usage [\"+increasedDays+\"]\", color:\"red\",      viz:\"barCell\"})\n            .addColMeta(\"savingsDays\",    {dis:\"Savings [\"+savingsDays+\"]\",      color:\"green\",      viz:\"barCell\"})\n            .addMeta({title:\"# Days With\", chartType:\"pie\"})\n    return pie\n\n  end\nend",
      "diff": "@@ -36,7 +36,9 @@   equip: modelPoint->equipRef\n   basePt: readById(modelPoint->modelRef)->pointRef\n   interval: modelPoint.get(\"hisRollupInterval\")\n-  weather: readAll(weatherStationRef==equip->siteRef->weatherStationRef and temp)\n+  weather: readById(readById(modelPoint->modelRef)->modelConfig->viewInputs->temp)//readAll(weatherStationRef==equip->siteRef->weatherStationRef and temp and not enthalpy) //Added filter to remove enthalpy as an accidentally considered point here\n+  weatherUnit: weather.get(\"unit\")\n+  if(weatherUnit == \"\u00b0F\") temp = 50\u00b0F else if (weatherUnit == \"btu/lb_dry\") temp = 28.as(\"btu/lb_dry\")\n   weatherHis: weather.hisRead(dates, {-limit}).renameCol(\"v0\",\"oat\").setColMeta(\"oat\",{chartGroup:\"a\", color:\"gray\", opacity:0.1, strokeDasharray:\"2,2\", dis:\"Outside Air Temp\"})\n   siteDis: readById(modelPoint->siteRef).dis\n   modelDis: modelPoint.dis\n"
    },
    {
      "name": "virtualBlueprintToPoint",
      "pod": "kwLinkVirtualExt",
      "pod_src": "(blueprint) => do\n\n    //remove blueprint tags\n    if (blueprint.isStr) blueprint = read(virtualBlueprint and navName==blueprint, false)\n    if (blueprint.isRef) blueprint = readById(blueprint)\n    if (blueprint.isNull) throw blueprint+\" not found\"\n    id: blueprint.get(\"id\")\n    blueprint = blueprint.remove(\"id\").remove(\"mod\")\n\n    //check for required tags\n    if (blueprint.missing(\"siteRef\")) throw \"Failed - missing siteRef\"\n    if (blueprint.missing(\"equipRef\")) throw \"Failed - missing equipRef\"\n\n    //add and convert tags\n    tz: blueprint.get(\"siteRef\").toRec.get(\"tz\")\n    virtualPointTags: blueprint.get(\"virtualPointTags\").listTagsToDict\n    if (virtualPointTags.isNull) virtualPointTags = {}\n    dict: blueprint.merge({point,\n                           virtualPoint,\n                           his,\n                           tz:tz,\n                           -virtualBlueprint,\n                           virtualPointCreator: userCur()->id,\n                           -virtualPointTags,\n                           -virtualPointOn,\n                           -virtualPointCategory,\n                           virtualBlueprintRef:id,\n                           disMacro:\"\\$siteRef \\$navName\"\n                           }.merge(virtualPointTags))\n    return dict\nend",
      "local_src": "(blueprint) => do\n\n    //remove blueprint tags\n    if (blueprint.isStr) blueprint = read(virtualBlueprint and navName==blueprint, false)\n    if (blueprint.isRef) blueprint = readById(blueprint)\n    if (blueprint.isNull) throw blueprint+\" not found\"\n    id: blueprint.get(\"id\")\n    blueprint = blueprint.remove(\"id\").remove(\"mod\")\n\n    //check for required tags\n    //if (blueprint.missing(\"siteRef\")) throw \"Failed - missing siteRef\"\n    //if (blueprint.missing(\"equipRef\")) throw \"Failed - missing equipRef\"\n\n    //add and convert tags\n    tz: blueprint.get(\"tz\")\n    virtualPointTags: blueprint.get(\"virtualPointTags\").listTagsToDict\n    if (virtualPointTags.isNull) virtualPointTags = {}\n    dict: blueprint.merge({point,\n                           virtualPoint,\n                           his,\n                           tz:tz,\n                           unit: blueprint.get(\"unit\"),\n                           siteRef: blueprint.get(\"siteRef\"),\n                           equipRef: blueprint.get(\"equipRef\"),\n                           -virtualBlueprint,\n                           virtualPointCreator: userCur()->id,\n                           -virtualPointTags,\n                           -virtualPointOn,\n                           -virtualPointCategory,\n                           virtualBlueprintRef:id,\n                           disMacro:\"\\$siteRef \\$navName\"\n                           }.merge(virtualPointTags))\n    return dict\nend",
      "diff": "@@ -8,17 +8,20 @@     blueprint = blueprint.remove(\"id\").remove(\"mod\")\n \n     //check for required tags\n-    if (blueprint.missing(\"siteRef\")) throw \"Failed - missing siteRef\"\n-    if (blueprint.missing(\"equipRef\")) throw \"Failed - missing equipRef\"\n+    //if (blueprint.missing(\"siteRef\")) throw \"Failed - missing siteRef\"\n+    //if (blueprint.missing(\"equipRef\")) throw \"Failed - missing equipRef\"\n \n     //add and convert tags\n-    tz: blueprint.get(\"siteRef\").toRec.get(\"tz\")\n+    tz: blueprint.get(\"tz\")\n     virtualPointTags: blueprint.get(\"virtualPointTags\").listTagsToDict\n     if (virtualPointTags.isNull) virtualPointTags = {}\n     dict: blueprint.merge({point,\n                            virtualPoint,\n                            his,\n                            tz:tz,\n+                           unit: blueprint.get(\"unit\"),\n+                           siteRef: blueprint.get(\"siteRef\"),\n+                           equipRef: blueprint.get(\"equipRef\"),\n                            -virtualBlueprint,\n                            virtualPointCreator: userCur()->id,\n                            -virtualPointTags,\n"
    },
    {
      "name": "virtualSyncHis",
      "pod": "kwLinkVirtualExt",
      "pod_src": "(recs, span:null, logging:false, taskId:null, heapLimit:95%) => do\n\n  //ogSpan check to use later\n  ogSpan:span\n  ogRecs: recs.toRecList.toGrid.colToList(\"id\")\n\n  //only sync persistent points\n  recs = recs.toRecList\n             .toGrid\n             .filter(virtualPointMode==\"Persistent\")\n\n  //get full list of all dependencies\n  full: virtualSyncOrder(recs).toGrid.filter(virtualPoint and virtualPointMode==\"Persistent\")\n                              .map(r => if (r.isRef) r.toRec else r)\n\n  //list for keeping track of already synced ids\n  syncedIds: []\n\n  //loop through each point\n  results: full.map() r => do\n\n    //check to see if that point was already synced.  If so continue to next point\n    if(syncedIds.contains(r.get(\"id\"))) do\n      if(logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Point has already been synced.  Proceeding to next point\")\n      return r=r\n    end\n\n    //Check Heap usage and cancel task if above 95%\n    if(taskId.isNull) log(\"warn\", {name:\"virtualSyncHis\", point:r->id}, \"No task Id provided.  Skipping Heap Check\")\n    if(taskId.isNonNull) do\n      try heapUsed: diags()[5].get(\"attrs\").last.get(\"val\") catch throw \"Please create a task user with userAllow = diags\"\n      if(heapUsed > heapLimit) taskCancel(taskId)\n    end\n\n\n    //initiate loop, log stuff, sleep\n    if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Sync Triggered\")\n    projSync()\n    hisSync()\n    if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Proj Sync\")\n\n  try do\n\n    //lookup necessary values\n    virtualFunc: r.get(\"virtualFunc\")\n\n    dependencies: r.virtualDependenciesDown\n    virtualPointOpts: r.get(\"virtualPointOpts\")\n\n    //get span to sync for (if not defined)\n    //hisStart: try dependencies.filter(hisStart).sort(\"hisStart\").first.get(\"hisStart\")\n    //          catch now()-10yr\n    //hisEnd:   try dependencies.filter(hisEnd).sortr(\"hisEnd\").first.get(\"hisEnd\")\n    //          catch now()\n    //\n    //if (span==null) span = (hisStart..hisEnd).toSpan\n    if (span==null) span = (r.get(\"hisEnd\"))..now() //TODO: need error catching here for missing hisEnd. Just use past month.\n    span = span.toSpan\n    if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id, span:span}, \"Span set\")\n\n    //initialize return values for grid\n    dependencyCount: dependencies.size\n    virtualDependencyCount: dependencies.filter(virtualPoint).size\n    virtualDependenciesSynced: dependencies.filter(virtualPoint and virtualPointMode==\"Persistent\").size\n    virtualSyncSuccess: \"failed\"\n    syncSpan: span\n    numItemsWritten: 0\n\n    //err values initialize\n    virtualErrDetails: {spanStart:span.start,\n                        spandEnd: span.end,\n                        dependencies:   try dependencies.colToList(\"id\") catch dependencies.size,\n                        virtualPointOpts: if (virtualPointOpts.isNull) \"none\" else virtualPointOpts,\n                        }\n    dependencies.each() (dep,i) => virtualErrDetails=virtualErrDetails.set(\"depDetails\"+i, [dep.get(\"hisEnd\"), dep.get(\"hisEnd\"), dep.get(\"id\")])\n\n    //recursive dependencies\n   // dependencies.each(p => do\n   //   if (p.get(\"virtualPointMode\")==\"Persistent\") do\n   //     p.virtualSyncHis(span) //todo check if this works\n   //   end\n   // end)\n\n    //now, check if all dependencies have his within span\n    dependencies.each(p => do\n      if (p.toRec.missing(\"hisEnd\"))          throw \"Dependency missing hisEnd: \"+p.dis\n      if ((p.toRec.get(\"hisEnd\") < span.start) or (p.toRec.get(\"hisStart\") > span.end)) throw \"Dependency (\"+p.dis+\") missing data within span: \"+span\n    end)\n\n    //try to do it\n    try do\n\n      //get data by running virtualFunc\n      if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id, span:span}, \"Calling \"+virtualFunc+ \" with args \")\n      data: virtualFunc.call([r, span])\n\n      //if data is dict, means it errored, throw err\n      if (data.isDict) throw if (data.has(\"virtualErr\")) data.get(\"virtualErr\") else {dis:\"logic returned dict\"}\n      if (data.isEmpty) throw \"logic returned empty\"\n      if (data.isNull) throw \"logic returned null\"\n\n      if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Data returned with size: \"+data.size)\n\n      //find all the non-null data\n      // Added != nan() check 6/24/2025\n      data = data.findAll(r => r.get(data.colNames[1]).isNonNull and r.get(data.colNames[1]) != nan() and r.get(data.colNames[1]) != na())\n\n      //check if units match\n      dataUnit: data.find(r=>r.get(data.colNames[1]).isNumber).get(data.colNames[1]).unit\n      recUnit: readById(r->id).get(\"unit\")\n      if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Comparing data/rec units: \"+dataUnit+\"/\"+recUnit)\n      if (readById(r->id).missing(\"hisOnWrite\")) do\n        if (dataUnit!=recUnit and dataUnit!=null and recUnit!=null) r = diff(readById(r->id), {unit:dataUnit}).commit\n        if (dataUnit!=recUnit and dataUnit!=null and recUnit!=null) r = diff(readById(r->id), {virtualMessage:\"WARNING - auto updated mismatched unit from \"+recUnit+\" => \"+dataUnit}, {transient}).commit\n      end\n\n      //check for nan values\n      //hasNanValues: data.find(r => r.vals.any(v=> v.isNaN))!=null\n      //if (hasNanValues) throw \"Data has NaN values\"\n\n      //clear all his for span\n      if (ogSpan==null) hisClear(r, null)\n      else              hisClear(r, span)\n    //if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Cleared data via hisClear\")\n\n      //check tz\n      if (r.get(\"tz\")!=data.first.get(\"ts\").tz) data = data.map(row=>row.set(\"ts\",row.get(\"ts\").toTimeZone(r.get(\"tz\"))))\n\n      //write data\n      futureWaitFor(hisWrite(data, r))\n      syncedIds = syncedIds.add(r.get(\"id\"))\n      if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Wrote data via hisWrite\")\n\n      //clear errors and return success\n      point: diff(readById(r->id), {-virtualErr, -virtualErrDetails, virtualStatus:\"ok\"}, {transient}).commit\n\n      //out values\n      virtualSyncSuccess=\"success\"\n      numItemsWritten= data.size\n      scope: if (ogRecs.contains(r.get(\"id\"))) \"Direct\"\n             else \"Dependency\"\n\n      pointResult: {virtualSyncSuccess:virtualSyncSuccess, numItemsWritten:numItemsWritten, dependencyCount:dependencyCount, virtualDependencyCount:virtualDependencyCount, virtualDependenciesSynced:virtualDependenciesSynced, span:span, scope:scope}\n      end\n    catch (err) do\n\n      if (logging) log(\"err\", {name:\"virtualSyncHis\", point:r->id}, \"Data Error\", err)\n\n      dependencyGrid: try dependencies.toGrid.keepCols([\"id\",\"hisEnd\",\"hisSize\",\"hisStart\",\"hisErr\",\"virtualErr\"])\n                      catch null\n\n      point: diff(readById(r->id), {virtualErr:err.dis, virtualErrDetails:virtualErrDetails, virtualStatus:\"failed\"},{transient}).commit\n\n      pointResult: {virtualSyncSuccess:virtualSyncSuccess, virtualErrDetails:virtualErrDetails, span:span}\n      end\n\n    return point.merge(pointResult)\n    end catch (err) do\n\n      if (logging) log(\"err\", {name:\"virtualSyncHis\", point:r->id}, \"General Error\", err)\n\n      //if any errors caught, update point\n      point: diff(readById(r->id), {virtualErr:err.dis, virtualErrDetails:err, virtualStatus:\"failed\"},{transient}).commit\n    end\n  end\n\n  return results.addMeta({view:\"table\"})\n                .reorderKeepCols([\"scope\",\"id\",\"virtualSyncSuccess\",\"virtualStatus\",\"virtualErr\",\"virtualErrDetails\",\"span\",\"numItemsWritten\",\"dependencyCount\",\"virtualDependencyCount\",\"virtualDependenciesSynced\",\"hisEnd\",\"hisStart\"], true)\nend\n\n//changed the error details on line 107 because virtualErrDetails is not defined outside of the scope of the initial try",
      "local_src": "(recs, span:null, logging:false, taskId:null, heapLimit:95%) => do\n\n  //ogSpan check to use later\n  ogSpan:span\n  ogRecs: recs.toRecList.toGrid.colToList(\"id\")\n\n  //only sync persistent points\n  recs = recs.toRecList\n             .toGrid\n             .filter(virtualPointMode==\"Persistent\")\n\n  //get full list of all dependencies\n  full: virtualSyncOrder(recs).toGrid.filter(virtualPoint and virtualPointMode==\"Persistent\")\n                              .map(r => if (r.isRef) r.toRec else r)\n                              \n  //list for keeping track of already synced ids\n  syncedIds: []\n\n  //loop through each point\n  results: full.map() r => do\n  \n    //check to see if that point was already synced.  If so continue to next point\n    if(syncedIds.contains(r.get(\"id\"))) do\n      if(logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Point has already been synced.  Proceeding to next point\")\n      return r=r\n    end\n    \n    //Check Heap usage and cancel task if above 95%\n    if(taskId.isNull) log(\"warn\", {name:\"virtualSyncHis\", point:r->id}, \"No task Id provided.  Skipping Heap Check\")\n    if(taskId.isNonNull) do\n      try heapUsed: diags()[5].get(\"attrs\").last.get(\"val\") catch throw \"Please create a task user with userAllow = diags\"\n      if(heapUsed > heapLimit) taskCancel(taskId)\n    end\n    \n\n    //initiate loop, log stuff, sleep\n    if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Sync Triggered\")\n    projSync()\n    hisSync()\n    if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Proj Sync\")\n\n  try do\n\n    //lookup necessary values\n    virtualFunc: r.get(\"virtualFunc\")\n\n    dependencies: r.virtualDependenciesDown\n    virtualPointOpts: r.get(\"virtualPointOpts\")\n\n    //get span to sync for (if not defined)\n    //hisStart: try dependencies.filter(hisStart).sort(\"hisStart\").first.get(\"hisStart\")\n    //          catch now()-10yr\n    //hisEnd:   try dependencies.filter(hisEnd).sortr(\"hisEnd\").first.get(\"hisEnd\")\n    //          catch now()\n    //\n    //if (span==null) span = (hisStart..hisEnd).toSpan\n    if (span==null) span = (r.get(\"hisEnd\"))..now() //TODO: need error catching here for missing hisEnd. Just use past month.\n    span = span.toSpan\n    if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id, span:span}, \"Span set\")\n\n    //initialize return values for grid\n    dependencyCount: dependencies.size\n    virtualDependencyCount: dependencies.filter(virtualPoint).size\n    virtualDependenciesSynced: dependencies.filter(virtualPoint and virtualPointMode==\"Persistent\").size\n    virtualSyncSuccess: \"failed\"\n    syncSpan: span\n    numItemsWritten: 0\n\n    //err values initialize\n    virtualErrDetails: {spanStart:span.start,\n                        spandEnd: span.end,\n                        dependencies:   try dependencies.colToList(\"id\") catch dependencies.size,\n                        virtualPointOpts: if (virtualPointOpts.isNull) \"none\" else virtualPointOpts,\n                        }\n    dependencies.each() (dep,i) => virtualErrDetails=virtualErrDetails.set(\"depDetails\"+i, [dep.get(\"hisEnd\"), dep.get(\"hisEnd\"), dep.get(\"id\")])\n\n    //recursive dependencies\n   // dependencies.each(p => do\n   //   if (p.get(\"virtualPointMode\")==\"Persistent\") do\n   //     p.virtualSyncHis(span) //todo check if this works\n   //   end\n   // end)\n\n    //now, check if all dependencies have his within span\n    dependencies.each(p => do\n      if (p.toRec.missing(\"hisEnd\"))          throw \"Dependency missing hisEnd: \"+p.dis\n      if ((p.toRec.get(\"hisEnd\") < span.start) or (p.toRec.get(\"hisStart\") > span.end)) throw \"Dependency (\"+p.dis+\") missing data within span: \"+span\n    end)\n\n    //try to do it\n    try do\n\n      //get data by running virtualFunc\n      if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id, span:span}, \"Calling \"+virtualFunc+ \" with args \")\n      data: virtualFunc.call([r, span])\n\n      //if data is dict, means it errored, throw err\n      if (data.isDict) throw if (data.has(\"virtualErr\")) data.get(\"virtualErr\") else {dis:\"logic returned dict\"}\n      if (data.isEmpty) throw \"logic returned empty\"\n      if (data.isNull) throw \"logic returned null\"\n\n      if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Data returned with size: \"+data.size)\n\n      //find all the non-null data\n      // Added != nan() check 6/24/2025\n      if(data.isList) data = data.toGrid.reorderKeepCols([\"ts\"])\n      data = data.findAll(r => r.get(data.colNames[1]).isNonNull and r.get(data.colNames[1]) != nan() and r.get(data.colNames[1]) != na())\n\n      //check if units match\n      dataUnit: data.find(r=>r.get(data.colNames[1]).isNumber).get(data.colNames[1]).unit\n      recUnit: readById(r->id).get(\"unit\")\n      if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Comparing data/rec units: \"+dataUnit+\"/\"+recUnit)\n      if (readById(r->id).missing(\"hisOnWrite\")) do\n        if (dataUnit!=recUnit and dataUnit!=null and recUnit!=null) r = diff(readById(r->id), {unit:dataUnit}).commit\n        if (dataUnit!=recUnit and dataUnit!=null and recUnit!=null) r = diff(readById(r->id), {virtualMessage:\"WARNING - auto updated mismatched unit from \"+recUnit+\" => \"+dataUnit}, {transient}).commit\n      end\n\n      //check for nan values\n      //hasNanValues: data.find(r => r.vals.any(v=> v.isNaN))!=null\n      //if (hasNanValues) throw \"Data has NaN values\"\n\n      //clear all his for span\n      if (ogSpan==null) hisClear(r, null)\n      else              hisClear(r, span)\n    //if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Cleared data via hisClear\")\n\n      //check tz \n      if (r.get(\"tz\")!=data.first.get(\"ts\").tz) data = data.map(row=>row.set(\"ts\",row.get(\"ts\").toTimeZone(r.get(\"tz\"))))\n      \n      //write data\n      futureWaitFor(hisWrite(data, r))\n      syncedIds = syncedIds.add(r.get(\"id\"))\n      if (logging) log(\"info\", {name:\"virtualSyncHis\", point:r->id}, \"Wrote data via hisWrite\")\n\n      //clear errors and return success\n      point: diff(readById(r->id), {-virtualErr, -virtualErrDetails, virtualStatus:\"ok\"}, {transient}).commit\n\n      //out values\n      virtualSyncSuccess=\"success\"\n      numItemsWritten= data.size\n      scope: if (ogRecs.contains(r.get(\"id\"))) \"Direct\"\n             else \"Dependency\"\n\n      pointResult: {virtualSyncSuccess:virtualSyncSuccess, numItemsWritten:numItemsWritten, dependencyCount:dependencyCount, virtualDependencyCount:virtualDependencyCount, virtualDependenciesSynced:virtualDependenciesSynced, span:span, scope:scope}\n      end\n    catch (err) do\n\n      if (logging) log(\"err\", {name:\"virtualSyncHis\", point:r->id}, \"Data Error\", err)\n\n      dependencyGrid: try dependencies.toGrid.keepCols([\"id\",\"hisEnd\",\"hisSize\",\"hisStart\",\"hisErr\",\"virtualErr\"])\n                      catch null\n\n      point: diff(readById(r->id), {virtualErr:err.dis, virtualErrDetails:virtualErrDetails, virtualStatus:\"failed\"},{transient}).commit\n\n      pointResult: {virtualSyncSuccess:virtualSyncSuccess, virtualErrDetails:virtualErrDetails, span:span}\n      end\n\n    return point.merge(pointResult)\n    end catch (err) do\n\n      if (logging) log(\"err\", {name:\"virtualSyncHis\", point:r->id}, \"General Error\", err)\n\n      //if any errors caught, update point\n      point: diff(readById(r->id), {virtualErr:err.dis, virtualErrDetails:err, virtualStatus:\"failed\"},{transient}).commit\n    end\n  end\n\n  return results.addMeta({view:\"table\"})\n                .reorderKeepCols([\"scope\",\"id\",\"virtualSyncSuccess\",\"virtualStatus\",\"virtualErr\",\"virtualErrDetails\",\"span\",\"numItemsWritten\",\"dependencyCount\",\"virtualDependencyCount\",\"virtualDependenciesSynced\",\"hisEnd\",\"hisStart\"], true)\nend\n\n//changed the error details on line 107 because virtualErrDetails is not defined outside of the scope of the initial try",
      "diff": "@@ -103,6 +103,7 @@ \n       //find all the non-null data\n       // Added != nan() check 6/24/2025\n+      if(data.isList) data = data.toGrid.reorderKeepCols([\"ts\"])\n       data = data.findAll(r => r.get(data.colNames[1]).isNonNull and r.get(data.colNames[1]) != nan() and r.get(data.colNames[1]) != na())\n \n       //check if units match\n"
    },
    {
      "name": "virtual_kwModeling_forecast_syncModelPointPredictions_timegpt",
      "pod": "kwLinkModelExt",
      "pod_src": "//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   16 Sep 24   Apoorv Khanuja        - Creation.\n//\n// Description\n//   This virtualFunc reads in the modelPoint rec and the dates to get the predictions for and outputs a hisGrid with the predictions for those dates.\n//\n// Parameters\n//   Name                 Type(s)             DefVal\n//   modelPointRec        Dict/Ref            NA\n//   dates                dateSpan            NA\n//   opts                 Dict                {debug:false}\n//\n// Returns\n//   Type           Description\n//   hisGrid        - Just return predictions\n//\n\n(modelPointRec, dates, opts:{}) => do\n\n  // Normalize inputs\n  modelPointRec = modelPointRec.toRec\n  dates         = dates.toSpan\n\n  // Handle Opts\n  debug: optNorm(opts, \"debug\", false)\n\n  modelPointRef  : modelPointRec.get(\"id\")\n  modelConfigRec : try modelPointRec.get(\"modelConfigRef\").toRec catch return diff(modelPointRec, {virtualErr:\"No modelConfig rec associated with the modelPoint\"}, {transient}).commit\n  modelConfigRef : modelConfigRec.get(\"id\")\n  siteTZ: modelPointRec.get(\"siteRef\").toRec.get(\"tz\")\n\n  // Extract modelActivationConfig\n  try modelActivationConfig : modelPointRec.get(\"virtualPointOpts\")     catch return diff(modelPointRec, {virtualErr:\"virtualPointOpts not defined for the modelPoint\"}, {transient}).commit\n  try virtualSyncFrequency  : modelPointRec.get(\"virtualSyncFrequency\") catch return diff(modelPointRec, {virtualErr:\"virtualSyncFrequency not defined for the modelPoint\"}, {transient}).commit\n  forecastLength            : modelActivationConfig.get(\"forecastLength\")\n  trainLength               : modelActivationConfig.get(\"trainLength\")\n  trainStartDateOverride    : if (modelPointRec.has(\"trainStartDateOverride\")) modelPointRec.get(\"trainStartDateOverride\") else null\n\n///////////////// Create a grid of fake modelConfig dicts for 1 week predictions ///////////////////////////////////////////////////\n\n  endTimestamp: dates.end //(dates.end.toStr + \" 00:00:00\").parseDateTime(\"YYYY-MM-DD hh:mm:SS\")\n  timestampList: hisSlidingWindows(dates, forecastLength, virtualSyncFrequency).colToList(\"ts\")\n\n  if (timestampList.last + forecastLength < endTimestamp) timestampList = timestampList.add(timestampList.last + forecastLength)\n\n  spans: []\n  timestampList.each r=> do\n    spanEnd: if (r + forecastLength > endTimestamp) endTimestamp else r + forecastLength\n    spanX  : if (trainStartDateOverride.isNull) ((r - trainLength)..spanEnd).toSpan  else (trainStartDateOverride..spanEnd).toSpan\n    spans  = spans.add(spanX)\n  end\n\n\n  fakeModelConfigRec     : modelConfigRec.remove(\"id\").remove(\"mod\").remove(\"modelConfig2\")\n  fakeModelConfigRecGrid : {}.toGrid\n\n  timestampList.each() (r,idx) => do\n    newModelParameters     : {testTrainSplitDate: r, span:spans.get(idx)}\n    updatedModelParameters : fakeModelConfigRec.get(\"modelParameters\").merge(newModelParameters)\n    newModelConfig         : fakeModelConfigRec.set(\"modelParameters\", updatedModelParameters)\n    fakeModelConfigRecGrid = fakeModelConfigRecGrid.addRow(newModelConfig)\n  end\n\n  if (debug==true) return fakeModelConfigRecGrid\n  logInfo(\"modelForecastingVirtualFunc\", \"Model Config Rec Grid generated. Size: \" + fakeModelConfigRecGrid.size.toStr)\n\n\n///////////////// Map through the fakeModelConfigRecGrid and runModel for each row /////////////////////////////////////////////////////\n\n  detailedResults: {}.toGrid\n  counter: 0\n\n  fakeModelConfigRecGrid.each() r=> do   //// Errors show up in Tasks>Debug but not Debug>Logs. (Think through which task errors we want to show up in logs)\n    try do\n      resultsX: callTask(logic_kwModeling_runModel, [r, opts:{commitToRec:false, detailedResults:true}], getVal: true)\n      r=r.remove(\"modelUseState\").remove(\"modelEngine\").remove(\"mlModel\").remove(\"modelType\").remove(\"modelPointStatus\").remove(\"createdBy\").remove(\"syntheticModel\").remove(\"modelRunState\")\n      resultsX = r.merge(resultsX)\n    end catch return modelConfigRec = diff(modelConfigRec, {modelPointStatus:\"error\"}, {transient}).commit\n    if (not resultsX.isStr) detailedResults = detailedResults.addRow(resultsX)\n    else                    detailedResults = detailedResults\n    counter = counter + 1\n    logInfo(\"modelForecastingVirtualFunc\", \"Run #\" + counter.toStr + \" Dates: \" + r.get(\"modelParameters\").get(\"span\").start.date.toStr + \" to \"  + r.get(\"modelParameters\").get(\"span\").end.date.toStr + \". Current out size: \" + resultsX.get(\"modelPredictions\").size.toStr)\n  end\n\n  logInfo(\"modelForecastingVirtualFunc\", \"Attempting to save DetailedModelResults to Zinc file. Size: \" + detailedResults.size.toStr)\n  detailedResults = logic_kwModeling_saveDetailedModelResults(modelPointRec, detailedResults) // Also takes care of renaming the col to val\n  logInfo(\"modelForecastingVirtualFunc\", \"Saved DetailedModelResults to Zinc file.\")\n\n  out: {}.toGrid\n\n  // Extract the predictions from the modelResults to write to modelPoint\n  // TODO: might need a try catch somewhere\n  detailedResults.each() r=> do\n    predictions: r.get(\"modelPredictions\")\n    out = out.addRows(predictions)\n  end\n\n  logInfo(\"modelForecastingVirtualFunc\", \"All model Configs Run. Output size: \" + out.size.toStr)\n  if (out.isEmpty or out.isNull) return out\n\n  // Update the modelPointStatus in the modelConfigRec\n  //TODO: NEEDED TO COMMENT OUT THESE CHECKS FOR THE MODELS TO RUN FOR ADMING AND RCD BUILDINGS -> ///\n  ///if      ((fakeModelConfigRecGrid.size==53 and out.size==8760)       or (fakeModelConfigRecGrid.size==1 and out.size==168))       modelConfigRec = diff(modelConfigRec, {modelPointStatus:\"ok\"}, {transient}).commit\n  ///else if ((fakeModelConfigRecGrid.size==53 and (not out.size==8760)) or (fakeModelConfigRecGrid.size==1 and (not out.size==168))) modelConfigRec = diff(modelConfigRec, {modelPointStatus:\"warn\"}, {transient}).commit\n\n  /// Check for completely empty predictions\n  ///if      (modelConfigRec.get(\"modelEngine\")==\"timegpt\") modelPredictions: out.isColBlank(\"timegpt\")   /// doublecheck for 2024, fails if there's no col called timegpt.\n  ///else if (modelConfigRec.get(\"modelEngine\")==\"nmecpy\")  modelPredictions: out.isColBlank(\"y_fit\")\n\n  ///if (modelPredictions==true) return diff(modelPointRec, {virtualErr:\"Target Variable (meter point) missing hisData for the given dates.\"}, {transient}).commit\n\n\n///////////////// Save the additional data to modelPoint results ///////////////////////////////////////////////////////////////////////\n\n  ///if      (modelConfigRec.get(\"modelEngine\")==\"timegpt\") out = out.removeCol(\"v0\")\n  ///else if (modelConfigRec.get(\"modelEngine\")==\"nmecpy\")  out = out.removeCol(\"load\")\n  out = out.removeCol(\"v0\") ///Added to replace the line two above\n\n  logInfo(\"modelForecastingVirtualFunc\", \"Attempting to save confidence intervals to Zinc file. Size: \" + out.size.toStr)\n  out = logic_kwModeling_saveModelPointResults(modelPointRec, out) // Also takes care of renaming the col to val\n  logInfo(\"modelForecastingVirtualFunc\", \"Saved confidence intervals to Zinc file.\")\n\n  return out\n\n///////////////\n//// TODO 2.0: Get the percent of missing hisData for exoVars and targetVar for the new dates\n//// getRawHis already handles if the entire hisData for targetVar is missing (but still goes through for missing exoVar data)\n///////////////\n\n  //TODO: sep logic for tgpt vs nmecpy;; Just different modelParameters: span + testTrainSplitDate vs. trainDates and testDates, and different activationConfigs\n  //virtual_kwModeling_forecast_syncModelPointPredictions\n  //virtual_kwModeling_forecast_syncModelPointPredictions_nmecpy\n\nend",
      "local_src": "//\n// Copyright (c) 2024, kW Engineering\n// All Rights Reserved\n//\n// History\n//   Date:       Name:                 Description:\n//   16 Sep 24   Apoorv Khanuja        - Creation.\n//\n// Description\n//   This virtualFunc reads in the modelPoint rec and the dates to get the predictions for and outputs a hisGrid with the predictions for those dates.\n//\n// Parameters\n//   Name                 Type(s)             DefVal\n//   modelPointRec        Dict/Ref            NA\n//   dates                dateSpan            NA\n//   opts                 Dict                {debug:false}\n//\n// Returns\n//   Type           Description\n//   hisGrid        - Just return predictions\n//\n\n(modelPointRec, dates, opts:{}) => do\n\n  // Normalize inputs\n  modelPointRec = modelPointRec.toRec\n  dates         = dates.toSpan\n\n  // Handle Opts\n  debug: optNorm(opts, \"debug\", false)\n\n  modelPointRef  : modelPointRec.get(\"id\")\n  modelConfigRec : try modelPointRec.get(\"modelConfigRef\").toRec catch return diff(modelPointRec, {virtualErr:\"No modelConfig rec associated with the modelPoint\"}, {transient}).commit\n  modelConfigRef : modelConfigRec.get(\"id\")\n  siteTZ: modelPointRec.get(\"siteRef\").toRec.get(\"tz\")\n\n  // Extract modelActivationConfig\n  try modelActivationConfig : modelPointRec.get(\"virtualPointOpts\")     catch return diff(modelPointRec, {virtualErr:\"virtualPointOpts not defined for the modelPoint\"}, {transient}).commit\n  try virtualSyncFrequency  : modelPointRec.get(\"virtualSyncFrequency\") catch return diff(modelPointRec, {virtualErr:\"virtualSyncFrequency not defined for the modelPoint\"}, {transient}).commit\n  forecastLength            : modelActivationConfig.get(\"forecastLength\")\n  trainLength               : modelActivationConfig.get(\"trainLength\")\n  trainStartDateOverride    : if (modelPointRec.has(\"trainStartDateOverride\")) modelPointRec.get(\"trainStartDateOverride\") else null\n\n///////////////// Create a grid of fake modelConfig dicts for 1 week predictions ///////////////////////////////////////////////////\n\n  endTimestamp: dates.end //(dates.end.toStr + \" 00:00:00\").parseDateTime(\"YYYY-MM-DD hh:mm:SS\")\n  timestampList: hisSlidingWindows(dates, forecastLength, virtualSyncFrequency).colToList(\"ts\")\n\n  if (timestampList.last + forecastLength < endTimestamp) timestampList = timestampList.add(timestampList.last + forecastLength)\n\n  spans: []\n  timestampList.each r=> do\n    spanEnd: if (r + forecastLength > endTimestamp) endTimestamp else r + forecastLength\n    spanX  : if (trainStartDateOverride.isNull) ((r - trainLength)..spanEnd).toSpan  else (trainStartDateOverride..spanEnd).toSpan\n    spans  = spans.add(spanX)\n  end\n\n  fakeModelConfigRec     : modelConfigRec.remove(\"id\").remove(\"mod\").remove(\"modelConfig2\")\n  fakeModelConfigRecGrid : {}.toGrid\n\n  timestampList.each() (r,idx) => do\n    newModelParameters     : {testTrainSplitDate: r, span:spans.get(idx)}\n    updatedModelParameters : fakeModelConfigRec.get(\"modelParameters\").merge(newModelParameters)\n    newModelConfig         : fakeModelConfigRec.set(\"modelParameters\", updatedModelParameters)\n    fakeModelConfigRecGrid = fakeModelConfigRecGrid.addRow(newModelConfig)\n  end\n\n  if (debug==true) return fakeModelConfigRecGrid\n  logInfo(\"modelForecastingVirtualFunc\", \"Model Config Rec Grid generated. Size: \" + fakeModelConfigRecGrid.size.toStr)\n\n\n///////////////// Map through the fakeModelConfigRecGrid and runModel for each row /////////////////////////////////////////////////////\n\n  detailedResults: {}.toGrid\n  counter: 0\n\n  fakeModelConfigRecGrid.each() r=> do   //// Errors show up in Tasks>Debug but not Debug>Logs. (Think through which task errors we want to show up in logs)\n    try do\n      resultsX: callTask(logic_kwModeling_runModel, [r, opts:{commitToRec:false, detailedResults:true}], getVal: true)\n      r=r.remove(\"modelUseState\").remove(\"modelEngine\").remove(\"mlModel\").remove(\"modelType\").remove(\"modelPointStatus\").remove(\"createdBy\").remove(\"syntheticModel\").remove(\"modelRunState\")\n      resultsX = r.merge(resultsX)\n    end catch return modelConfigRec = diff(modelConfigRec, {modelPointStatus:\"error\"}, {transient}).commit\n    if (not resultsX.isStr) detailedResults = detailedResults.addRow(resultsX) \n    else                    detailedResults = detailedResults\n    counter = counter + 1\n    logInfo(\"modelForecastingVirtualFunc\", \"Run #\" + counter.toStr + \" Dates: \" + r.get(\"modelParameters\").get(\"span\").start.date.toStr + \" to \"  + r.get(\"modelParameters\").get(\"span\").end.date.toStr + \". Current out size: \" + resultsX.get(\"modelPredictions\").size.toStr)\n  end\n\n  logInfo(\"modelForecastingVirtualFunc\", \"Attempting to save DetailedModelResults to Zinc file. Size: \" + detailedResults.size.toStr)\n  detailedResults = logic_kwModeling_saveDetailedModelResults(modelPointRec, detailedResults) // Also takes care of renaming the col to val\n  logInfo(\"modelForecastingVirtualFunc\", \"Saved DetailedModelResults to Zinc file.\")\n\n  out: {}.toGrid\n  \n  // Extract the predictions from the modelResults to write to modelPoint\n  // TODO: might need a try catch somewhere\n  detailedResults.each() r=> do \n    predictions: r.get(\"modelPredictions\")\n    out = out.addRows(predictions)\n  end\n  \n  logInfo(\"modelForecastingVirtualFunc\", \"All model Configs Run. Output size: \" + out.size.toStr)\n  if (out.isEmpty or out.isNull) return out\n\n  // Update the modelPointStatus in the modelConfigRec\n  //TODO: NEEDED TO COMMENT OUT THESE CHECKS FOR THE MODELS TO RUN FOR ADMING AND RCD BUILDINGS -> ///\n  ///if      ((fakeModelConfigRecGrid.size==53 and out.size==8760)       or (fakeModelConfigRecGrid.size==1 and out.size==168))       modelConfigRec = diff(modelConfigRec, {modelPointStatus:\"ok\"}, {transient}).commit\n  ///else if ((fakeModelConfigRecGrid.size==53 and (not out.size==8760)) or (fakeModelConfigRecGrid.size==1 and (not out.size==168))) modelConfigRec = diff(modelConfigRec, {modelPointStatus:\"warn\"}, {transient}).commit\n\n  /// Check for completely empty predictions\n  ///if      (modelConfigRec.get(\"modelEngine\")==\"timegpt\") modelPredictions: out.isColBlank(\"timegpt\")   /// doublecheck for 2024, fails if there's no col called timegpt.\n  ///else if (modelConfigRec.get(\"modelEngine\")==\"nmecpy\")  modelPredictions: out.isColBlank(\"y_fit\")\n\n  ///if (modelPredictions==true) return diff(modelPointRec, {virtualErr:\"Target Variable (meter point) missing hisData for the given dates.\"}, {transient}).commit\n\n\n///////////////// Save the additional data to modelPoint results ///////////////////////////////////////////////////////////////////////\n\n  ///if      (modelConfigRec.get(\"modelEngine\")==\"timegpt\") out = out.removeCol(\"v0\")\n  ///else if (modelConfigRec.get(\"modelEngine\")==\"nmecpy\")  out = out.removeCol(\"load\")\n  out = out.removeCol(\"v0\") ///Added to replace the line two above\n\n  logInfo(\"modelForecastingVirtualFunc\", \"Attempting to save confidence intervals to Zinc file. Size: \" + out.size.toStr)\n  out = logic_kwModeling_saveModelPointResults(modelPointRec, out) // Also takes care of renaming the col to val\n  logInfo(\"modelForecastingVirtualFunc\", \"Saved confidence intervals to Zinc file.\")\n\n  return out\n\n///////////////\n//// TODO 2.0: Get the percent of missing hisData for exoVars and targetVar for the new dates\n//// getRawHis already handles if the entire hisData for targetVar is missing (but still goes through for missing exoVar data)\n///////////////\n\n  //TODO: sep logic for tgpt vs nmecpy;; Just different modelParameters: span + testTrainSplitDate vs. trainDates and testDates, and different activationConfigs\n  //virtual_kwModeling_forecast_syncModelPointPredictions\n  //virtual_kwModeling_forecast_syncModelPointPredictions_nmecpy\n\nend",
      "diff": "@@ -54,7 +54,6 @@     spanX  : if (trainStartDateOverride.isNull) ((r - trainLength)..spanEnd).toSpan  else (trainStartDateOverride..spanEnd).toSpan\n     spans  = spans.add(spanX)\n   end\n-\n \n   fakeModelConfigRec     : modelConfigRec.remove(\"id\").remove(\"mod\").remove(\"modelConfig2\")\n   fakeModelConfigRecGrid : {}.toGrid\n"
    },
    {
      "name": "wdg_ahu_table_summaryAnalytics",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "////////////////////////////////////////////////////////////////////////////\n/////////////////              DOCS                    /////////////////////\n////////////////////////////////////////////////////////////////////////////\n\n//  Author: James Gessel\n//  Created: Feb 2023\n//  Updated:\n//  By Who:       Date:     Description:\n//  Will          04/03/24  Update datResetting and dspResetting display values\n//  Will          05/29/24  Add % time economizing properly to ahu table\n//  Will          02/07/25  Add Count of Zones Occupied 24/7\n\n//  Docs: Widget that shows summary analytics for all ahus on a site\n//          -fan runtimes\n//          -hw chw valve open times\n//          -both valves open time\n//          -dat/dsp resets\n//          -dat/dsp meeting sp\n//\n//  Logic:\n//    Loop through each ahu, find relevant points, rollup the data\n\n//  Pod: kwlink pro\n\n//  INPUTS:\n//    site: single site\n//    options:\n\n//  OUTPUTS:\n//    out: table with a bunch of analytics for each AHU\n\n//  TODO:\n//\n\n(site, dates, opts:{}, d:false) => do\n\n  //validate and authorize current user\n  validateUser()\n\n  //normalize inputs\n  siteRec: site.getRecord\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  rollupOverride:   if (opts.has(\"rollupOverride\")) opts.get(\"rollupOverride\") else \"Auto\"\n                    if (rollupOverride==\"Auto\") rollupOverride=null\n  occOnly:      if (opts.has(\"occOnly\")) opts.get(\"occOnly\") else true\n  debug:         opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  ahuTableSortBy: if (opts.has(\"ahuTableSortBy\")) opts.get(\"ahuTableSortBy\") else \"id\"\n  showEmptyColumns: if (opts.has(\"showEmptyColumns\")) opts.get(\"showEmptyColumns\") else false\n  noDataValue: if (showEmptyColumns) \"\" else null\n  coldTemp: if (opts.has(\"coldTemp\")) opts.get(\"coldTemp\") else 45\n  hotTemp: if (opts.has(\"hotTemp\")) opts.get(\"hotTemp\") else 70\n\n  //formatting\n  fanColor: kwLinkColors(\"fan speed\")\n  chwColor: kwLinkColors(\"chilled water\")\n  hwColor:  kwLinkColors(\"hot water\")\n  sparkColor: kwLinkColors(\"general spark\")\n  meetingSpColor: \"green\"\n\n  title: \"AHU Summary - \"+siteRec.dis\n  subtitle: null\n  colOrder:      if (opts.has(\"colOrder\")) opts.get(\"colOrder\")\n                 else [\"id\",\n                       \"supplyFanRuntime\",\n                       \"datResetting\",\n                       \"dspResetting\",\n                       \"datMeetingSp\",\n                       \"dspMeetingSp\",\n                       \"avgSupplyFan\",\n                       \"avgReturnFan\",\n                       \"avgReliefFan\",\n                       \"avgExhaustFan\",\n                       \"avgHwValve\",\n                       \"hwValveOpenHot\",\n                       \"avgChwValve\",\n                       \"chwValveOpenCold\",\n                       \"simultaneousHeatingCooling\",\n                       \"occupied247\",\n                       \"sparks\",\n                       \"economizingProperly\"\n                       ]\n\n  //calculate total hours\n  hours: (dates.end - dates.start).to(1hr)\n\n  //AHU LOOP\n  units: readAll((mau or ahu or rtu) and equip and siteRef==site->id and not ahuSummaryNoShow)\n  table: units.map row => do\n\n    //normalize input\n    id: row->id\n\n    //get points\n    requiredPoints: []\n    optionalPoints: [\"occ_cmd\",\"daFan_run\",\"daFan_speed\",\"dat_sensor\",\"dat_sp\",\"dsp_sensor\", \"dsp_sp\", \"clgValve_cmd\",\"htgValve_cmd\",\"phtValve_cmd\",\"daFan_speed\",\"rtnFan_speed\",\"rlfFan_speed\",\"exhFan_speed\",\"siteOcc_cmd\"]\n    points: retrievePoints(requiredPoints, optionalPoints, {equipRef:id})\n    if (debug and opts.get(\"debug\")==\"Points\") return points\n    if (not points.get(\"htgValve_cmd\").isRef and points.get(\"phtValve_cmd\").isRef) points=points.set(\"htgValve_cmd\",points.get(\"phtValve_cmd\"))\n\n    if (debug and opts.get(\"debug\")==\"His\") return points.retrieveHis(dates, {rollupOverride:rollupOverride, hisSizeOnly})\n\n    //get occ periods\n    occPeriods: try logic_ahu_occPeriods(id, dates, opts, points) catch null\n    occHours: if (occPeriods.isGrid) occPeriods.hisClip.foldCol(\"v0\",sum)\n              else noDataValue\n    if (not occOnly) occPeriods=null\n    curOpts: opts.merge({occPeriods:occPeriods})\n\n    //calculate analytics\n    avgSupplyFan: try\n                    if (points.get(\"daFan_speed\").isRef)\n                      logic_ahu_pointRollup(points.get(\"daFan_speed\"), dates, avg, curOpts)\n                    else points.get(\"daFan_speed\")\n                  catch (err) err.get(\"dis\")\n                  if (not debug and avgSupplyFan.isStr) avgSupplyFan=noDataValue\n\n    avgReturnFan: try\n                    if (points.get(\"rtnFan_speed\").isRef)\n                      logic_ahu_pointRollup(points.get(\"rtnFan_speed\"), dates, avg, curOpts)\n                    else points.get(\"rtnFan_speed\")\n                  catch (err) err.get(\"dis\")\n                  if (not debug and avgReturnFan.isStr) avgReturnFan=noDataValue\n\n    avgReliefFan:  try\n                     if (points.get(\"rlfFan_speed\").isRef)\n                       logic_ahu_pointRollup(points.get(\"rlfFan_speed\"), dates, avg, curOpts)\n                     else points.get(\"rlfFan_speed\")\n                   catch (err) err.get(\"dis\")\n                   if (not debug and avgReliefFan.isStr) avgReliefFan=noDataValue\n\n    avgExhaustFan: try\n                    if (points.get(\"exhFan_speed\").isRef)\n                     logic_ahu_pointRollup(points.get(\"exhFan_speed\"), dates, avg, curOpts)\n                   else points.get(\"exhFan_speed\")\n                   catch (err) err.get(\"dis\")\n                   if (not debug and avgExhaustFan.isStr) avgExhaustFan=noDataValue\n\n    avgHwValve:    try\n                    if (points.get(\"htgValve_cmd\").isRef)\n                     logic_ahu_pointRollup(points.get(\"htgValve_cmd\"), dates, avg, curOpts)\n                   else points.get(\"htgValve_cmd\")\n                   catch (err) err.get(\"dis\")\n                   if (not debug and avgHwValve.isStr) avgHwValve=noDataValue\n\n    avgChwValve:   try\n                    if (points.get(\"clgValve_cmd\").isRef)\n                     logic_ahu_pointRollup(points.get(\"clgValve_cmd\"), dates, avg, curOpts)\n                   else points.get(\"clgValve_cmd\")\n                   catch (err) err.get(\"dis\")\n                   if (not debug and avgChwValve.isStr) avgChwValve=noDataValue\n\n    hwValveOpenHot: try\n                    if (points.get(\"htgValve_cmd\").isRef)\n                     logic_ahu_hwValveOpenHot(id, dates, curOpts, points).round\n                    else \"missing required points\"\n                   catch (err) err.get(\"dis\")\n                    if (not debug and hwValveOpenHot.isStr) hwValveOpenHot=noDataValue\n\n    chwValveOpenCold: try\n                    if (points.get(\"clgValve_cmd\").isRef)\n                        logic_ahu_chwValveOpenCold(id, dates, curOpts, points).round\n                      else points.get(\"clgValve_cmd\")\n                   catch (err) err.get(\"dis\")\n                      if (not debug and chwValveOpenCold.isStr) chwValveOpenCold=noDataValue\n\n    simultaneousHeatingCooling:  try\n                                  if (points.get(\"clgValve_cmd\").isRef and points.get(\"htgValve_cmd\").isRef)\n                                    logic_ahu_simultHeatCool(id, dates, curOpts, points).meta.get(\"percTime\")\n                                  else \"missing required points\"\n                                 catch (err) err.get(\"dis\")\n                                 if (not debug and simultaneousHeatingCooling.isStr) simultaneousHeatingCooling=noDataValue\n\n    datResetting: if(row.has(\"noDatReset\")) \"notApplicable\"\n                  else do\n                    try\n                      if (points.get(\"dat_sensor\").isRef) do\n                        temp : logic_ahu_datResetting(id, dates, curOpts, points)\n                        if(temp.isStr) temp\n                        else temp.meta.get(\"datResetting\")\n                      end\n                      else \"missingPoint\"\n                    catch (err) err.dis//\"missingPoint\"\n                  end\n\n    dspResetting: if(row.has(\"noDspReset\")) \"notApplicable\"\n                  else do\n                    try\n                      if (points.get(\"dsp_sensor\").isRef) do\n                        temp1: logic_ahu_dspResetting(id, dates, curOpts, points)\n                        if(temp1.isStr) temp1\n                        else temp1.meta.get(\"dspResetting\")\n                      end\n                      else \"missingPoint\"\n                    catch (err) err.dis//\"missingPoint\"\n                  end\n\n    datMeetingSp: try\n                    if (points.get(\"dat_sensor\").isRef and points.get(\"dat_sp\").isRef)\n                    logic_ahu_datMeetingSp(id, dates, curOpts, points).meta.get(\"datMeetingSp\")\n                  else \"missing required points\"\n                   catch (err) err.get(\"dis\")\n                  if (not debug and datMeetingSp.isStr) datMeetingSp=noDataValue\n\n    dspMeetingSp: try\n                    if (points.get(\"dsp_sensor\").isRef and points.get(\"dsp_sp\").isRef)\n                    logic_ahu_dspMeetingSp(id, dates, curOpts, points).meta.get(\"dspMeetingSp\")\n                  else \"missing required points\"\n                   catch (err) err.get(\"dis\")\n                  if (not debug and dspMeetingSp.isStr) dspMeetingSp=noDataValue\nif (d) do\nb: points.get(\"daFan_run\")\nc: points.get(\"daFan_speed\")\na: try logic_ahu_pointRollup(points.get(\"daFan_run\"), dates, \"runtime\", {occPeriods:occPeriods, runtimePercent}) catch (err) err\nreturn {a:a, b:b, c:c}\nend\n    supplyFanRuntime: try\n                        if (points.get(\"daFan_run\").isRef)\n                          logic_ahu_pointRollup(points.get(\"daFan_run\"), dates, \"runtime\", {occPeriods:occPeriods, runtimePercent})\n                        else if (points.get(\"daFan_speed\").isRef)\n                          logic_ahu_pointRollup(points.get(\"daFan_speed\"), dates, \"runtime\", {occPeriods:occPeriods, runtimePercent})\n                        else \"missing required points\"\n                      catch (err) err.get(\"dis\")\n                      if (not debug and supplyFanRuntime.isStr) supplyFanRuntime=noDataValue\n\n    economizingProperly: try\n                           logic_ahu_economizingProperly(id, dates, curOpts)\n                         catch(err) err.get(\"dis\")\n                         if (not debug and economizingProperly.isStr) economizingProperly=noDataValue\n    economizingProperly = if(economizingProperly.isGrid) economizingProperly.meta.get(\"economizingProperly\").as(1%) else null\n\n    rules: readAll(rule and not disabled)\n    targets: id.toRec.equipToPoints.toGrid.addRow(id.toRec)\n    sparks: ruleSparksCount(targets, dates, rules)\n\n    occupied247: try\n                  logic_zones_occupancyCount(id, dates, curOpts)\n                 catch\n                   \"Couldn't Calculate\"\n\n    //put together dict\n    analytics: {id: id,\n                avgSupplyFan:              if (avgSupplyFan.isNumber) avgSupplyFan.round else avgSupplyFan,\n                avgReturnFan:              if (avgReturnFan.isNumber) avgReturnFan.round else avgReturnFan,\n                avgReliefFan:              if (avgReliefFan.isNumber) avgReliefFan.round else avgReliefFan,\n                avgExhaustFan:             if (avgExhaustFan.isNumber) avgExhaustFan.round else avgExhaustFan,\n                avgHwValve:                if (avgHwValve.isNumber) avgHwValve.round else avgHwValve,\n                avgChwValve:               if (avgChwValve.isNumber) avgChwValve.round else avgChwValve,\n                hwValveOpenHot:            if (hwValveOpenHot.isNumber) hwValveOpenHot.round else hwValveOpenHot,\n                chwValveOpenCold:          if (chwValveOpenCold.isNumber) chwValveOpenCold.round else chwValveOpenCold,\n                simultaneousHeatingCooling:if (simultaneousHeatingCooling.isNumber) simultaneousHeatingCooling.round else simultaneousHeatingCooling,\n                datResetting:              datResetting ,\n                dspResetting:              dspResetting,\n                datMeetingSp:              if (datMeetingSp.isNumber) datMeetingSp.round else datMeetingSp,\n                dspMeetingSp:              if (dspMeetingSp.isNumber) dspMeetingSp.round else dspMeetingSp,\n                supplyFanRuntime:          if (supplyFanRuntime.isNumber) supplyFanRuntime.round else supplyFanRuntime,\n                sparks:                    sparks ,\n                occHours:                  if (occHours.isNumber) occHours.round else occHours,\n                occupied247:               if (occupied247.isStr) occupied247 else \"Couldn't Calculate\",\n                economizingProperly:       if (economizingProperly.isNumber) economizingProperly.round else economizingProperly,\n                }\n\n  end\n\nif (debug) return table\n//return table\n\nout: table.toGrid\n          .stream\n          .addColMetaClean(\"id\", {dis:\"id\"})\n          .addColMetaClean(\"avgSupplyFan\"    ,{dis:\"Avg Supply Fan Spd\", color:fanColor, viz:\"barCell\"})\n          .addColMetaClean(\"avgReturnFan\"    ,{dis:\"Avg Rtn Fan Spd\",    color:fanColor, viz:\"barCell\"})\n          .addColMetaClean(\"avgReliefFan\"    ,{dis:\"Avg Rlf Fan Spd\",    color:fanColor, viz:\"barCell\"})\n          .addColMetaClean(\"avgExhaustFan\"   ,{dis:\"Avg Exh Fan Spd\",    color:fanColor, viz:\"barCell\"})\n          .addColMetaClean(\"avgHwValve\"      ,{dis:\"Avg Heating Demand\",   color:hwColor, viz:\"barCell\"})\n          .addColMetaClean(\"avgChwValve\"     ,{dis:\"Avg Cooling Demand\",  color:chwColor, viz:\"barCell\"})\n          .addColMetaClean(\"hwValveOpenHot\"  ,{dis:\"Heating While > \"+hotTemp+\"F\",  color:hwColor, viz:\"barCell\"})\n          .addColMetaClean(\"chwValveOpenCold\",{dis:\"Cooling While < \"+coldTemp+\"F\", color:chwColor, viz:\"barCell\"})\n          .addColMetaClean(\"simultaneousHeatingCooling\"    ,{dis:\"Simult Heat/Cool\",   color:sparkColor, viz:\"barCell\", chartMax:hours})\n          .addColMetaClean(\"datResetting\"    ,{dis:\"DAT Resetting\",      color:\"\", viz:\"barCell\", })\n          .addColMetaClean(\"dspResetting\"    ,{dis:\"DSP Resetting\",      color:\"\", viz:\"barCell\", })\n          .addColMetaClean(\"datMeetingSp\"    ,{dis:\"DAT Meeting Sp\",     color:meetingSpColor, viz:\"barCell\"})\n          .addColMetaClean(\"dspMeetingSp\"    ,{dis:\"DSP Meeting Sp\",     color:meetingSpColor, viz:\"barCell\"})\n          .addColMetaClean(\"supplyFanRuntime\",{dis:\"Supply Fan Runtime\", color:fanColor, viz:\"barCell\"})\n          .addColMetaClean(\"sparks\"          ,{dis:\"Total Sparks\",       color:sparkColor, viz:\"barCell\", })\n          .addColMetaClean(\"occHours\"        ,{dis:\"Occupied Hours\",     })\n          .addColMetaClean(\"occupied247\"     ,{dis:\"Zones Occupied 24/7\",     })\n          .addColMetaClean(\"economizingProperly\",{dis:\"Economizing Properly\", color:meetingSpColor, viz:\"barCell\"})\n          .collect(toGrid)\n          .reorderKeepCols(colOrder)\n          .sort(\"id\")\n\n  //apply sorting\n  ahuTableSortBy.split(\",\").each(c => if (c.startsWith(\"r_\")) out=out.sortr(c.replace(\"r_\",\"\")) else out=out.sort(c))\n\n\n  //add prez\n  prez: []\n  out.each() (row,idx) => do\n    datColor: if (not row.get(\"datMeetingSp\").isNumber) null\n              else if (row.get(\"datMeetingSp\")>95%) \"green\"\n              else if (row.get(\"datMeetingSp\")>80%) \"orange\"\n              else \"red\"\n    dspColor: if (not row.get(\"dspMeetingSp\").isNumber) null\n              else if (row.get(\"dspMeetingSp\")>95%) \"green\"\n              else if (row.get(\"dspMeetingSp\")>80%) \"orange\"\n              else \"red\"\n    prez = prez.add({row:idx, col:\"datMeetingSp\", color:datColor})\n               .add({row:idx, col:\"dspMeetingSp\", color:dspColor})\n  end\n  return out.addMeta({presentation:prez.toGrid, title:title, subtitle:subtitle})\nend",
      "local_src": "////////////////////////////////////////////////////////////////////////////\n/////////////////              DOCS                    /////////////////////\n////////////////////////////////////////////////////////////////////////////\n\n//  Author: James Gessel\n//  Created: Feb 2023\n//  Updated:\n//  By Who:       Date:     Description:\n//  Will          04/03/24  Update datResetting and dspResetting display values\n//  Will          05/29/24  Add % time economizing properly to ahu table\n//  Will          02/07/25  Add Count of Zones Occupied 24/7\n\n//  Docs: Widget that shows summary analytics for all ahus on a site\n//          -fan runtimes\n//          -hw chw valve open times\n//          -both valves open time\n//          -dat/dsp resets\n//          -dat/dsp meeting sp\n//\n//  Logic:\n//    Loop through each ahu, find relevant points, rollup the data\n\n//  Pod: kwlink pro\n\n//  INPUTS:\n//    site: single site\n//    options:\n\n//  OUTPUTS:\n//    out: table with a bunch of analytics for each AHU\n\n//  TODO:\n//\n\n(site, dates, opts:{}, d:false) => do\n\n  //validate and authorize current user\n  validateUser()\n\n  //normalize inputs\n  siteRec: site.getRecord\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  rollupOverride:   if (opts.has(\"rollupOverride\")) opts.get(\"rollupOverride\") else \"Auto\"\n                    if (rollupOverride==\"Auto\") rollupOverride=null\n  occOnly:      if (opts.has(\"occOnly\")) opts.get(\"occOnly\") else true\n  debug:         opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  ahuTableSortBy: if (opts.has(\"ahuTableSortBy\")) opts.get(\"ahuTableSortBy\") else \"id\"\n  showEmptyColumns: if (opts.has(\"showEmptyColumns\")) opts.get(\"showEmptyColumns\") else false\n  noDataValue: if (showEmptyColumns) \"\" else null\n  coldTemp: if (opts.has(\"coldTemp\")) opts.get(\"coldTemp\") else 45\n  hotTemp: if (opts.has(\"hotTemp\")) opts.get(\"hotTemp\") else 70\n\n  //formatting\n  fanColor: kwLinkColors(\"fan speed\")\n  chwColor: kwLinkColors(\"chilled water\")\n  hwColor:  kwLinkColors(\"hot water\")\n  sparkColor: kwLinkColors(\"general spark\")\n  meetingSpColor: \"green\"\n\n  title: \"AHU Summary - \"+siteRec.dis\n  subtitle: null\n  colOrder:      if (opts.has(\"colOrder\")) opts.get(\"colOrder\")\n                 else [\"id\",\n                       \"supplyFanRuntime\",\n                       \"datResetting\",\n                       \"dspResetting\",\n                       \"datMeetingSp\",\n                       \"dspMeetingSp\",\n                       \"avgSupplyFan\",\n                       \"avgReturnFan\",\n                       \"avgReliefFan\",\n                       \"avgExhaustFan\",\n                       \"avgHwValve\",\n                       \"hwValveOpenHot\",\n                       \"avgChwValve\",\n                       \"chwValveOpenCold\",\n                       \"avgDaFlow\",\n                       \"simultaneousHeatingCooling\",\n                       \"occupied247\",\n                       \"sparks\",\n                       \"economizingProperly\",\n                       \"occHours\"\n                       ]\n\n  //calculate total hours\n  hours: (dates.end - dates.start).to(1hr)\n\n  //AHU LOOP\n  units: readAll((mau or ahu or rtu) and equip and siteRef==site->id and not ahuSummaryNoShow)\n  table: units.map() (row,idx) => do\n\n    //normalize input\n    id: row->id\n\n    //get points\n    requiredPoints: []\n    optionalPoints: [\"occ_cmd\",\"daFan_run\",\"daFan_speed\",\"dat_sensor\",\"dat_sp\",\"dsp_sensor\", \"dsp_sp\", \"clgValve_cmd\",\"htgValve_cmd\",\"phtValve_cmd\",\"daFlow_sensor\",\"daFan_speed\",\"rtnFan_speed\",\"rlfFan_speed\",\"exhFan_speed\",\"siteOcc_cmd\"]\n    points: retrievePoints(requiredPoints, optionalPoints, {equipRef:id})\n    if (debug and opts.get(\"debug\")==\"Points\") return points\n    if (not points.get(\"htgValve_cmd\").isRef and points.get(\"phtValve_cmd\").isRef) points=points.set(\"htgValve_cmd\",points.get(\"phtValve_cmd\"))\n\n    if (debug and opts.get(\"debug\")==\"His\") return points.retrieveHis(dates, {rollupOverride:rollupOverride, hisSizeOnly})\n\n    //get occ periods\n    occPeriods: try logic_ahu_occPeriods(id, dates, opts, points) catch null\n    occHours: if (occPeriods.isGrid) occPeriods.hisClip.foldCol(\"v0\",sum)\n              else noDataValue\n    if (not occOnly) occPeriods=null\n    curOpts: opts.merge({occPeriods:occPeriods})\n\n    //calculate analytics\n    avgSupplyFan: try\n                    if (points.get(\"daFan_speed\").isRef)\n                      logic_ahu_pointRollup(points.get(\"daFan_speed\"), dates, avg, curOpts)\n                    else points.get(\"daFan_speed\")\n                  catch (err) err.get(\"dis\")\n                  if (not debug and avgSupplyFan.isStr) avgSupplyFan=noDataValue\n\n    avgReturnFan: try\n                    if (points.get(\"rtnFan_speed\").isRef)\n                      logic_ahu_pointRollup(points.get(\"rtnFan_speed\"), dates, avg, curOpts)\n                    else points.get(\"rtnFan_speed\")\n                  catch (err) err.get(\"dis\")\n                  if (not debug and avgReturnFan.isStr) avgReturnFan=noDataValue\n\n    avgReliefFan:  try\n                     if (points.get(\"rlfFan_speed\").isRef)\n                       logic_ahu_pointRollup(points.get(\"rlfFan_speed\"), dates, avg, curOpts)\n                     else points.get(\"rlfFan_speed\")\n                   catch (err) err.get(\"dis\")\n                   if (not debug and avgReliefFan.isStr) avgReliefFan=noDataValue\n\n    avgExhaustFan: try\n                    if (points.get(\"exhFan_speed\").isRef)\n                     logic_ahu_pointRollup(points.get(\"exhFan_speed\"), dates, avg, curOpts)\n                   else points.get(\"exhFan_speed\")\n                   catch (err) err.get(\"dis\")\n                   if (not debug and avgExhaustFan.isStr) avgExhaustFan=noDataValue\n\n    avgHwValve:    try\n                    if (points.get(\"htgValve_cmd\").isRef)\n                     logic_ahu_pointRollup(points.get(\"htgValve_cmd\"), dates, avg, curOpts)\n                   else points.get(\"htgValve_cmd\")\n                   catch (err) err.get(\"dis\")\n                   if (not debug and avgHwValve.isStr) avgHwValve=noDataValue\n\n    avgChwValve:   try\n                    if (points.get(\"clgValve_cmd\").isRef)\n                     logic_ahu_pointRollup(points.get(\"clgValve_cmd\"), dates, avg, curOpts)\n                   else points.get(\"clgValve_cmd\")\n                   catch (err) err.get(\"dis\")\n                   if (not debug and avgChwValve.isStr) avgChwValve=noDataValue\n                   \n    avgDaFlow:    try\n                    if (points.get(\"daFlow_sensor\").isRef)\n                     logic_ahu_pointRollup(points.get(\"daFlow_sensor\"), dates, avg, curOpts)\n                   else points.get(\"daFlow_sensor\")\n                   catch (err) err.get(\"dis\")\n                   if (not debug and avgChwValve.isStr) avgDaFlow=noDataValue               \n\n    hwValveOpenHot: try\n                    if (points.get(\"htgValve_cmd\").isRef)\n                     logic_ahu_hwValveOpenHot(id, dates, curOpts, points).round\n                    else \"missing required points\"\n                   catch (err) err.get(\"dis\")\n                    if (not debug and hwValveOpenHot.isStr) hwValveOpenHot=noDataValue\n\n    chwValveOpenCold: try\n                    if (points.get(\"clgValve_cmd\").isRef)\n                        logic_ahu_chwValveOpenCold(id, dates, curOpts, points).round\n                      else points.get(\"clgValve_cmd\")\n                   catch (err) err.get(\"dis\")\n                      if (not debug and chwValveOpenCold.isStr) chwValveOpenCold=noDataValue\n\n    simultaneousHeatingCooling:  try\n                                  if (points.get(\"clgValve_cmd\").isRef and points.get(\"htgValve_cmd\").isRef)\n                                    logic_ahu_simultHeatCool(id, dates, curOpts, points).meta.get(\"percTime\")\n                                  else \"missing required points\"\n                                 catch (err) err.get(\"dis\")\n                                 if (not debug and simultaneousHeatingCooling.isStr) simultaneousHeatingCooling=noDataValue\n\n    datResetting: if(row.has(\"noDatReset\")) \"notApplicable\"\n                  else do\n                    try\n                      if (points.get(\"dat_sensor\").isRef) do\n                        temp : logic_ahu_datResetting(id, dates, curOpts, points)\n                        if(temp.isStr) temp\n                        else temp.meta.get(\"datResetting\")\n                      end\n                      else \"missingPoint\"\n                    catch (err) err.dis//\"missingPoint\"\n                  end\n\n    dspResetting: if(row.has(\"noDspReset\")) \"notApplicable\"\n                  else do\n                    try\n                      if (points.get(\"dsp_sensor\").isRef) do\n                        temp1: logic_ahu_dspResetting(id, dates, curOpts, points)\n                        if(temp1.isStr) temp1\n                        else temp1.meta.get(\"dspResetting\")\n                      end\n                      else \"missingPoint\"\n                    catch (err) err.dis//\"missingPoint\"\n                  end\n\n    datMeetingSp: try\n                    if (points.get(\"dat_sensor\").isRef and points.get(\"dat_sp\").isRef)\n                    logic_ahu_datMeetingSp(id, dates, curOpts, points).meta.get(\"datMeetingSp\")\n                  else \"missing required points\"\n                   catch (err) err.get(\"dis\")\n                  if (not debug and datMeetingSp.isStr) datMeetingSp=noDataValue\n\n    dspMeetingSp: try\n                    if (points.get(\"dsp_sensor\").isRef and points.get(\"dsp_sp\").isRef)\n                    logic_ahu_dspMeetingSp(id, dates, curOpts, points).meta.get(\"dspMeetingSp\")\n                  else \"missing required points\"\n                   catch (err) err.get(\"dis\")\n                  if (not debug and dspMeetingSp.isStr) dspMeetingSp=noDataValue\nif (d) do\nb: points.get(\"daFan_run\")\nc: points.get(\"daFan_speed\")\na: try logic_ahu_pointRollup(points.get(\"daFan_run\"), dates, \"runtime\", {occPeriods:occPeriods, runtimePercent}) catch (err) err\nreturn {a:a, b:b, c:c}\nend\n    supplyFanRuntime: try\n                        if (points.get(\"daFan_run\").isRef)\n                          logic_ahu_pointRollup(points.get(\"daFan_run\"), dates, \"runtime\", {occPeriods:occPeriods, runtimePercent})\n                        else if (points.get(\"daFan_speed\").isRef)\n                          logic_ahu_pointRollup(points.get(\"daFan_speed\"), dates, \"runtime\", {occPeriods:occPeriods, runtimePercent})\n                        else \"missing required points\"\n                      catch (err) err.get(\"dis\")\n                      if (not debug and supplyFanRuntime.isStr) supplyFanRuntime=noDataValue\n\n    economizingProperly: try\n                           logic_ahu_economizingProperly(id, dates, curOpts)\n                         catch(err) err.get(\"dis\")\n                         if (not debug and economizingProperly.isStr) economizingProperly=noDataValue\n    economizingProperly = if(economizingProperly.isGrid) economizingProperly.meta.get(\"economizingProperly\").as(1%) else null\n\n    rules: readAll(rule and not disabled)\n    targets: id.toRec.equipToPoints.toGrid.addRow(id.toRec)\n    sparks: ruleSparksCount(targets, dates, rules)\n\n    occupied247: try\n                  logic_zones_occupancyCount(id, dates, curOpts)\n                 catch\n                   \"Couldn't Calculate\"\n\n    //put together dict\n    analytics: {id: id,\n                order: row.get(\"order\"),\n                avgSupplyFan:              if (avgSupplyFan.isNumber) avgSupplyFan.round else avgSupplyFan,\n                avgReturnFan:              if (avgReturnFan.isNumber) avgReturnFan.round else avgReturnFan,\n                avgReliefFan:              if (avgReliefFan.isNumber) avgReliefFan.round else avgReliefFan,\n                avgExhaustFan:             if (avgExhaustFan.isNumber) avgExhaustFan.round else avgExhaustFan,\n                avgHwValve:                if (avgHwValve.isNumber) avgHwValve.round else avgHwValve,\n                avgChwValve:               if (avgChwValve.isNumber) avgChwValve.round else avgChwValve,\n                \n                avgDaFlow:                 if (avgDaFlow.isNumber) avgDaFlow.round else avgDaFlow,\n                \n                hwValveOpenHot:            if (hwValveOpenHot.isNumber) hwValveOpenHot.round else hwValveOpenHot,\n                chwValveOpenCold:          if (chwValveOpenCold.isNumber) chwValveOpenCold.round else chwValveOpenCold,\n                simultaneousHeatingCooling:if (simultaneousHeatingCooling.isNumber) simultaneousHeatingCooling.round else simultaneousHeatingCooling,\n                datResetting:              datResetting ,\n                dspResetting:              dspResetting,\n                datMeetingSp:              if (datMeetingSp.isNumber) datMeetingSp.round else datMeetingSp,\n                dspMeetingSp:              if (dspMeetingSp.isNumber) dspMeetingSp.round else dspMeetingSp,\n                supplyFanRuntime:          if (supplyFanRuntime.isNumber) supplyFanRuntime.round else supplyFanRuntime,\n                sparks:                    sparks ,\n                occHours:                  if (occHours.isNumber) occHours.round else occHours,\n                occupied247:               if (occupied247.isStr) occupied247 else \"Couldn't Calculate\",\n                economizingProperly:       if (economizingProperly.isNumber) economizingProperly.round else economizingProperly,\n                mod:now()\n                }\n\n  end\n\nif (debug) return table\n//return table\n\nout: table.toGrid\n          .stream\n          .addColMetaClean(\"id\", {dis:\"AHU\"})\n          .addColMetaClean(\"avgSupplyFan\"    ,{dis:\"Avg Supply Fan Spd\", color:fanColor, viz:\"barCell\", chartMax:100%,})\n          .addColMetaClean(\"avgReturnFan\"    ,{dis:\"Avg Rtn Fan Spd\",    color:fanColor, viz:\"barCell\", chartMax:100%,})\n          .addColMetaClean(\"avgReliefFan\"    ,{dis:\"Avg Rlf Fan Spd\",    color:fanColor, viz:\"barCell\", chartMax:100%,})\n          .addColMetaClean(\"avgExhaustFan\"   ,{dis:\"Avg Exh Fan Spd\",    color:fanColor, viz:\"barCell\", chartMax:100%,})\n          .addColMetaClean(\"avgHwValve\"      ,{dis:\"Avg Heating Demand\",   color:hwColor, viz:\"barCell\", chartMax:100%,})\n          .addColMetaClean(\"avgChwValve\"     ,{dis:\"Avg Cooling Demand\",  color:chwColor, viz:\"barCell\", chartMax:100%,})\n          \n          .addColMetaClean(\"avgDaFlow\"       ,{dis:\"Avg Discharge Flow\"})//,  color:chwColor, viz:\"barCell\", chartMax:100%,})\n          \n          .addColMetaClean(\"hwValveOpenHot\"  ,{dis:\"Heating While > \"+hotTemp+\"F\",  color:hwColor, viz:\"barCell\", chartMax:100%,})\n          .addColMetaClean(\"chwValveOpenCold\",{dis:\"Cooling While < \"+coldTemp+\"F\", color:chwColor, viz:\"barCell\", chartMax:100%,})\n          .addColMetaClean(\"simultaneousHeatingCooling\"    ,{dis:\"Simult Heat/Cool\",   color:sparkColor, viz:\"barCell\", chartMax:hours})\n          .addColMetaClean(\"datResetting\"    ,{dis:\"DAT Resetting\",      color:\"\", viz:\"barCell\", })\n          .addColMetaClean(\"dspResetting\"    ,{dis:\"DSP Resetting\",      color:\"\", viz:\"barCell\", })\n          .addColMetaClean(\"datMeetingSp\"    ,{dis:\"DAT Meeting Sp\",     color:meetingSpColor, viz:\"barCell\", chartMax:100%})\n          .addColMetaClean(\"dspMeetingSp\"    ,{dis:\"DSP Meeting Sp\",     color:meetingSpColor, viz:\"barCell\", chartMax:100%})\n          .addColMetaClean(\"supplyFanRuntime\",{dis:\"Supply Fan Runtime\", color:fanColor, viz:\"barCell\", chartMax:100%})\n          .addColMetaClean(\"sparks\"          ,{dis:\"Total Sparks\",       color:sparkColor, viz:\"barCell\", })\n          .addColMetaClean(\"occHours\"        ,{dis:\"Occupied Hours\",     })\n          .addColMetaClean(\"occupied247\"     ,{dis:\"Zones Occupied 24/7\",     })\n          .addColMetaClean(\"economizingProperly\",{dis:\"Economizing Properly\", color:meetingSpColor, viz:\"barCell\", chartMax:100%})\n          .addColMetaClean(\"mod\",{hidden})\n          .collect(toGrid)\n          .reorderKeepCols(colOrder)\n          .sort(\"id\")\n          .sort(\"order\")\n\n  //apply sorting\n  //ahuTableSortBy.split(\",\").each(c => if (c.startsWith(\"r_\")) out=out.sortr(c.replace(\"r_\",\"\")) else out=out.sort(c))\n\n\n  //add prez\n  prez: []\n  out.each() (row,idx) => do\n    datColor: if (not row.get(\"datMeetingSp\").isNumber) null\n              else if (row.get(\"datMeetingSp\")>95%) \"green\"\n              else if (row.get(\"datMeetingSp\")>80%) \"orange\"\n              else \"red\"\n    dspColor: if (not row.get(\"dspMeetingSp\").isNumber) null\n              else if (row.get(\"dspMeetingSp\")>95%) \"green\"\n              else if (row.get(\"dspMeetingSp\")>80%) \"orange\"\n              else \"red\"\n    prez = prez.add({row:idx, col:\"datMeetingSp\", color:datColor})\n               .add({row:idx, col:\"dspMeetingSp\", color:dspColor})\n  end\n  return out.addMeta({presentation:prez.toGrid, title:title, subtitle:subtitle})\nend",
      "diff": "@@ -76,10 +76,12 @@                        \"hwValveOpenHot\",\n                        \"avgChwValve\",\n                        \"chwValveOpenCold\",\n+                       \"avgDaFlow\",\n                        \"simultaneousHeatingCooling\",\n                        \"occupied247\",\n                        \"sparks\",\n-                       \"economizingProperly\"\n+                       \"economizingProperly\",\n+                       \"occHours\"\n                        ]\n \n   //calculate total hours\n@@ -87,14 +89,14 @@ \n   //AHU LOOP\n   units: readAll((mau or ahu or rtu) and equip and siteRef==site->id and not ahuSummaryNoShow)\n-  table: units.map row => do\n+  table: units.map() (row,idx) => do\n \n     //normalize input\n     id: row->id\n \n     //get points\n     requiredPoints: []\n-    optionalPoints: [\"occ_cmd\",\"daFan_run\",\"daFan_speed\",\"dat_sensor\",\"dat_sp\",\"dsp_sensor\", \"dsp_sp\", \"clgValve_cmd\",\"htgValve_cmd\",\"phtValve_cmd\",\"daFan_speed\",\"rtnFan_speed\",\"rlfFan_speed\",\"exhFan_speed\",\"siteOcc_cmd\"]\n+    optionalPoints: [\"occ_cmd\",\"daFan_run\",\"daFan_speed\",\"dat_sensor\",\"dat_sp\",\"dsp_sensor\", \"dsp_sp\", \"clgValve_cmd\",\"htgValve_cmd\",\"phtValve_cmd\",\"daFlow_sensor\",\"daFan_speed\",\"rtnFan_speed\",\"rlfFan_speed\",\"exhFan_speed\",\"siteOcc_cmd\"]\n     points: retrievePoints(requiredPoints, optionalPoints, {equipRef:id})\n     if (debug and opts.get(\"debug\")==\"Points\") return points\n     if (not points.get(\"htgValve_cmd\").isRef and points.get(\"phtValve_cmd\").isRef) points=points.set(\"htgValve_cmd\",points.get(\"phtValve_cmd\"))\n@@ -150,6 +152,13 @@                    else points.get(\"clgValve_cmd\")\n                    catch (err) err.get(\"dis\")\n                    if (not debug and avgChwValve.isStr) avgChwValve=noDataValue\n+\n+    avgDaFlow:    try\n+                    if (points.get(\"daFlow_sensor\").isRef)\n+                     logic_ahu_pointRollup(points.get(\"daFlow_sensor\"), dates, avg, curOpts)\n+                   else points.get(\"daFlow_sensor\")\n+                   catch (err) err.get(\"dis\")\n+                   if (not debug and avgChwValve.isStr) avgDaFlow=noDataValue\n \n     hwValveOpenHot: try\n                     if (points.get(\"htgValve_cmd\").isRef)\n@@ -241,12 +250,16 @@ \n     //put together dict\n     analytics: {id: id,\n+                order: row.get(\"order\"),\n                 avgSupplyFan:              if (avgSupplyFan.isNumber) avgSupplyFan.round else avgSupplyFan,\n                 avgReturnFan:              if (avgReturnFan.isNumber) avgReturnFan.round else avgReturnFan,\n                 avgReliefFan:              if (avgReliefFan.isNumber) avgReliefFan.round else avgReliefFan,\n                 avgExhaustFan:             if (avgExhaustFan.isNumber) avgExhaustFan.round else avgExhaustFan,\n                 avgHwValve:                if (avgHwValve.isNumber) avgHwValve.round else avgHwValve,\n                 avgChwValve:               if (avgChwValve.isNumber) avgChwValve.round else avgChwValve,\n+\n+                avgDaFlow:                 if (avgDaFlow.isNumber) avgDaFlow.round else avgDaFlow,\n+\n                 hwValveOpenHot:            if (hwValveOpenHot.isNumber) hwValveOpenHot.round else hwValveOpenHot,\n                 chwValveOpenCold:          if (chwValveOpenCold.isNumber) chwValveOpenCold.round else chwValveOpenCold,\n                 simultaneousHeatingCooling:if (simultaneousHeatingCooling.isNumber) simultaneousHeatingCooling.round else simultaneousHeatingCooling,\n@@ -259,6 +272,7 @@                 occHours:                  if (occHours.isNumber) occHours.round else occHours,\n                 occupied247:               if (occupied247.isStr) occupied247 else \"Couldn't Calculate\",\n                 economizingProperly:       if (economizingProperly.isNumber) economizingProperly.round else economizingProperly,\n+                mod:now()\n                 }\n \n   end\n@@ -268,31 +282,36 @@ \n out: table.toGrid\n           .stream\n-          .addColMetaClean(\"id\", {dis:\"id\"})\n-          .addColMetaClean(\"avgSupplyFan\"    ,{dis:\"Avg Supply Fan Spd\", color:fanColor, viz:\"barCell\"})\n-          .addColMetaClean(\"avgReturnFan\"    ,{dis:\"Avg Rtn Fan Spd\",    color:fanColor, viz:\"barCell\"})\n-          .addColMetaClean(\"avgReliefFan\"    ,{dis:\"Avg Rlf Fan Spd\",    color:fanColor, viz:\"barCell\"})\n-          .addColMetaClean(\"avgExhaustFan\"   ,{dis:\"Avg Exh Fan Spd\",    color:fanColor, viz:\"barCell\"})\n-          .addColMetaClean(\"avgHwValve\"      ,{dis:\"Avg Heating Demand\",   color:hwColor, viz:\"barCell\"})\n-          .addColMetaClean(\"avgChwValve\"     ,{dis:\"Avg Cooling Demand\",  color:chwColor, viz:\"barCell\"})\n-          .addColMetaClean(\"hwValveOpenHot\"  ,{dis:\"Heating While > \"+hotTemp+\"F\",  color:hwColor, viz:\"barCell\"})\n-          .addColMetaClean(\"chwValveOpenCold\",{dis:\"Cooling While < \"+coldTemp+\"F\", color:chwColor, viz:\"barCell\"})\n+          .addColMetaClean(\"id\", {dis:\"AHU\"})\n+          .addColMetaClean(\"avgSupplyFan\"    ,{dis:\"Avg Supply Fan Spd\", color:fanColor, viz:\"barCell\", chartMax:100%,})\n+          .addColMetaClean(\"avgReturnFan\"    ,{dis:\"Avg Rtn Fan Spd\",    color:fanColor, viz:\"barCell\", chartMax:100%,})\n+          .addColMetaClean(\"avgReliefFan\"    ,{dis:\"Avg Rlf Fan Spd\",    color:fanColor, viz:\"barCell\", chartMax:100%,})\n+          .addColMetaClean(\"avgExhaustFan\"   ,{dis:\"Avg Exh Fan Spd\",    color:fanColor, viz:\"barCell\", chartMax:100%,})\n+          .addColMetaClean(\"avgHwValve\"      ,{dis:\"Avg Heating Demand\",   color:hwColor, viz:\"barCell\", chartMax:100%,})\n+          .addColMetaClean(\"avgChwValve\"     ,{dis:\"Avg Cooling Demand\",  color:chwColor, viz:\"barCell\", chartMax:100%,})\n+\n+          .addColMetaClean(\"avgDaFlow\"       ,{dis:\"Avg Discharge Flow\"})//,  color:chwColor, viz:\"barCell\", chartMax:100%,})\n+\n+          .addColMetaClean(\"hwValveOpenHot\"  ,{dis:\"Heating While > \"+hotTemp+\"F\",  color:hwColor, viz:\"barCell\", chartMax:100%,})\n+          .addColMetaClean(\"chwValveOpenCold\",{dis:\"Cooling While < \"+coldTemp+\"F\", color:chwColor, viz:\"barCell\", chartMax:100%,})\n           .addColMetaClean(\"simultaneousHeatingCooling\"    ,{dis:\"Simult Heat/Cool\",   color:sparkColor, viz:\"barCell\", chartMax:hours})\n           .addColMetaClean(\"datResetting\"    ,{dis:\"DAT Resetting\",      color:\"\", viz:\"barCell\", })\n           .addColMetaClean(\"dspResetting\"    ,{dis:\"DSP Resetting\",      color:\"\", viz:\"barCell\", })\n-          .addColMetaClean(\"datMeetingSp\"    ,{dis:\"DAT Meeting Sp\",     color:meetingSpColor, viz:\"barCell\"})\n-          .addColMetaClean(\"dspMeetingSp\"    ,{dis:\"DSP Meeting Sp\",     color:meetingSpColor, viz:\"barCell\"})\n-          .addColMetaClean(\"supplyFanRuntime\",{dis:\"Supply Fan Runtime\", color:fanColor, viz:\"barCell\"})\n+          .addColMetaClean(\"datMeetingSp\"    ,{dis:\"DAT Meeting Sp\",     color:meetingSpColor, viz:\"barCell\", chartMax:100%})\n+          .addColMetaClean(\"dspMeetingSp\"    ,{dis:\"DSP Meeting Sp\",     color:meetingSpColor, viz:\"barCell\", chartMax:100%})\n+          .addColMetaClean(\"supplyFanRuntime\",{dis:\"Supply Fan Runtime\", color:fanColor, viz:\"barCell\", chartMax:100%})\n           .addColMetaClean(\"sparks\"          ,{dis:\"Total Sparks\",       color:sparkColor, viz:\"barCell\", })\n           .addColMetaClean(\"occHours\"        ,{dis:\"Occupied Hours\",     })\n           .addColMetaClean(\"occupied247\"     ,{dis:\"Zones Occupied 24/7\",     })\n-          .addColMetaClean(\"economizingProperly\",{dis:\"Economizing Properly\", color:meetingSpColor, viz:\"barCell\"})\n+          .addColMetaClean(\"economizingProperly\",{dis:\"Economizing Properly\", color:meetingSpColor, viz:\"barCell\", chartMax:100%})\n+          .addColMetaClean(\"mod\",{hidden})\n           .collect(toGrid)\n           .reorderKeepCols(colOrder)\n           .sort(\"id\")\n+          .sort(\"order\")\n \n   //apply sorting\n-  ahuTableSortBy.split(\",\").each(c => if (c.startsWith(\"r_\")) out=out.sortr(c.replace(\"r_\",\"\")) else out=out.sort(c))\n+  //ahuTableSortBy.split(\",\").each(c => if (c.startsWith(\"r_\")) out=out.sortr(c.replace(\"r_\",\"\")) else out=out.sort(c))\n \n \n   //add prez\n"
    },
    {
      "name": "wdg_ahu_widgetSelector",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(widget, ahu, dates, opts:{}) => do\n  dates = dates.toSpan\n//charts\n  if (widget==\"Temp Trends\")                  return wdg_ahu_chart_tempTrends(ahu, dates, opts)\n  if (widget==\"Valve Trends\")                 return wdg_ahu_chart_valveTrends(ahu, dates, opts)\n  if (widget==\"Fan Trends\")                   return wdg_ahu_chart_fanTrends(ahu, dates, opts)\n  if (widget==\"Fan Runtimes\")                 return wdg_ahu_chart_fanRuntimes(ahu, dates, opts)\n  if (widget==\"CHW/HW vs OAT\")                return wdg_ahu_chart_chwVsHw(ahu, dates, opts)\n  if (widget==\"AHU Cool vs VAV Reheat\")       return wdg_ahu_chart_ahuCoolVavReheat(ahu, dates, opts)\n  if (widget==\"Simultaneous Heating/Cooling\") return wdg_ahu_chart_simultaneousHeatingCooling(ahu, dates, opts)\n  if (widget==\"AHU Spark Runtime\")            return wdg_ahu_chart_sparkRuntime(ahu, dates, opts)\n  if (widget==\"AHU Cooling Load\")             return wdg_ahu_chart_ahuCoolLoad(ahu, dates, opts)\n  if (widget==\"Economizing\")                  return wdg_ahu_chart_economizing(ahu, dates, opts)\n  if (widget==\"DAT/DSP Meeting Sp\")           return wdg_ahu_chart_datDspMeetingSp(ahu, dates, opts)\n  if (widget==\"DAT/DSP Resets\")               return wdg_ahu_chart_datDspResets(ahu, dates, opts)\n\n//cards\n\n//tables\n  if (widget==\"Spark Runtime Rollup\")         return wdg_ahu_table_sparkRuntimeRollup(ahu, dates, opts)\n\n\n  return [\"Select a different widget - \"+widget+\" unavailable\"].toGrid.addMeta({noData:\"Select a different widget - \"+widget+\" unavailable\"})\nend",
      "local_src": "(widget, ahu, dates, opts:{}) => do\n  dates = dates.toSpan\n//charts\n  if (widget==\"Temp Trends\")                  return wdg_ahu_chart_tempTrends(ahu, dates, opts)\n  if (widget==\"Valve Trends\")                 return wdg_ahu_chart_valveTrends(ahu, dates, opts)\n  if (widget==\"Fan Trends\")                   return wdg_ahu_chart_fanTrends(ahu, dates, opts)\n  if (widget==\"Fan Runtimes\")                 return wdg_ahu_chart_fanRuntimes(ahu, dates, opts)\n  if (widget==\"CHW/HW vs OAT\")                return wdg_ahu_chart_chwVsHw(ahu, dates, opts)\n  if (widget==\"AHU Cool vs VAV Reheat\")       return wdg_ahu_chart_ahuCoolVavReheat(ahu, dates, opts)\n  if (widget==\"Simultaneous Heating/Cooling\") return wdg_ahu_chart_simultaneousHeatingCooling(ahu, dates, opts)\n  if (widget==\"AHU Spark Runtime\")            return wdg_ahu_chart_sparkRuntime(ahu, dates, opts)\n  if (widget==\"AHU Cooling Load\")             return wdg_ahu_chart_ahuCoolLoad(ahu, dates, opts)\n  if (widget==\"Economizing\")                  return wdg_ahu_chart_economizing(ahu, dates, opts)\n  if (widget==\"DAT/DSP Meeting Sp\")           return wdg_ahu_chart_datDspMeetingSp(ahu, dates, opts)\n  if (widget==\"DAT/DSP Resets\")               return wdg_ahu_chart_datDspResets(ahu, dates, opts)\n  if (widget==\"DAF Sp Delta\")                 return logic_ahu_flowSpDelta(ahu, dates, opts)\n\n//cards\n\n//tables\n  if (widget==\"Spark Runtime Rollup\")         return wdg_ahu_table_sparkRuntimeRollup(ahu, dates, opts)\n\n\n  return [\"Select a different widget - \"+widget+\" unavailable\"].toGrid.addMeta({noData:\"Select a different widget - \"+widget+\" unavailable\"})\nend",
      "diff": "@@ -13,6 +13,7 @@   if (widget==\"Economizing\")                  return wdg_ahu_chart_economizing(ahu, dates, opts)\n   if (widget==\"DAT/DSP Meeting Sp\")           return wdg_ahu_chart_datDspMeetingSp(ahu, dates, opts)\n   if (widget==\"DAT/DSP Resets\")               return wdg_ahu_chart_datDspResets(ahu, dates, opts)\n+  if (widget==\"DAF Sp Delta\")                 return logic_ahu_flowSpDelta(ahu, dates, opts)\n \n //cards\n \n"
    },
    {
      "name": "wdg_anomalyDetection_v2_chart_modelPredictionsPreview_v2",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/*\n*   Description :\n*    Display all connected sites along with relevant information and KPIs\n*\n*   Parameters :\n*     Type                   Name            Description\n*\n*\n*   Output :\n*     Type                   Name            Description\n*     Grid                   out            - A gbGrid containing all connected sites and relevant info/KPIs\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         12/11/2024      - Initial Creation\n*     Apoorv Khanuja         05/28/2025      - Added a dateSpan argument and moved the widget to the top subview.\n*                                              dateSpan allows the subview to line up with the concurrent sparks widget.\n*                                              Updated to work with adaptive model instead of fixed baseline.\n*/\n\n// Example of how to use/test\n//\n//\n\n(siteRef, dateSpan, utilityType, opts:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // USER VALIDATION\n  validateUser()\n\n  dateSpan = dateSpan.toSpan\n  // Do we need to navResolve siteSelection?\n\n  meterRec: if      (utilityType==\"elec\" or utilityType==\"all\") try pointQuery(\"elec_intervalPwr\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"steam\")                      try pointQuery(\"steam_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"chw\")                        try pointQuery(\"chilledWater_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"hhw\")                        try pointQuery(\"hotWater_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"naturalGas\")                 try pointQuery(\"natGas_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"water\")                      try pointQuery(\"water_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n      // return     chart = blankChart(meterRec.toStr)\n  if (meterRec.isNull) return {info: meterRec}.toGrid.addMeta({noData: \"Utility meter point missing or tagged incorrectly.\"}).addColMeta(\"info\", {dis: \"Information\"})\n  else if (meterRec.isNonNull) return chart: logic_energyAgent_meterPointToPredictionsChart(meterRec, dateSpan)\n\n\n  if (chart.isNonNull) return chart\n    //return blankChart(\"Please select an anomaly to populate charts\")\n\n\n\n  try do\n    anomalyRec : anomalyRef.toRec\n    anomalyRef = anomalyRec.get(\"id\")\n  end catch return blankChart(\"Please refresh page and select an anomaly to populate charts\")\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  //// Move everything below to logic_anomalyDetectionV2_anomalyPreview(anomalyRef, opts:{})\n\n  // Points\n\n  modelPointRef : try anomalyRec.get(\"anomalyTriggerRef\").toRec.get(\"dynamicModelPointRef\") catch return blankChart(\"Anomaly Trigger not found.\")\n  targetVarRef  : anomalyRec.get(\"targetVarRef\")\n  //staticModelConfig:  try anomalyRec.get(\"staticModelConfigRef\").toRec catch return blankChart(\"Old anomaly record. Missing staticModelConfigRef. Reobserve anomalies and try again.\")\n\n  // Dates ",
      "local_src": "/* \n*   Description :\n*    Display all connected sites along with relevant information and KPIs\n*\n*   Parameters :\n*     Type                   Name            Description\n*\n*\n*   Output :\n*     Type                   Name            Description\n*     Grid                   out            - A gbGrid containing all connected sites and relevant info/KPIs\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         12/11/2024      - Initial Creation\n*     Apoorv Khanuja         05/28/2025      - Added a dateSpan argument and moved the widget to the top subview. \n*                                              dateSpan allows the subview to line up with the concurrent sparks widget.\n*                                              Updated to work with adaptive model instead of fixed baseline.\n*/\n\n// Example of how to use/test\n//\n//\n\n(siteRef, dateSpan, utilityType, opts:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // USER VALIDATION\n  validateUser()\n  \n  dateSpan = dateSpan.toSpan\n  // Do we need to navResolve siteSelection?\n\n  meterRec: if      (utilityType==\"elec\" or utilityType==\"all\") try pointQuery(\"elec_intervalPwr\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"steam\")                      try pointQuery(\"steam_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"chw\")                        try pointQuery(\"chilledWater_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"hhw\")                        try pointQuery(\"hotWater_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"naturalGas\")                 try pointQuery(\"natGas_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"water\")                      try pointQuery(\"water_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n      // return     chart = blankChart(meterRec.toStr)\n  if (meterRec.isNull) return {info: meterRec}.toGrid.addMeta({noData: \"Utility meter point missing or tagged incorrectly.\"}).addColMeta(\"info\", {dis: \"Information\"})\n  else if (meterRec.isNonNull) return chart: logic_energyAgent_meterPointToPredictionsChart_v2(meterRec, dateSpan)\n\n  \n  if (chart.isNonNull) return chart\n    //return blankChart(\"Please select an anomaly to populate charts\")\n  \n  \n  \n  try do\n    anomalyRec : anomalyRef.toRec\n    anomalyRef = anomalyRec.get(\"id\")\n  end catch return blankChart(\"Please refresh page and select an anomaly to populate charts\")\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  //// Move everything below to logic_anomalyDetectionV2_anomalyPreview(anomalyRef, opts:{})\n\n  // Points\n\n  modelPointRef : try anomalyRec.get(\"anomalyTriggerRef\").toRec.get(\"dynamicModelPointRef\") catch return blankChart(\"Anomaly Trigger not found.\")\n  targetVarRef  : anomalyRec.get(\"targetVarRef\")\n  //staticModelConfig:  try anomalyRec.get(\"staticModelConfigRef\").toRec catch return blankChart(\"Old anomaly record. Missing staticModelConfigRef. Reobserve anomalies and try again.\")\n\n  // Dates -----> -3mo to +3mo?\n  anomalyStartTS  : anomalyRec.get(\"ts\")\n  extendedStartTS : anomalyStartTS - 3mo\n  anomalyEndTS    : anomalyStartTS + anomalyRec.get(\"duration\")\n  extendedEndTS   : anomalyEndTS + 3mo\n\n  // Check hisStart and hisEnd for both points\n  modelPointStartDate: modelPointRef.toRec.get(\"hisStart\")\n  modelPointEndDate  : modelPointRef.toRec.get(\"hisEnd\")\n    // Usually model point would be the bottleneck ===================> TODO: How can we show the meter data and perhaps the adaptiveBaseline modelPoint along with confInts for the previous 3 months?\n    //if (modelPointStartDate > extendedStartTS) extendedStartTS = modelPointStartDate\n    //if (modelPointEndDate < extendedEndTS) extendedEndTS = modelPointEndDate\n\n  extendedSpan    : (extendedStartTS..extendedEndTS).toSpan\n\n  // His\n  hisGrid: [targetVarRef, modelPointRef].hisRead(dateSpan).hisClip\n  //hisGrid: try logic_kwModeling_getBaselineModelResults(staticModelConfig) catch (err) return blankChart(err.toStr)\n  //  if (hisGrid.isStr) return blankChart(hisGrid)\n  \n  confIntGrid         : try logic_kwModeling_getModelPointResults(modelPointRef).reorderKeepCols([\"ts\"]) catch (err) return blankChart(err.dis)\n  filteredConfIntGrid : confIntGrid.findAll(v=> v.get(\"ts\") >= dateSpan.start and v.get(\"ts\") < dateSpan.end)  // filteredHisGrid.first.get(\"ts\") -1hr?\n  loCol               : confIntGrid.colNames.find() r=>r.contains(\"lo\")\n  hiCol               : confIntGrid.colNames.find() r=>r.contains(\"hi\")\n  \n  //filteredHisGrid: hisGrid.findAll(v=> v.get(\"ts\") >= extendedStartTS and v.get(\"ts\") < extendedEndTS )\n  //  if (filteredHisGrid.last.get(\"ts\") < extendedEndTS) extendedEndTS = filteredHisGrid.last.get(\"ts\")\n\n  //return blankChart(hisGrid.colNames.toStr)//.map(r=> {\"colName\":r}).toGrid\n  //// Fill in the inital 3 months with Adaptive baseline modelPoint his data\n  //if (filteredHisGrid.first.get(\"ts\") > extendedStartTS) do \n  //  \n  //  // Get points:\n  //  meterPoint:                  anomalyRec.get(\"targetVarRef\")\n  //  adaptiveBaselineModelPoint:  try anomalyRec.get(\"anomalyTriggerRef\").toRec.get(\"dynamicModelPointRef\") catch (err) return blankChart(\"Widget trying to read a deleted Anomaly Trigger - Rerun Energy Agent Task\", err.dis)\n  //  //return blankChart(adaptiveBaselineModelPoint.toStr)\n  //  // Get His\n  //  preAnomalySpan: (extendedStartTS..filteredHisGrid.first.get(\"ts\")).toSpan                                                       // filteredHisGrid.first.get(\"ts\") -1hr?\n  //  \n  //  adaptiveModelHisGrid: [meterPoint, adaptiveBaselineModelPoint].hisRead(preAnomalySpan).hisClip\n  //  \n  //  confIntGrid         : try logic_kwModeling_getModelPointResults(adaptiveBaselineModelPoint) catch (err) return blankChart(err.dis)\n  //  filteredConfIntGrid : confIntGrid.findAll(v=> v.get(\"ts\") >= extendedStartTS and v.get(\"ts\") < filteredHisGrid.first.get(\"ts\"))  // filteredHisGrid.first.get(\"ts\") -1hr?\n  //  loCol               : confIntGrid.colNames.find() r=>r.contains(\"lo\")\n  //  hiCol               : confIntGrid.colNames.find() r=>r.contains(\"hi\")\n  //  \n  //  preAnomalyHisGrid: hisJoin([adaptiveModelHisGrid, filteredConfIntGrid]).reorderKeepCols([\"ts\", \"v0\", hiCol, \"v1\", loCol]).renameCol(\"v1\", \"timegpt\")\n  //  preAnomalyHisGrid = transferMeta(hisGrid, preAnomalyHisGrid)\n//\n  //  hisGrid = addRows(filteredHisGrid, preAnomalyHisGrid)\n  //  \n  //end\n\n  \n  //holidayCalendar: read(calendar and dis==\"Holiday Calendar\", false).scheduleHis(extendedSpan)\n  //                                                                  .map(r=> r = if (r.get(\"val\")!= true) r.set(\"val\", false) else r.set(\"val\", true))\n  //                                                                  .addColMeta(\"holidayCalendar\", {chartGroup: \"duration\"})\n\n\n  //Create Anomaly Duration Grid\n  anomalyDurationGrid: {ts: anomalyStartTS, anomalyDuration: anomalyRec.get(\"duration\")}.toGrid.addMeta({hisStart: dateSpan.start, hisEnd: dateSpan.end, hisSize:1})\n                                                                                        \n\n  allAnomalies: readAll(anomaly and targetVarRef==anomalyRec.get(\"targetVarRef\") and ts >= dateSpan.start and ts < dateSpan.end)\n  try allAnomalies = allAnomalies.reorderCols([\"ts\", \"duration\"]).toGrid.addMeta({hisStart: dateSpan.start, hisEnd: dateSpan.end, hisSize:allAnomalies.size}) catch null\n  \n  //out: hisJoin([hisGrid, anomalyDurationGrid, allAnomalies, holidayCalendar])\n  lineCharts: hisJoin([hisGrid, filteredConfIntGrid]).addMeta({hisStart: dateSpan.start, hisEnd: dateSpan.end})\n  runtimeCharts: hisJoin([ anomalyDurationGrid, allAnomalies])\n  out: hisJoin([lineCharts, runtimeCharts])\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // FORMAT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  gridMeta: {title: \"Extended Model Prediction Preview - \" + anomalyRec.get(\"siteRef\").toRec.get(\"dis\"), subtitle: anomalyRec.get(\"targetVarRef\").toRec.get(\"navName\").toStr + \" (\"+dateSpan.start.date+\" to \"+dateSpan.end.date+\")\", hisStart:dateSpan.start, hisEnd:dateSpan.end}\n\n  // prezGrid for anomalyImpact color?\n\n  out = out.stream\n           .addColMeta(\"ts\",            {dis:\"Timestamp\"})\n           .addColMeta(\"v0\", {dis:\"Actual\", strokeWidth:1, chartGroup: \"predictions\", chartType:\"line\"})//\n           .addColMeta(hiCol, {dis:\"Upper Bound\", strokeWidth:0.5, chartGroup: \"predictions\", chartAreaMode: \"nextSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n           .addColMeta(\"v1\", {dis:\"Model\", strokeWidth:1.5, chartGroup: \"predictions\", chartType:\"line\", strokeDasharray:\"6,3\"})//\n           .addColMeta(loCol, {dis:\"Lower Bound\", strokeWidth:0.5, chartGroup: \"predictions\", chartAreaMode: \"prevSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n           .addColMeta(\"anomalyDuration\", {chartGroup: \"xduration\", chartType:\"runtime\", dis:\"Active Anomaly\"})\n           .addColMeta(\"duration\", {chartGroup: \"xduration\", chartType:\"runtime\", dis: \"All Anomalies\"})\n           .addMeta(gridMeta)\n           .addPrez_chartOrder(\"\", [ \"v0\", \"v1\", loCol, hiCol])//\n           .collect\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  //RETURN RESULT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  return out.reorderKeepCols([\"ts\", \"v0\", hiCol, \"v1\", loCol, \"anomalyDuration\", \"duration\"])\n\nend",
      "diff": "@@ -42,7 +42,7 @@             else if (utilityType==\"water\")                      try pointQuery(\"water_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n       // return     chart = blankChart(meterRec.toStr)\n   if (meterRec.isNull) return {info: meterRec}.toGrid.addMeta({noData: \"Utility meter point missing or tagged incorrectly.\"}).addColMeta(\"info\", {dis: \"Information\"})\n-  else if (meterRec.isNonNull) return chart: logic_energyAgent_meterPointToPredictionsChart(meterRec, dateSpan)\n+  else if (meterRec.isNonNull) return chart: logic_energyAgent_meterPointToPredictionsChart_v2(meterRec, dateSpan)\n \n \n   if (chart.isNonNull) return chart\n@@ -72,4 +72,101 @@   targetVarRef  : anomalyRec.get(\"targetVarRef\")\n   //staticModelConfig:  try anomalyRec.get(\"staticModelConfigRef\").toRec catch return blankChart(\"Old anomaly record. Missing staticModelConfigRef. Reobserve anomalies and try again.\")\n \n-  // Dates+  // Dates -----> -3mo to +3mo?\n+  anomalyStartTS  : anomalyRec.get(\"ts\")\n+  extendedStartTS : anomalyStartTS - 3mo\n+  anomalyEndTS    : anomalyStartTS + anomalyRec.get(\"duration\")\n+  extendedEndTS   : anomalyEndTS + 3mo\n+\n+  // Check hisStart and hisEnd for both points\n+  modelPointStartDate: modelPointRef.toRec.get(\"hisStart\")\n+  modelPointEndDate  : modelPointRef.toRec.get(\"hisEnd\")\n+    // Usually model point would be the bottleneck ===================> TODO: How can we show the meter data and perhaps the adaptiveBaseline modelPoint along with confInts for the previous 3 months?\n+    //if (modelPointStartDate > extendedStartTS) extendedStartTS = modelPointStartDate\n+    //if (modelPointEndDate < extendedEndTS) extendedEndTS = modelPointEndDate\n+\n+  extendedSpan    : (extendedStartTS..extendedEndTS).toSpan\n+\n+  // His\n+  hisGrid: [targetVarRef, modelPointRef].hisRead(dateSpan).hisClip\n+  //hisGrid: try logic_kwModeling_getBaselineModelResults(staticModelConfig) catch (err) return blankChart(err.toStr)\n+  //  if (hisGrid.isStr) return blankChart(hisGrid)\n+\n+  confIntGrid         : try logic_kwModeling_getModelPointResults(modelPointRef).reorderKeepCols([\"ts\"]) catch (err) return blankChart(err.dis)\n+  filteredConfIntGrid : confIntGrid.findAll(v=> v.get(\"ts\") >= dateSpan.start and v.get(\"ts\") < dateSpan.end)  // filteredHisGrid.first.get(\"ts\") -1hr?\n+  loCol               : confIntGrid.colNames.find() r=>r.contains(\"lo\")\n+  hiCol               : confIntGrid.colNames.find() r=>r.contains(\"hi\")\n+\n+  //filteredHisGrid: hisGrid.findAll(v=> v.get(\"ts\") >= extendedStartTS and v.get(\"ts\") < extendedEndTS )\n+  //  if (filteredHisGrid.last.get(\"ts\") < extendedEndTS) extendedEndTS = filteredHisGrid.last.get(\"ts\")\n+\n+  //return blankChart(hisGrid.colNames.toStr)//.map(r=> {\"colName\":r}).toGrid\n+  //// Fill in the inital 3 months with Adaptive baseline modelPoint his data\n+  //if (filteredHisGrid.first.get(\"ts\") > extendedStartTS) do\n+  //\n+  //  // Get points:\n+  //  meterPoint:                  anomalyRec.get(\"targetVarRef\")\n+  //  adaptiveBaselineModelPoint:  try anomalyRec.get(\"anomalyTriggerRef\").toRec.get(\"dynamicModelPointRef\") catch (err) return blankChart(\"Widget trying to read a deleted Anomaly Trigger - Rerun Energy Agent Task\", err.dis)\n+  //  //return blankChart(adaptiveBaselineModelPoint.toStr)\n+  //  // Get His\n+  //  preAnomalySpan: (extendedStartTS..filteredHisGrid.first.get(\"ts\")).toSpan                                                       // filteredHisGrid.first.get(\"ts\") -1hr?\n+  //\n+  //  adaptiveModelHisGrid: [meterPoint, adaptiveBaselineModelPoint].hisRead(preAnomalySpan).hisClip\n+  //\n+  //  confIntGrid         : try logic_kwModeling_getModelPointResults(adaptiveBaselineModelPoint) catch (err) return blankChart(err.dis)\n+  //  filteredConfIntGrid : confIntGrid.findAll(v=> v.get(\"ts\") >= extendedStartTS and v.get(\"ts\") < filteredHisGrid.first.get(\"ts\"))  // filteredHisGrid.first.get(\"ts\") -1hr?\n+  //  loCol               : confIntGrid.colNames.find() r=>r.contains(\"lo\")\n+  //  hiCol               : confIntGrid.colNames.find() r=>r.contains(\"hi\")\n+  //\n+  //  preAnomalyHisGrid: hisJoin([adaptiveModelHisGrid, filteredConfIntGrid]).reorderKeepCols([\"ts\", \"v0\", hiCol, \"v1\", loCol]).renameCol(\"v1\", \"timegpt\")\n+  //  preAnomalyHisGrid = transferMeta(hisGrid, preAnomalyHisGrid)\n+//\n+  //  hisGrid = addRows(filteredHisGrid, preAnomalyHisGrid)\n+  //\n+  //end\n+\n+\n+  //holidayCalendar: read(calendar and dis==\"Holiday Calendar\", false).scheduleHis(extendedSpan)\n+  //                                                                  .map(r=> r = if (r.get(\"val\")!= true) r.set(\"val\", false) else r.set(\"val\", true))\n+  //                                                                  .addColMeta(\"holidayCalendar\", {chartGroup: \"duration\"})\n+\n+\n+  //Create Anomaly Duration Grid\n+  anomalyDurationGrid: {ts: anomalyStartTS, anomalyDuration: anomalyRec.get(\"duration\")}.toGrid.addMeta({hisStart: dateSpan.start, hisEnd: dateSpan.end, hisSize:1})\n+\n+\n+  allAnomalies: readAll(anomaly and targetVarRef==anomalyRec.get(\"targetVarRef\") and ts >= dateSpan.start and ts < dateSpan.end)\n+  try allAnomalies = allAnomalies.reorderCols([\"ts\", \"duration\"]).toGrid.addMeta({hisStart: dateSpan.start, hisEnd: dateSpan.end, hisSize:allAnomalies.size}) catch null\n+\n+  //out: hisJoin([hisGrid, anomalyDurationGrid, allAnomalies, holidayCalendar])\n+  lineCharts: hisJoin([hisGrid, filteredConfIntGrid]).addMeta({hisStart: dateSpan.start, hisEnd: dateSpan.end})\n+  runtimeCharts: hisJoin([ anomalyDurationGrid, allAnomalies])\n+  out: hisJoin([lineCharts, runtimeCharts])\n+\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+  // FORMAT\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+\n+  gridMeta: {title: \"Extended Model Prediction Preview - \" + anomalyRec.get(\"siteRef\").toRec.get(\"dis\"), subtitle: anomalyRec.get(\"targetVarRef\").toRec.get(\"navName\").toStr + \" (\"+dateSpan.start.date+\" to \"+dateSpan.end.date+\")\", hisStart:dateSpan.start, hisEnd:dateSpan.end}\n+\n+  // prezGrid for anomalyImpact color?\n+\n+  out = out.stream\n+           .addColMeta(\"ts\",            {dis:\"Timestamp\"})\n+           .addColMeta(\"v0\", {dis:\"Actual\", strokeWidth:1, chartGroup: \"predictions\", chartType:\"line\"})//\n+           .addColMeta(hiCol, {dis:\"Upper Bound\", strokeWidth:0.5, chartGroup: \"predictions\", chartAreaMode: \"nextSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n+           .addColMeta(\"v1\", {dis:\"Model\", strokeWidth:1.5, chartGroup: \"predictions\", chartType:\"line\", strokeDasharray:\"6,3\"})//\n+           .addColMeta(loCol, {dis:\"Lower Bound\", strokeWidth:0.5, chartGroup: \"predictions\", chartAreaMode: \"prevSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n+           .addColMeta(\"anomalyDuration\", {chartGroup: \"xduration\", chartType:\"runtime\", dis:\"Active Anomaly\"})\n+           .addColMeta(\"duration\", {chartGroup: \"xduration\", chartType:\"runtime\", dis: \"All Anomalies\"})\n+           .addMeta(gridMeta)\n+           .addPrez_chartOrder(\"\", [ \"v0\", \"v1\", loCol, hiCol])//\n+           .collect\n+\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+  //RETURN RESULT\n+  //////////////////////////////////////////////////////////////////////////////////////////\n+\n+  return out.reorderKeepCols([\"ts\", \"v0\", hiCol, \"v1\", loCol, \"anomalyDuration\", \"duration\"])\n+\n+end"
    },
    {
      "name": "wdg_chart_energyAgent_portfolioHealthOverview_portfolioPointPredictions",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/*\n*   Description :\n*    Display all connected sites along with relevant information and KPIs\n*\n*   Parameters :\n*     Type                   Name            Description\n*\n*\n*   Output :\n*     Type                   Name            Description\n*     Grid                   out            - A gbGrid containing all connected sites and relevant info/KPIs\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         09/08/2025      - Initial Creation\n*/\n\n// Example of how to use/test\n//\n//\n\n(utilType, dateSpan, opts:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // USER VALIDATION\n  validateUser()\n\n  dateSpan = dateSpan.toSpan\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // Find relevant portfolio level calculatedPoint/virtualPoint based on utility\n  portfolioMeterPoint: if      (utilType==\"all\")   try read(portfolioPoint and elec and power and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint) catch null\n                       else if (utilType==\"elec\")  try read(portfolioPoint and elec and power and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint) catch null\n                       else if (utilType==\"chw\")   try read(portfolioPoint and chilled and water and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint) catch null\n                       else if (utilType==\"steam\") try read(portfolioPoint and steam and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint) catch null\n                       else if (utilType==\"gas\")   try read(portfolioPoint and naturalGas and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint) catch null\n\n  if (portfolioMeterPoint.isNull) return {info: \"Portfolio Level Meter point doesn't exist for the selected utility.\"}.toGrid.addMeta({noData: \"Portfolio Level Meter point doesn't exist for the selected utility.\"}).addColMeta(\"info\", {dis: \"Information\"})\n\n\n// pointQuery portfolio summed calculatedPoint/virtualPoint\n\n  out: logic_energyAgent_meterPointToPredictionsChart(portfolioMeterPoint, dateSpan, opts)\n\n  return out\n\n\n\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // FORMAT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  gridMeta: {title: \"Extended Model Prediction Preview - \" + anomalyRec.get(\"siteRef\").toRec.get(\"dis\"), subtitle: anomalyRec.get(\"targetVarRef\").toRec.get(\"navName\").toStr + \" (\"+dateSpan.start.date+\" to \"+dateSpan.end.date+\")\", hisStart:dateSpan.start, hisEnd:dateSpan.end}\n\n  // prezGrid for anomalyImpact color?\n\n  out = out.stream\n           .addColMeta(\"ts\",            {dis:\"Timestamp\"})\n           .addColMeta(\"v0\", {dis:\"Actual\", strokeWidth:1, chartGroup: \"predictions\", chartType:\"line\"})//\n           .addColMeta(hiCol, {dis:\"Upper Bound\", strokeWidth:0.5, chartGroup: \"predictions\", chartAreaMode: \"nextSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n           .addColMeta(\"v1\", {dis:\"Model\", strokeWidth:1.5, chartGroup: \"predictions\", chartType:\"line\", strokeDasharray:\"6,3\"})//\n           .addColMeta(loCol, {dis:\"Lower Bound\", strokeWidth:0.5, chartGroup: \"predictions\", chartAreaMode: \"prevSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n           .addColMeta(\"anomalyDuration\", {chartGroup: \"xduration\", chartType:\"runtime\", dis:\"Active Anomaly\"})\n           .addColMeta(\"duration\", {chartGroup: \"xduration\", chartType:\"runtime\", dis: \"All Anomalies\"})\n           .addMeta(gridMeta)\n           .addPrez_chartOrder(\"\", [ \"v0\", \"v1\", loCol, hiCol])//\n           .collect\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  //RETURN RESULT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  return out.reorderKeepCols([\"ts\", \"v0\", hiCol, \"v1\", loCol, \"anomalyDuration\", \"duration\"])\n\nend",
      "local_src": "/* \n*   Description :\n*    Display all connected sites along with relevant information and KPIs\n*\n*   Parameters :\n*     Type                   Name            Description\n*\n*\n*   Output :\n*     Type                   Name            Description\n*     Grid                   out            - A gbGrid containing all connected sites and relevant info/KPIs\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         09/08/2025      - Initial Creation\n*/\n\n// Example of how to use/test\n//\n//\n\n(utilType, dateSpan, opts:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // USER VALIDATION\n  validateUser()\n\n  dateSpan = dateSpan.toSpan\n  \n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // Find relevant portfolio level calculatedPoint/virtualPoint based on utility\n  portfolioMeterPoint: if      (utilType==\"all\")   read(portfolioPoint and elec and power and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint)\n                       else if (utilType==\"elec\")  read(portfolioPoint and elec and power and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint)\n                       else if (utilType==\"chw\")   read(portfolioPoint and chilled and water and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint)\n                       else if (utilType==\"steam\") read(portfolioPoint and steam and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint)\n                       else if (utilType==\"gas\")   read(portfolioPoint and naturalGas and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint)\n  \n  if (portfolioMeterPoint.isNull) return {info: \"Portfolio Level Meter point doesn't exist for the selected utility.\"}.toGrid.addMeta({noData: \"Portfolio Level Meter point doesn't exist for the selected utility.\"}).addColMeta(\"info\", {dis: \"Information\"})\n\n\n// pointQuery portfolio summed calculatedPoint/virtualPoint\n\n  out: logic_energyAgent_meterPointToPredictionsChart_v2(portfolioMeterPoint, dateSpan, opts)\n\n  return out\n  \n  \n\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // FORMAT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  gridMeta: {title: \"Extended Model Prediction Preview - \" + anomalyRec.get(\"siteRef\").toRec.get(\"dis\"), \n             subtitle: anomalyRec.get(\"targetVarRef\").toRec.get(\"navName\").toStr + \" (\"+dateSpan.start.date+\" to \"+dateSpan.end.date+\")\", \n             hisStart:dateSpan.start, \n             hisEnd:dateSpan.end}\n\n  // prezGrid for anomalyImpact color?\n\n  out = out.stream\n           .addColMeta(\"ts\",            {dis:\"Timestamp\"})\n           .addColMeta(\"v0\", {dis:\"Actual\", strokeWidth:1, chartGroup: \"predictions\", chartType:\"line\"})//\n           .addColMeta(hiCol, {dis:\"Upper Bound\", strokeWidth:0.5, chartGroup: \"predictions\", chartAreaMode: \"nextSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n           .addColMeta(\"v1\", {dis:\"Model\", strokeWidth:1.5, chartGroup: \"predictions\", chartType:\"line\", strokeDasharray:\"6,3\"})//\n           .addColMeta(loCol, {dis:\"Lower Bound\", strokeWidth:0.5, chartGroup: \"predictions\", chartAreaMode: \"prevSeries\", color:\"#FDD081\", opacity: 0.01, chartType:\"line\"})//\n           .addColMeta(\"anomalyDuration\", {chartGroup: \"xduration\", chartType:\"runtime\", dis:\"Active Anomaly\"})\n           .addColMeta(\"duration\", {chartGroup: \"xduration\", chartType:\"runtime\", dis: \"All Anomalies\"})\n           .addMeta(gridMeta)\n           .addPrez_chartOrder(\"\", [ \"v0\", \"v1\", loCol, hiCol])//\n           .collect\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  //RETURN RESULT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  return out.reorderKeepCols([\"ts\", \"v0\", hiCol, \"v1\", loCol, \"anomalyDuration\", \"duration\"])\n\nend",
      "diff": "@@ -41,18 +41,18 @@   //////////////////////////////////////////////////////////////////////////////////////////\n \n   // Find relevant portfolio level calculatedPoint/virtualPoint based on utility\n-  portfolioMeterPoint: if      (utilType==\"all\")   try read(portfolioPoint and elec and power and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint) catch null\n-                       else if (utilType==\"elec\")  try read(portfolioPoint and elec and power and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint) catch null\n-                       else if (utilType==\"chw\")   try read(portfolioPoint and chilled and water and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint) catch null\n-                       else if (utilType==\"steam\") try read(portfolioPoint and steam and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint) catch null\n-                       else if (utilType==\"gas\")   try read(portfolioPoint and naturalGas and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint) catch null\n+  portfolioMeterPoint: if      (utilType==\"all\")   read(portfolioPoint and elec and power and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint)\n+                       else if (utilType==\"elec\")  read(portfolioPoint and elec and power and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint)\n+                       else if (utilType==\"chw\")   read(portfolioPoint and chilled and water and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint)\n+                       else if (utilType==\"steam\") read(portfolioPoint and steam and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint)\n+                       else if (utilType==\"gas\")   read(portfolioPoint and naturalGas and his and virtualPoint and not cost and not savings and not modelPoint and not syntheticPoint)\n \n   if (portfolioMeterPoint.isNull) return {info: \"Portfolio Level Meter point doesn't exist for the selected utility.\"}.toGrid.addMeta({noData: \"Portfolio Level Meter point doesn't exist for the selected utility.\"}).addColMeta(\"info\", {dis: \"Information\"})\n \n \n // pointQuery portfolio summed calculatedPoint/virtualPoint\n \n-  out: logic_energyAgent_meterPointToPredictionsChart(portfolioMeterPoint, dateSpan, opts)\n+  out: logic_energyAgent_meterPointToPredictionsChart_v2(portfolioMeterPoint, dateSpan, opts)\n \n   return out\n \n@@ -64,7 +64,10 @@   // FORMAT\n   //////////////////////////////////////////////////////////////////////////////////////////\n \n-  gridMeta: {title: \"Extended Model Prediction Preview - \" + anomalyRec.get(\"siteRef\").toRec.get(\"dis\"), subtitle: anomalyRec.get(\"targetVarRef\").toRec.get(\"navName\").toStr + \" (\"+dateSpan.start.date+\" to \"+dateSpan.end.date+\")\", hisStart:dateSpan.start, hisEnd:dateSpan.end}\n+  gridMeta: {title: \"Extended Model Prediction Preview - \" + anomalyRec.get(\"siteRef\").toRec.get(\"dis\"),\n+             subtitle: anomalyRec.get(\"targetVarRef\").toRec.get(\"navName\").toStr + \" (\"+dateSpan.start.date+\" to \"+dateSpan.end.date+\")\",\n+             hisStart:dateSpan.start,\n+             hisEnd:dateSpan.end}\n \n   // prezGrid for anomalyImpact color?\n \n"
    },
    {
      "name": "wdg_chw_chart_supTempSpReset",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(site, dates, opts:{equip:null}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  //INPUTS\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n\n  //logic\n  data: try logic_chw_supTempSpReset(site, dates, opts)\n        catch (err) return blankChart(\"Unable to calculate.\", err.dis)\n  if(data.isStr) return blankChart(data)\n\n\n  //format\n  out: data\n\n  // return out\n  return out\nend",
      "local_src": "(site, dates, opts:{equip:null}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  //INPUTS\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n\n  //logic\n  data: try logic_chw_supTempSpReset(site, dates, opts)\n        catch (err) return blankChart(\"Unable to calculate.\", err.dis)\n  if(data.isStr) return blankChart(data)\n  \n  //format\n  out: data\n\n  // return out\n  return out\nend",
      "diff": "@@ -16,7 +16,6 @@         catch (err) return blankChart(\"Unable to calculate.\", err.dis)\n   if(data.isStr) return blankChart(data)\n \n-\n   //format\n   out: data\n \n"
    },
    {
      "name": "wdg_chw_chart_temperaturesDeltaT",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     This function takes an site, a DateSpan, an optionally opts to\n*     show in a chart widget information regarding deltaT of the\n*     building loop between the supply and return temperatures.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When        Change\n*     ?                      ?           - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024   - Added loop override support (Line 22)\n*/\n(target, dates, opts:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  equipRec: if (target.toRec.has(\"site\"))\n              try   read(equip and chilled and plant and water and siteRef == targetId)\n              catch return blankChart(\"No CHWS in this Site\")\n            else targetRec\n  equipId: equipRec->id\n\n  siteRec: if(targetRec.has(\"site\")) target.toRec else targetRec.get(\"siteRef\")\n  siteId: siteRec->id\n\n  numBars: if (opts.has(\"numBars\")) opts.get(\"numBars\") else 10\n  dates = dates.toSpan\n\n  his : logic_chws_deltaT(equipId, dates, opts)\n\n  // Handle Edge-case\n  if (his.isStr) return null\n\n  // Return his\n  try valueCol : his.colToList(\"delta\").findAll(v=>v.isNumber) catch return blankChart(\"Missing Data\")\n  min : valueCol.fold(min)\n  max : valueCol.fold(max)\n  spread : max - min\n\n  // Return spread\n  if(spread < 10) numBars = floor(spread).as(1)\n\n  // CHWS 2.0 Preparation\n  funcName: \"wdg_chw_chart_temperaturesDeltaT\"\n  titles: logicFuncTitle(funcName, equipRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              chartLegend:\"hide\"\n              }\n\n  out: his.keepCols([\"ts\",\"delta\"]).createHistogram(numBars,{}).addMeta(outputMeta)\n  return out.chart\nend",
      "local_src": "/*\n*   Description :\n*     This function takes an site, a DateSpan, an optionally opts to\n*     show in a chart widget information regarding deltaT of the\n*     building loop between the supply and return temperatures.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When        Change\n*     ?                      ?           - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024   - Added loop override support (Line 22)\n*/\n(target, dates, opts:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  equipRec: if (target.toRec.has(\"site\"))\n              try   read(equip and chilled and plant and water and siteRef == targetId)\n              catch return blankChart(\"No CHWS in this Site\")\n            else targetRec\n  equipId: equipRec->id\n\n  siteRec: if(targetRec.has(\"site\")) target.toRec else targetRec.get(\"siteRef\")\n  siteId: siteRec->id\n\n  numBars: if (opts.has(\"numBars\")) opts.get(\"numBars\") else 10\n  dates = dates.toSpan\n\n  his : logic_chws_deltaT(equipId, dates, opts)\n\n  // Handle Edge-case\n  if (his.isStr) return null\n\n  // Return his\n  try valueCol : his.colToList(\"delta\").findAll(v=>v.isNumber) catch return blankChart(\"Missing Data\")\n  min : valueCol.fold(min)\n  max : valueCol.fold(max)\n  spread : max - min\n\n  // Return spread\n  if(spread < 10) numBars = floor(spread).as(1)\n\n  // CHWS 2.0 Preparation\n  funcName: \"wdg_chw_chart_temperaturesDeltaT\"\n  titles: logicFuncTitle(funcName, equipRec, dates)\n  outputMeta: {title:\"Temperature \u0394T\",\n              subtitle: titles[1],\n              funcName: funcName,\n              chartLegend:\"hide\"\n              }\n\n  out: his.keepCols([\"ts\",\"delta\"]).createHistogram(numBars,{}).addMeta(outputMeta)\n  return out.chart\nend",
      "diff": "@@ -52,7 +52,7 @@   // CHWS 2.0 Preparation\n   funcName: \"wdg_chw_chart_temperaturesDeltaT\"\n   titles: logicFuncTitle(funcName, equipRec, dates)\n-  outputMeta: {title:titles[0],\n+  outputMeta: {title:\"Temperature \u0394T\",\n               subtitle: titles[1],\n               funcName: funcName,\n               chartLegend:\"hide\"\n"
    },
    {
      "name": "wdg_chws_chart_chillerStaging",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     This function takes an site, a DateSpan, an optionally opts to\n*     show in a chart widget information regarding chiller staging in the facility.\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/12/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024       - Added loop override support (Lines 34-38)\n*/\n(target, dates, opts:{}) => do\n\n  // Validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  equipRec: if (target.toRec.has(\"site\"))\n              try   read(equip and chilled and plant and water and siteRef == targetId)\n              catch return blankChart(\"No CHWS in this Site\")\n            else targetRec\n  equipId: equipRec->id\n\n  siteRec: if(targetRec.has(\"site\")) target.toRec else targetRec.get(\"siteRef\")\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  // Call Logic Funcs\n  bldgPoints : buildingLoopDigest(equipId, opts).get(\"buildingLoop\")\n\n  // Replaced logic to support loop overrides.\n  chillers: try chwsDigest(equipId, opts).get(\"primaryLoop\").first.get(\"chillers\")\n            catch return blankChart(\"No Chillers in this Site\")\n\n  // Retrieve Temp Points\n  tempPoints:[]\n  if (bldgPoints.isNonNull and not bldgPoints.isStr) do\n    supplyTemp:  bldgPoints.first.get(\"chw_supply_temp\")\n    returnTemp:  bldgPoints.first.get(\"chw_return_temp\")\n    tempPoints = [supplyTemp, returnTemp].findAll(v => not v.isStr)\n  end\n\n  // Retrieve Chiller Points\n  chillerPoints: []\n  chillers.each() chiller => do\n    // Find all points\n    points: chiller.get(\"points\")\n\n    // Retrieve needed points\n    chlr_fla: points.get(\"chlr_fla\")\n    chlr_power: points.get(\"chlr_power\")\n    chlr_status: points.get(\"chlr_status\")\n\n    // Keep existing points\n    existing : [chlr_fla, chlr_power, chlr_status].findAll(v => not v.isStr)\n    if (not existing.isEmpty) chillerPoints = chillerPoints.add(existing)\n  end\n\n  // Combine points\n  points: [tempPoints, chillerPoints].flatten\n\n  // Retrieve his of every point (rolling up points that can be rolled up)\n  hisPoints : points.map(point => try hisRead(point, dates, {-limit}).hisInterpolate.hisClip.hisRollupAuto catch hisRead(point, dates, {-limit}).hisInterpolate.hisClip)\n\n  // If there is no his data, return blankChart explaining as much.\n  if (hisPoints.isEmpty) return blankChart(\"No History Data available for this site\")\n\n  // Join all his\n  his : hisJoin(hisPoints)\n\n  // CHWS 2.0 Preparation\n  funcName: \"wdg_chws_chart_chillerStaging\"\n  titles: logicFuncTitle(funcName, equipRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  out : his.stream\n           .addMeta(outputMeta)\n           .collect\n  return out.chart\nend",
      "local_src": "/*\n*   Description :\n*     This function takes an site, a DateSpan, an optionally opts to\n*     show in a chart widget information regarding chiller staging in the facility.\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/12/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/8/2024       - Added loop override support (Lines 34-38)\n*/\n(target, dates, opts:{}) => do\n\n  // Validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize Inputs\n  targetRec: target.toRec\n  targetId: targetRec->id\n  equipRec: if (target.toRec.has(\"site\"))\n              try   read(equip and chilled and plant and water and siteRef == targetId)\n              catch return blankChart(\"No CHWS in this Site\")\n            else targetRec\n  equipId: equipRec->id\n\n  siteRec: if(targetRec.has(\"site\")) target.toRec else targetRec.get(\"siteRef\")\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  // Call Logic Funcs\n  bldgPoints : buildingLoopDigest(equipId, opts).get(\"buildingLoop\")\n\n  // Replaced logic to support loop overrides.\n  chillers: try chwsDigest(equipId, opts).get(\"primaryLoop\").first.get(\"chillers\")\n            catch return blankChart(\"No Chillers in this Site\")\n            if (chillers.isNull) return blankChart(\"No Chillers in this Site\") // ADDED isNull CHECKING\n\n  // Retrieve Temp Points\n  tempPoints:[]\n  if (bldgPoints.isNonNull and not bldgPoints.isStr) do\n    supplyTemp:  bldgPoints.first.get(\"chw_supply_temp\")\n    returnTemp:  bldgPoints.first.get(\"chw_return_temp\")\n    tempPoints = [supplyTemp, returnTemp].findAll(v => not v.isStr)\n  end\n\n  // Retrieve Chiller Points\n  chillerPoints: []\n  chillers.each() chiller => do\n    // Find all points\n    points: chiller.get(\"points\")\n\n    // Retrieve needed points\n    chlr_fla: points.get(\"chlr_fla\")\n    chlr_power: points.get(\"chlr_power\")\n    chlr_status: points.get(\"chlr_status\")\n\n    // Keep existing points\n    existing : [chlr_fla, chlr_power, chlr_status].findAll(v => not v.isStr)\n    if (not existing.isEmpty) chillerPoints = chillerPoints.add(existing)\n  end\n\n  // Combine points\n  points: [tempPoints, chillerPoints].flatten\n\n  // Retrieve his of every point (rolling up points that can be rolled up)\n  hisPoints : points.map(point => try hisRead(point, dates, {-limit}).hisInterpolate.hisClip.hisRollupAuto catch hisRead(point, dates, {-limit}).hisInterpolate.hisClip)\n\n  // If there is no his data, return blankChart explaining as much.\n  if (hisPoints.isEmpty) return blankChart(\"No History Data available for this site\")\n\n  // Join all his\n  his : hisJoin(hisPoints)\n\n  // CHWS 2.0 Preparation\n  funcName: \"wdg_chws_chart_chillerStaging\"\n  titles: logicFuncTitle(funcName, equipRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              }\n\n  // Add Meta\n  out : his.stream\n           .addMeta(outputMeta)\n           .collect\n  return out.chart\nend",
      "diff": "@@ -37,6 +37,7 @@   // Replaced logic to support loop overrides.\n   chillers: try chwsDigest(equipId, opts).get(\"primaryLoop\").first.get(\"chillers\")\n             catch return blankChart(\"No Chillers in this Site\")\n+            if (chillers.isNull) return blankChart(\"No Chillers in this Site\") // ADDED isNull CHECKING\n \n   // Retrieve Temp Points\n   tempPoints:[]\n"
    },
    {
      "name": "wdg_chws_table_systemSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     This function takes an site, a DateSpan, an optionally opts to\n*     show in a table widget information regarding system KPIs.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/13/2024       - Initial Creation\n*/\n(site, dates, opts:{}) => do\n\n  // Validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize inputs\n  siteRec: site.toRec\n  siteId: site->id\n  dates = dates.toSpan\n\n  // Call logic\n  grid: logic_chws_systemSummary(siteId, dates, opts)\n\n  // Edge-case handling\n  if (grid.isStr) grid = [grid].toGrid\n\n  // Add Additional Meta\n  out : grid.stream\n            .addColMeta(\"val\", {dis: \"Info\"})\n            .addMeta({title: \"System\"})\n            .collect\n\n  return out.table\nend",
      "local_src": "/*\n*   Description :\n*     This function takes an site, a DateSpan, an optionally opts to\n*     show in a table widget information regarding system KPIs.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/13/2024       - Initial Creation\n*/\n(site, dates, opts:{}) => do\n\n  // Validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize inputs\n  siteRec: site.toRec\n  siteId: site->id\n  dates = dates.toSpan\n\n  // Call logic\n  grid: logic_chws_systemSummary(siteId, dates, opts)\n\n  // Edge-case handling\n  if (grid.isStr) grid = [grid].toGrid\n\n  // Add Additional Meta\n  out : grid.stream\n            .addColMeta(\"val\", {dis: \"Info\"})\n            .addMeta({title: site->dis + \" KPIs\"})\n            .collect\n\n  return out.table\nend",
      "diff": "@@ -32,7 +32,7 @@   // Add Additional Meta\n   out : grid.stream\n             .addColMeta(\"val\", {dis: \"Info\"})\n-            .addMeta({title: \"System\"})\n+            .addMeta({title: site->dis + \" KPIs\"})\n             .collect\n \n   return out.table\n"
    },
    {
      "name": "wdg_chws_widgetSelector",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     This function takes a widget, a site, a DateSpan, an optionally opts to\n*     show in a chart widget information regarding system KPIs.\n*   Parameters :\n*     Type                   Name            Description\n*     Str                    widget          - The widget to show.\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/13/2024       - Initial Creation\n*/\n(widget, site, dates, opts:{}) => do\n\n  // Validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  // Handle Opts\n  chwsEquipOverride : if (opts.has(\"chwsEquipOverride\")) opts.get(\"chwsEquipOverride\") else null\n\n  // Add it as rec to the opts list\n  targetId: if (chwsEquipOverride.isNull) siteId else chwsEquipOverride\n\n  // Charts\n  if (widget == \"Chiller Staging\")                        return wdg_chws_chart_chillerStaging(targetId, dates, opts)\n  if (widget == \"Differential Pressure Setpoint Reset\")   return wdg_chw_chart_difPressureSpReset(targetId, dates, opts)\n  if (widget == \"Supply Temperature Meeting Setpoint\")    return wdg_chw_chart_supTempMeetingSp(targetId, dates, opts)\n  if (widget == \"Supply Temperature Setpoint Resetting\")  return wdg_chw_chart_supTempSpReset(targetId, dates, opts)\n  if (widget == \"Temperature \u0394T\")                         return wdg_chw_chart_temperaturesDeltaT(targetId, dates, opts)\n\n  // Cards\n  /* None for now*/\n\n  // Tables\n  /* None for now*/\n\n  return [].toGrid.addMeta({noData:\"Select a different widget - \" + widget + \" unavailable\"})\nend",
      "local_src": "/*\n*   Description :\n*     This function takes a widget, a site, a DateSpan, an optionally opts to\n*     show in a chart widget information regarding system KPIs.\n*   Parameters :\n*     Type                   Name            Description\n*     Str                    widget          - The widget to show.\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/13/2024       - Initial Creation\n*/\n(widget, site, dates, opts:{}) => do\n\n  // Validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  // Handle Opts\n  chwsEquipOverride : if (opts.has(\"chwsEquipOverride\")) opts.get(\"chwsEquipOverride\") else null\n\n  // Add it as rec to the opts list\n  targetId: if (chwsEquipOverride.isNull) siteId else chwsEquipOverride\n\n  // Charts\n  if (widget == \"Chiller Staging\")                        return wdg_chws_chart_chillerStaging(targetId, dates, opts)\n  if (widget == \"Differential Pressure Setpoint Reset\")   return wdg_chw_chart_difPressureSpReset(targetId, dates, opts)\n  if (widget == \"Supply Temperature Meeting Setpoint\")    return wdg_chw_chart_supTempMeetingSp(targetId, dates, opts)\n  if (widget == \"Supply Temperature Setpoint Resetting\")  return wdg_chw_chart_supTempSpReset(targetId, dates, opts)\n  if (widget == \"Temperature \u0394T\")                         return wdg_chw_chart_temperaturesDeltaT(targetId, dates, opts)\n\n  if (widget == \"Building vs Plant \u0394T\")                    return wdg_chw_chart_bldgVsPlantDeltaT(targetId, dates, opts)\n  if (widget == \"CHW Valve Position\")      return wdg_chw_valves_chart(targetId, dates, opts)\n\n  // Cards\n  /* None for now*/\n\n  // Tables\n  /* None for now*/\n\n  return [].toGrid.addMeta({noData:\"Select a different widget - \" + widget + \" unavailable\"})\nend",
      "diff": "@@ -36,6 +36,9 @@   if (widget == \"Supply Temperature Setpoint Resetting\")  return wdg_chw_chart_supTempSpReset(targetId, dates, opts)\n   if (widget == \"Temperature \u0394T\")                         return wdg_chw_chart_temperaturesDeltaT(targetId, dates, opts)\n \n+  if (widget == \"Building vs Plant \u0394T\")                    return wdg_chw_chart_bldgVsPlantDeltaT(targetId, dates, opts)\n+  if (widget == \"CHW Valve Position\")      return wdg_chw_valves_chart(targetId, dates, opts)\n+\n   // Cards\n   /* None for now*/\n \n"
    },
    {
      "name": "wdg_energyAgent_anomalyDetection_widgetSelector",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "/*\n*   Description :\n*    Display all connected sites along with relevant information and KPIs\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Str                    wdg             Enum for selected widget to display (exoVarHis, siteSparks, exoVarSHAP, modelPredictionsWithoutAnomalyRef, modelPredictionsWithAnomalyRef\n*\n*   Output :\n*     Type                   Name            Description\n*     Grid                   out            - A gbGrid containing all connected sites and relevant info/KPIs\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         05/21/2025      - Initial Creation\n*/\n\n(wdg, site, utilityType, dates, rules, opts:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // USER VALIDATION\n  validateUser()\n\n  //if (anomalyRef.isNull or anomalyRef==@null)   return blankChart(\"Please select an anomaly to populate charts\")\n  // null anomaly refs only work with the following widgets:\n  //\n\n  //try do\n  //  anomalyRec : anomalyRef.toRec\n  //  anomalyRef = anomalyRec.get(\"id\")\n  //end catch return blankChart(\"Please refresh page and select an anomaly to populate charts\")\n\n  site = site.toRec\n  siteRef: site.get(\"id\")\n\n  // Nav resolve site, let the wdg input be resolved siteRef\n  //return blankChart(site.toStr)\n  //ips: site\tbinding\t\t../site, utilityType\tbinding\t\t../utilityType, dates\tbinding\t\t../dates, selectedAnomaly\tbinding\t\t../subView/selectedAnomaly\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  meterRec: if      (utilityType==\"elec\" or utilityType==\"all\") try pointQuery(\"elec_intervalPwr\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"steam\")                      try pointQuery(\"steam_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"chw\")                        try pointQuery(\"chilledWater_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"hhw\")                        try pointQuery(\"hotWater_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"naturalGas\")                 try pointQuery(\"natGas_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n            else if (utilityType==\"water\")                      try pointQuery(\"water_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n\n  // Call individual wdg/logic funcs here\n  // Handle which wdg to call/skip depending on available inputs here/\n\n  // akTODO: update wdg_energyAgent_anomalyDetection_exoVarProcessedHis and wdg_energyAgent_anomalyDetection_shapOverTime to work without anomalyRef\n  if      (wdg==\"exoVarHis\")                      wdg_energyAgent_anomalyDetection_exoVarProcessedHis(meterRec, dates) // return blankChart(\"exoVarHis widget implementation in Progress\") //wdg_energyAgent_anomalyDetection_exoVarProcessedHis\n  else if (wdg==\"exoVarSHAP\")                     wdg_energyAgent_anomalyDetection_shapOverTime(meterRec, dates)\n  else if (wdg==\"siteSparks\")                     wdg_energyAgent_anomalyDetection_concurrentSparks(site, dates, rules, opts)\n\n  // modelPredictionsWithoutAnomaly needs a specific utility, not \"all\"\n    // Consolidate in a single func, check for @null/null i/p, call the logicFunc for modelPredictionsWithoutAnomaly\n    // if utilityType==\"all\" return blankChart(\"In the absense of anomalies, a specific utilityType must be selected to view the modelPredictions for that utility meter\")\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // FORMAT\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // Should be handled by intermediate widgets\n\nend",
      "local_src": "/*\n*   Description :\n*    Display all connected sites along with relevant information and KPIs\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Str                    wdg             Enum for selected widget to display (exoVarHis, siteSparks, exoVarSHAP, modelPredictionsWithoutAnomalyRef, modelPredictionsWithAnomalyRef\n*\n*   Output :\n*     Type                   Name            Description\n*     Grid                   out            - A gbGrid containing all connected sites and relevant info/KPIs\n*\n*   Work :\n*     Who                    When            Change\n*     Apoorv Khanuja         05/21/2025      - Initial Creation\n*/\n\n(wdg, site, utility, dates, rules, opts:{}) => do\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // NORMALIZE + VALIDATE INPUTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n  // USER VALIDATION\n  validateUser()\n\n  //if (anomalyRef.isNull or anomalyRef==@null)   return blankChart(\"Please select an anomaly to populate charts\")\n  // null anomaly refs only work with the following widgets:\n  // \n  \n  //try do\n  //  anomalyRec : anomalyRef.toRec\n  //  anomalyRef = anomalyRec.get(\"id\")\n  //end catch return blankChart(\"Please refresh page and select an anomaly to populate charts\")\n  anomalyRef:null\n  \n  // Nav resolve site, let the wdg input be resolved siteRef\n  //return blankChart(site.toStr)\n  //ips: site\tbinding\t\t../site, utilityType\tbinding\t\t../utilityType, dates\tbinding\t\t../dates, selectedAnomaly\tbinding\t\t../subView/selectedAnomaly\n  \n  \n  //////////////////////////////////////////////////////////////////////////////////////////\n  // OPTS\n  //////////////////////////////////////////////////////////////////////////////////////////\n\n\n  //////////////////////////////////////////////////////////////////////////////////////////\n  // LOGIC\n  //////////////////////////////////////////////////////////////////////////////////////////\n  \n  // Call individual wdg/logic funcs here\n  // Handle which wdg to call/skip depending on available inputs here/\n  \n    // akTODO: update wdg_energyAgent_anomalyDetection_exoVarProcessedHis and wdg_energyAgent_anomalyDetection_shapOverTime to work without anomalyRef\n  if (wdg==\"exoVarHis\")                           wdg_energyAgent_anomalyDetection_exoVarProcessedHis(anomalyRef, dates) // return blankChart(\"exoVarHis widget implementation in Progress\") //wdg_energyAgent_anomalyDetection_exoVarProcessedHis\n  else if (wdg==\"exoVarSHAP\")                     wdg_energyAgent_anomalyDetection_shapOverTime(anomalyRef, dates)\n  else if (wdg==\"siteSparks\")                     wdg_energyAgent_anomalyDetection_concurrentSparks(site, dates, rules, opts)\n  //else if (wdg==\"modelPredictionsWithAnomalyRef\") wdg_anomalyDetection_v2_chart_modelPredictionsPreview_v2(anomalyRef) //return blankChart(\"modelPredictionsWithAnomalyRef widget implementation in Progress\")\n  \n\n  \n  // modelPredictionsWithoutAnomaly needs a specific utility, not \"all\"\n    // Consolidate in a single func, check for @null/null i/p, call the logicFunc for modelPredictionsWithoutAnomaly\n    // if utilityType==\"all\" return blankChart(\"In the absense of anomalies, a specific utilityType must be selected to view the modelPredictions for that utility meter\")\n\n  \n  //////////////////////////////////////////////////////////////////////////////////////////\n  // FORMAT\n  //////////////////////////////////////////////////////////////////////////////////////////\n  \n  // Should be handled by intermediate widgets\n\nend",
      "diff": "@@ -15,7 +15,7 @@ *     Apoorv Khanuja         05/21/2025      - Initial Creation\n */\n \n-(wdg, site, utilityType, dates, rules, opts:{}) => do\n+(wdg, site, utility, dates, rules, opts:{}) => do\n \n   //////////////////////////////////////////////////////////////////////////////////////////\n   // NORMALIZE + VALIDATE INPUTS\n@@ -32,9 +32,7 @@   //  anomalyRec : anomalyRef.toRec\n   //  anomalyRef = anomalyRec.get(\"id\")\n   //end catch return blankChart(\"Please refresh page and select an anomaly to populate charts\")\n-\n-  site = site.toRec\n-  siteRef: site.get(\"id\")\n+  anomalyRef:null\n \n   // Nav resolve site, let the wdg input be resolved siteRef\n   //return blankChart(site.toStr)\n@@ -50,20 +48,16 @@   // LOGIC\n   //////////////////////////////////////////////////////////////////////////////////////////\n \n-  meterRec: if      (utilityType==\"elec\" or utilityType==\"all\") try pointQuery(\"elec_intervalPwr\", {read, siteRef: siteRef, checked: false}) catch null\n-            else if (utilityType==\"steam\")                      try pointQuery(\"steam_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n-            else if (utilityType==\"chw\")                        try pointQuery(\"chilledWater_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n-            else if (utilityType==\"hhw\")                        try pointQuery(\"hotWater_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n-            else if (utilityType==\"naturalGas\")                 try pointQuery(\"natGas_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n-            else if (utilityType==\"water\")                      try pointQuery(\"water_intervalCons\", {read, siteRef: siteRef, checked: false}) catch null\n-\n   // Call individual wdg/logic funcs here\n   // Handle which wdg to call/skip depending on available inputs here/\n \n-  // akTODO: update wdg_energyAgent_anomalyDetection_exoVarProcessedHis and wdg_energyAgent_anomalyDetection_shapOverTime to work without anomalyRef\n-  if      (wdg==\"exoVarHis\")                      wdg_energyAgent_anomalyDetection_exoVarProcessedHis(meterRec, dates) // return blankChart(\"exoVarHis widget implementation in Progress\") //wdg_energyAgent_anomalyDetection_exoVarProcessedHis\n-  else if (wdg==\"exoVarSHAP\")                     wdg_energyAgent_anomalyDetection_shapOverTime(meterRec, dates)\n+    // akTODO: update wdg_energyAgent_anomalyDetection_exoVarProcessedHis and wdg_energyAgent_anomalyDetection_shapOverTime to work without anomalyRef\n+  if (wdg==\"exoVarHis\")                           wdg_energyAgent_anomalyDetection_exoVarProcessedHis(anomalyRef, dates) // return blankChart(\"exoVarHis widget implementation in Progress\") //wdg_energyAgent_anomalyDetection_exoVarProcessedHis\n+  else if (wdg==\"exoVarSHAP\")                     wdg_energyAgent_anomalyDetection_shapOverTime(anomalyRef, dates)\n   else if (wdg==\"siteSparks\")                     wdg_energyAgent_anomalyDetection_concurrentSparks(site, dates, rules, opts)\n+  //else if (wdg==\"modelPredictionsWithAnomalyRef\") wdg_anomalyDetection_v2_chart_modelPredictionsPreview_v2(anomalyRef) //return blankChart(\"modelPredictionsWithAnomalyRef widget implementation in Progress\")\n+\n+\n \n   // modelPredictionsWithoutAnomaly needs a specific utility, not \"all\"\n     // Consolidate in a single func, check for @null/null i/p, call the logicFunc for modelPredictionsWithoutAnomaly\n"
    },
    {
      "name": "wdg_hhws_chart_temperaturesDeltaT",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     This function takes an site, a DateSpan, an optionally opts to\n*     show in a histogram of temperature Delta T\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When            Change\n*     William Ludwig         8/19/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/10/2024      - Added individual loop override support (Line 34)\n*/\n(target, dates, opts:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  targetRec: target.toRec\n  targetId: targetRec.get(\"id\")\n  equipRec: if (target.toRec.has(\"site\"))\n              try   read(equip and hot and plant and water and not domestic and siteRef==targetId)\n              catch return blankChart(\"No HHWS in this Site\")\n            else targetRec\n  equipId: equipRec.get(\"id\")\n  dates = dates.toSpan\n\n  //opts\n  numBars: if (opts.has(\"numBars\")) opts.get(\"numBars\") else 10\n\n  his : logic_hhw_deltaT(equipId, dates, opts)\n\n  if (his.isStr) return blankChart(his)\n\n  valueCol : his.colToList(\"delta\").findAll(v=>v.isNumber)\n  min : valueCol.fold(min)\n  max : valueCol.fold(max)\n  spread : max - min\n\n  if(spread < 10) numBars = floor(spread).as(1)\n\n  // CHWS 2.0 Preparation\n  funcName: \"wdg_hhws_chart_temperaturesDeltaT\"\n  titles: logicFuncTitle(funcName, equipRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              chartLegend:\"hide\"\n              }\n\n  out: his.keepCols([\"ts\",\"delta\"]).createHistogram(numBars,{}).addMeta(outputMeta)\n  return out.chart\nend",
      "local_src": "/*\n*   Description :\n*     This function takes an site, a DateSpan, an optionally opts to\n*     show in a histogram of temperature Delta T\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When            Change\n*     William Ludwig         8/19/2024       - Initial Creation\n*     Thomas Kuhrke Limia    10/10/2024      - Added individual loop override support (Line 34)\n*/\n(target, dates, opts:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  targetRec: target.toRec\n  targetId: targetRec.get(\"id\")\n  equipRec: if (target.toRec.has(\"site\"))\n              try   read(equip and hot and plant and water and not domestic and siteRef==targetId)\n              catch return blankChart(\"No HHWS in this Site\")\n            else targetRec\n  equipId: equipRec.get(\"id\")\n  dates = dates.toSpan\n\n  //opts\n  numBars: if (opts.has(\"numBars\")) opts.get(\"numBars\") else 10\n\n  his : logic_hhw_deltaT(equipId, dates, opts)\n\n  if (his.isStr) return blankChart(his)\n\n  valueCol : his.colToList(\"delta\").findAll(v=>v.isNumber)\n  min : valueCol.fold(min)\n  max : valueCol.fold(max)\n  spread : max - min\n\n  if(spread < 10) numBars = floor(spread).as(1)\n\n  // CHWS 2.0 Preparation\n  funcName: \"wdg_hhws_chart_temperaturesDeltaT\"\n  titles: logicFuncTitle(funcName, equipRec, dates)\n  outputMeta: {title:titles[0],\n              subtitle: titles[1],\n              funcName: funcName,\n              chartLegend:\"hide\"\n              }\n\n  out: his.keepCols([\"ts\",\"delta\"]).createHistogram(numBars,{}).addMeta(outputMeta)\n  return out.chart.addMeta({title:\"HW System - Temperatures (\u0394T)\"})\nend",
      "diff": "@@ -53,5 +53,5 @@               }\n \n   out: his.keepCols([\"ts\",\"delta\"]).createHistogram(numBars,{}).addMeta(outputMeta)\n-  return out.chart\n+  return out.chart.addMeta({title:\"HW System - Temperatures (\u0394T)\"})\n end"
    },
    {
      "name": "wdg_hhws_table_systemSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "/*\n*   Description :\n*     This function takes an site, a DateSpan, an optionally opts to\n*     show in a table widget information regarding system KPIs.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/15/2024       - Initial Creation\n*/\n(site, dates, opts:{}) => do\n\n  // Validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize inputs\n  siteRec: site.toRec\n  siteId: site->id\n  dates = dates.toSpan\n\n  // Call logic\n  grid: logic_hhws_systemSummary(siteId, dates, opts)\n\n  // Edge-case handling\n  if (grid.isStr) grid = [grid].toGrid\n\n  // Add Additional Meta\n  out : grid.stream\n            .addColMeta(\"val\", {dis: \"Info\"})\n            .addMeta({title: \"System\"})\n            .collect\n\n  return out.table\nend",
      "local_src": "/*\n*   Description :\n*     This function takes an site, a DateSpan, an optionally opts to\n*     show in a table widget information regarding system KPIs.\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    site            - The site input data.\n*     DateSpan               dates           - The date range to look at.\n*     Dict                   opts            - Additional options that can be added.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    8/15/2024       - Initial Creation\n*/\n(site, dates, opts:{}) => do\n\n  // Validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  // Normalize inputs\n  siteRec: site.toRec\n  siteId: site->id\n  dates = dates.toSpan\n\n  // Call logic\n  grid: logic_hhws_systemSummary(siteId, dates, opts)\n\n  // Edge-case handling\n  if (grid.isStr) grid = [grid].toGrid\n\n  // Add Additional Meta\n  out : grid.stream\n            .addColMeta(\"val\", {dis: \"Info\"})\n            .addMeta({title: site->dis + \" KPIs\"})\n            .collect\n\n  return out.table\nend",
      "diff": "@@ -32,7 +32,7 @@   // Add Additional Meta\n   out : grid.stream\n             .addColMeta(\"val\", {dis: \"Info\"})\n-            .addMeta({title: \"System\"})\n+            .addMeta({title: site->dis + \" KPIs\"})\n             .collect\n \n   return out.table\n"
    },
    {
      "name": "wdg_portfolio_card_energy",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "//todo add emissions data\n\n(dates, opts:{}) => do\n  dates = dates.toSpan\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //opts\n  debugVal: opts.get(\"debug\")\n  debug: debugVal!=\"Disable\" and debugVal!=null\n  //debug:true\n\n  //get sites\n  grid: xq().xqProjs(navProjNames())\n            .xqDefine(\"dates\",dates)\n            .xqReadAll(site)\n            .xqMap(\"site => try {site:site->id}.merge(getUtilityMeterSummary(site,dates).meta) catch (err) {site:site->id, err:err}\")\n            .xqExecute\n\n  totalSites: grid.size\n  errSites: grid.findAll(r=>r.has(\"err\")).size\n  goodSites: grid.findAll(r=>r.missing(\"err\")).size\n  if (debug==\"Enable\") return grid.reorderKeepCols([\"site\",\"err\",\"elecMeta\",\"gasMeta\"])\n\n\n  //split elec and gas\n  elecGrid: grid.colToList(\"elecMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n  gasGrid: grid.colToList(\"gasMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n\n  if (debug==\"Elec\") return elecGrid\n  if (debug==\"Gas\")  return gasGrid\n\n  //split it up into new card\n  gas_usage_numbers:  gasGrid.colToList(\"gas_usage\").findAll(v=>v.isNumber)\n  gas_usage: gas_usage_numbers.fold(sum)\n  gas_cost:   gasGrid.colToList(\"gas_cost\").findAll(v=>v.isNumber).fold(sum)\n  gasMessage: errSites+\" errs, \"+ (goodSites - gas_usage_numbers.size)+\" missing\"\n\n  elec_usage_numbers:  elecGrid.colToList(\"elec_usage\").findAll(v=>v.isNumber)\n  elec_usage: elec_usage_numbers.fold(sum)\n  elec_cost:   try elecGrid.colToList(\"elec_cost\").findAll(v=>v.isNumber).fold(sum) catch \"Costs unavailable\"\n  elecMessage: errSites+\" errs, \"+ (goodSites - elec_usage_numbers.size)+\" missing\"\n\n  card: {title:\"Energy Summary\",\n         elecUse: elec_usage,\n         elecCost: elec_cost,\n         elecMessage:elecMessage,\n         gasUse: gas_usage,\n         gasCost: gas_cost,\n         gasMessage: gasMessage,\n         }\n\n  out: card.toGrid\n  try out=out.addColMeta(\"elecCost\",       {viz:\"barCell\", color:\"orange\", dis: \"Electricity Cost\", }) catch null\n  try out=out.addColMeta(\"elecUse\",        {color:\"orange\", dis: \"Electricity Use\", }) catch null\n  try out=out.addColMeta(\"elecMessage\",    {color:\"lightGray\", dis: \"Warning\", }) catch null\n  try out=out.addColMeta(\"gasCost\",        {viz:\"barCell\", color:\"purple\", dis: \"Gas Cost\", }) catch null\n  try out=out.addColMeta(\"gasUse\",         {color:\"purple\", dis: \"Gas Use\", }) catch null\n  try out=out.addColMeta(\"gasMessage\",    {color:\"lightGray\", dis: \"Warning\", }) catch null\n  try out=out.addMeta({vizByUnit}) catch null\n\n  return out\n\nend",
      "local_src": "//todo add emissions data\n\n(dates, opts:{}) => do\n\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //opts\n  debugVal: opts.get(\"debug\")\n  debug: debugVal!=\"Disable\" and debugVal!=null\n  //debug:true\n  \n  //Before caching\n  /*\n  //get sites\n  grid: xq().xqTimeout(5min).xqProjs(navProjNames())\n            .xqDefine(\"dates\",dates)\n            .xqReadAll(site)\n            .xqMap(\"site => try {site:site->id}.merge(getUtilityMeterSummary(site,dates).meta) catch (err) {site:site->id, err:err}\")\n            .xqExecute\n  */\n  \n  //Get Cached records\n  startDate: dates.start\n  endDate: dates.end\n  grid: readAll(cache and cache_portfolio_energySummary and cacheYear==\"all\" and ts >=startDate and ts<= endDate)\n        //.findAll(r => r.get(\"ts\") >= startDate and r.get(\"ts\") <= endDate)\n        //.reorderKeepCols([\"ts\"])\n        .removeCols([\"id\", \"cache\", \"cacheYear\", \"cache_portfolio_energySummary\"])\n  gridCols: grid.colNames.moveTo(\"ts\", 0)\n  grid = grid.reorderCols(gridCols)\n        //.renameCol(\"siteR\", \"site\")\n  //return grid\n  \n  //totalSites: grid.size\n  //errSites: grid.findAll(r=>r.has(\"err\")).size\n  //goodSites: grid.findAll(r=>r.missing(\"err\")).size\n  if (debug==\"Enable\") return grid.reorderKeepCols([\"site\",\"err\",\"elecMeta\",\"gasMeta\"])\n  //return grid\n  todaysDate: today()\n  card: {title:\"Energy Summary\"}\n  //split elec and gas\n  /*\n  if(grid.colNames.contains(\"elecMeta\")) do\n    elecGrid: grid.colToList(\"elecMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n    elec_usage_numbers:  elecGrid.colToList(\"elec_usage\").findAll(v=>v.isNumber)\n    elec_usage: elec_usage_numbers.fold(sum)\n    elec_cost:   try elecGrid.colToList(\"elec_cost\").findAll(v=>v.isNumber).fold(sum) catch \"Costs unavailable\"\n    elecMessage: errSites+\" errs, \"+ (goodSites - elec_usage_numbers.size)+\" missing\"\n    card = card.merge({\"elecUse\": elec_usage, \"elecCost\": elec_cost/*, \"elecMessage\":elecMessage*/})\n  end\n  */\n  //return grid.colNames\n  elec_usage_col_names : grid.colNames.findAll(r => r.contains(\"elec_usage\")).add(\"ts\")\n  x: grid.keepCols(elec_usage_col_names)\n  elec_usage: try x.hisFoldCols(sum).colToList(\"v0\").fold(sum) catch \"Incompatible Units\"\n  elec_cost_col_names : grid.colNames.findAll(r => r.contains(\"elec_cost\")).add(\"ts\")\n  y: grid.keepCols(elec_cost_col_names)\n  elec_cost: y.hisFoldCols(sum).colToList(\"v0\").fold(sum)\n  card = card.merge({\"elecUse\": elec_usage, \"elecCost\": elec_cost/*, \"elecMessage\":elecMessage*/})\n /* \n  if(grid.colNames.contains(\"hwMeta\")) do\n    hwGrid: grid.colToList(\"hwMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n    hw_usage_numbers:  hwGrid.colToList(\"hw_usage\").findAll(v=>v.isNumber).map(s => return s.to(\"MMBTU/h\"))\n    hw_usage: hw_usage_numbers.fold(sum).as(\"MMBTU\")\n    hw_cost:   try hwGrid.colToList(\"hw_cost\").findAll(v=>v.isNumber).fold(sum) catch \"Costs unavailable\"\n    hwMessage: errSites+\" errs, \"+ (goodSites - hw_usage_numbers.size)+\" missing\"\n    card = card.merge({\"hwUse\": hw_usage, \"hwCost\": hw_cost/*, \"hwMessage\":hwMessage*/})\n  end\n  */\n  hw_usage_col_names : grid.colNames.findAll(r => r.contains(\"hw_usage\")).add(\"ts\")\n  x= grid.keepCols(hw_usage_col_names)\n  hw_usage: try x.hisFoldCols(sum).colToList(\"v0\").fold(sum) catch \"Incompatible Units\"\n  hw_cost_col_names : grid.colNames.findAll(r => r.contains(\"hw_cost\")).add(\"ts\")\n  y= grid.keepCols(hw_cost_col_names)\n  hw_cost: y.hisFoldCols(sum).colToList(\"v0\").fold(sum)\n  card = card.merge({\"hwUse\": hw_usage, \"hwCost\": hw_cost/*, \"hwMessage\":hwMessage*/})\n  /*\n  if(grid.colNames.contains(\"chwMeta\")) do\n    chwGrid: grid.colToList(\"chwMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n    chw_usage_numbers:  chwGrid.colToList(\"chw_usage\").findAll(v=>v.isNumber)\n    chw_usage: chw_usage_numbers.fold(sum)\n    chw_cost:   try chwGrid.colToList(\"chw_cost\").findAll(v=>v.isNumber).fold(sum) catch \"Costs unavailable\"\n    chwMessage: errSites+\" errs, \"+ (goodSites - chw_usage_numbers.size)+\" missing\"\n    card = card.merge({\"chwUse\": chw_usage, \"chwCost\": chw_cost/*, \"chwMessage\":chwMessage*/})\n  end\n  */\n  chw_usage_col_names : grid.colNames.findAll(r => r.contains(\"chw_usage\")).add(\"ts\")\n  x= grid.keepCols(chw_usage_col_names)\n  chw_usage: try x.hisFoldCols(sum).colToList(\"v0\").fold(sum) catch \"Incompatible Units\"\n  chw_cost_col_names : grid.colNames.findAll(r => r.contains(\"chw_cost\")).add(\"ts\")\n  y= grid.keepCols(chw_cost_col_names)\n  chw_cost: y.hisFoldCols(sum).colToList(\"v0\").fold(sum)\n  card = card.merge({\"chwUse\": chw_usage, \"chwCost\": chw_cost/*, \"hwMessage\":hwMessage*/})\n  /*\n  if(grid.colNames.contains(\"steamMeta\")) do\n    steamGrid: grid.colToList(\"steamMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n    steam_usage_numbers:  steamGrid.colToList(\"steam_usage\").findAll(v=>v.isNumber).map(s => return s.to(\"klb/h\"))\n    steam_usage: steam_usage_numbers.fold(sum).as(\"klb\")\n    steam_cost:   try steamGrid.colToList(\"steam_cost\").findAll(v=>v.isNumber).fold(sum) catch \"Costs unavailable\"\n    steamMessage: errSites+\" errs, \"+ (goodSites - steam_usage_numbers.size)+\" missing\"\n    card = card.merge({\"steamUse\": steam_usage, \"steamCost\": steam_cost/*, \"steamMessage\":steamMessage*/})\n  end\n  */\n  steam_usage_col_names : grid.colNames.findAll(r => r.contains(\"steam_usage\")).add(\"ts\")\n  x= grid.keepCols(steam_usage_col_names)\n  steam_usage: try x.hisFoldCols(sum).colToList(\"v0\").fold(sum) catch \"Incompatible Units\"\n  steam_cost_col_names : grid.colNames.findAll(r => r.contains(\"steam_cost\")).add(\"ts\")\n  y= grid.keepCols(steam_cost_col_names)\n  steam_cost: y.hisFoldCols(sum).colToList(\"v0\").fold(sum)\n  card = card.merge({\"steamUse\": steam_usage, \"steamCost\": steam_cost/*, \"steamMessage\":steamMessage*/})\n\n  out: card.toGrid\n  try out=out.addColMetaClean(\"elecCost\",       {viz:\"barCell\", color:\"orange\", dis: \"Electricity Cost\", }) catch null\n  try out=out.addColMetaClean(\"elecUse\",        {color:\"orange\", dis: \"Electricity Use\", }) catch null\n  try out=out.addColMetaClean(\"elecMessage\",    {color:\"lightGray\", dis: \"Electricity Warning\", }) catch null\n  try out=out.addColMetaClean(\"hwCost\",       {viz:\"barCell\", color:\"orangeRed\", dis: \"Hot Water Cost\", }) catch null\n  try out=out.addColMetaClean(\"hwUse\",        {color:\"orangeRed\", dis: \"Hot Water Use\", }) catch null\n  try out=out.addColMetaClean(\"hwMessage\",    {color:\"lightGray\", dis: \"Hot Water Warning\", }) catch null\n  try out=out.addColMetaClean(\"chwCost\",       {viz:\"barCell\", color:\"skyBlue\", dis: \"Chilled Water Cost\", }) catch null\n  try out=out.addColMetaClean(\"chwUse\",        {color:\"skyBlue\", dis: \"Chilled Water Use\", }) catch null\n  try out=out.addColMetaClean(\"chwMessage\",    {color:\"lightGray\", dis: \"Chilled Water Warning\", }) catch null\n  try out=out.addColMetaClean(\"steamCost\",       {viz:\"barCell\", color:\"slateBlue\", dis: \"Steam Cost\", }) catch null\n  try out=out.addColMetaClean(\"steamUse\",        {color:\"slateBlue\", dis: \"Steam Use\", }) catch null\n  try out=out.addColMetaClean(\"steamMessage\",    {color:\"lightGray\", dis: \"Steam Warning\", }) catch null\n  try out=out.reorderKeepCols([\"elecUse\", \"elecCost\", \"chwUse\", \"chwCost\", \"steamUse\", \"steamCost\"]) catch null\n\n  return out//.sort()\n\nend",
      "diff": "@@ -1,7 +1,7 @@ //todo add emissions data\n \n (dates, opts:{}) => do\n-  dates = dates.toSpan\n+\n \n   //validate and authorize current user\n   kwLinkValidate(\"analytics\")\n@@ -11,55 +11,121 @@   debug: debugVal!=\"Disable\" and debugVal!=null\n   //debug:true\n \n+  //Before caching\n+  /*\n   //get sites\n-  grid: xq().xqProjs(navProjNames())\n+  grid: xq().xqTimeout(5min).xqProjs(navProjNames())\n             .xqDefine(\"dates\",dates)\n             .xqReadAll(site)\n             .xqMap(\"site => try {site:site->id}.merge(getUtilityMeterSummary(site,dates).meta) catch (err) {site:site->id, err:err}\")\n             .xqExecute\n+  */\n \n-  totalSites: grid.size\n-  errSites: grid.findAll(r=>r.has(\"err\")).size\n-  goodSites: grid.findAll(r=>r.missing(\"err\")).size\n+  //Get Cached records\n+  startDate: dates.start\n+  endDate: dates.end\n+  grid: readAll(cache and cache_portfolio_energySummary and cacheYear==\"all\" and ts >=startDate and ts<= endDate)\n+        //.findAll(r => r.get(\"ts\") >= startDate and r.get(\"ts\") <= endDate)\n+        //.reorderKeepCols([\"ts\"])\n+        .removeCols([\"id\", \"cache\", \"cacheYear\", \"cache_portfolio_energySummary\"])\n+  gridCols: grid.colNames.moveTo(\"ts\", 0)\n+  grid = grid.reorderCols(gridCols)\n+        //.renameCol(\"siteR\", \"site\")\n+  //return grid\n+\n+  //totalSites: grid.size\n+  //errSites: grid.findAll(r=>r.has(\"err\")).size\n+  //goodSites: grid.findAll(r=>r.missing(\"err\")).size\n   if (debug==\"Enable\") return grid.reorderKeepCols([\"site\",\"err\",\"elecMeta\",\"gasMeta\"])\n-\n-\n+  //return grid\n+  todaysDate: today()\n+  card: {title:\"Energy Summary\"}\n   //split elec and gas\n-  elecGrid: grid.colToList(\"elecMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n-  gasGrid: grid.colToList(\"gasMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n-\n-  if (debug==\"Elec\") return elecGrid\n-  if (debug==\"Gas\")  return gasGrid\n-\n-  //split it up into new card\n-  gas_usage_numbers:  gasGrid.colToList(\"gas_usage\").findAll(v=>v.isNumber)\n-  gas_usage: gas_usage_numbers.fold(sum)\n-  gas_cost:   gasGrid.colToList(\"gas_cost\").findAll(v=>v.isNumber).fold(sum)\n-  gasMessage: errSites+\" errs, \"+ (goodSites - gas_usage_numbers.size)+\" missing\"\n-\n-  elec_usage_numbers:  elecGrid.colToList(\"elec_usage\").findAll(v=>v.isNumber)\n-  elec_usage: elec_usage_numbers.fold(sum)\n-  elec_cost:   try elecGrid.colToList(\"elec_cost\").findAll(v=>v.isNumber).fold(sum) catch \"Costs unavailable\"\n-  elecMessage: errSites+\" errs, \"+ (goodSites - elec_usage_numbers.size)+\" missing\"\n-\n-  card: {title:\"Energy Summary\",\n-         elecUse: elec_usage,\n-         elecCost: elec_cost,\n-         elecMessage:elecMessage,\n-         gasUse: gas_usage,\n-         gasCost: gas_cost,\n-         gasMessage: gasMessage,\n-         }\n+  /*\n+  if(grid.colNames.contains(\"elecMeta\")) do\n+    elecGrid: grid.colToList(\"elecMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n+    elec_usage_numbers:  elecGrid.colToList(\"elec_usage\").findAll(v=>v.isNumber)\n+    elec_usage: elec_usage_numbers.fold(sum)\n+    elec_cost:   try elecGrid.colToList(\"elec_cost\").findAll(v=>v.isNumber).fold(sum) catch \"Costs unavailable\"\n+    elecMessage: errSites+\" errs, \"+ (goodSites - elec_usage_numbers.size)+\" missing\"\n+    card = card.merge({\"elecUse\": elec_usage, \"elecCost\": elec_cost/*, \"elecMessage\":elecMessage*/})\n+  end\n+  */\n+  //return grid.colNames\n+  elec_usage_col_names : grid.colNames.findAll(r => r.contains(\"elec_usage\")).add(\"ts\")\n+  x: grid.keepCols(elec_usage_col_names)\n+  elec_usage: try x.hisFoldCols(sum).colToList(\"v0\").fold(sum) catch \"Incompatible Units\"\n+  elec_cost_col_names : grid.colNames.findAll(r => r.contains(\"elec_cost\")).add(\"ts\")\n+  y: grid.keepCols(elec_cost_col_names)\n+  elec_cost: y.hisFoldCols(sum).colToList(\"v0\").fold(sum)\n+  card = card.merge({\"elecUse\": elec_usage, \"elecCost\": elec_cost/*, \"elecMessage\":elecMessage*/})\n+ /*\n+  if(grid.colNames.contains(\"hwMeta\")) do\n+    hwGrid: grid.colToList(\"hwMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n+    hw_usage_numbers:  hwGrid.colToList(\"hw_usage\").findAll(v=>v.isNumber).map(s => return s.to(\"MMBTU/h\"))\n+    hw_usage: hw_usage_numbers.fold(sum).as(\"MMBTU\")\n+    hw_cost:   try hwGrid.colToList(\"hw_cost\").findAll(v=>v.isNumber).fold(sum) catch \"Costs unavailable\"\n+    hwMessage: errSites+\" errs, \"+ (goodSites - hw_usage_numbers.size)+\" missing\"\n+    card = card.merge({\"hwUse\": hw_usage, \"hwCost\": hw_cost/*, \"hwMessage\":hwMessage*/})\n+  end\n+  */\n+  hw_usage_col_names : grid.colNames.findAll(r => r.contains(\"hw_usage\")).add(\"ts\")\n+  x= grid.keepCols(hw_usage_col_names)\n+  hw_usage: try x.hisFoldCols(sum).colToList(\"v0\").fold(sum) catch \"Incompatible Units\"\n+  hw_cost_col_names : grid.colNames.findAll(r => r.contains(\"hw_cost\")).add(\"ts\")\n+  y= grid.keepCols(hw_cost_col_names)\n+  hw_cost: y.hisFoldCols(sum).colToList(\"v0\").fold(sum)\n+  card = card.merge({\"hwUse\": hw_usage, \"hwCost\": hw_cost/*, \"hwMessage\":hwMessage*/})\n+  /*\n+  if(grid.colNames.contains(\"chwMeta\")) do\n+    chwGrid: grid.colToList(\"chwMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n+    chw_usage_numbers:  chwGrid.colToList(\"chw_usage\").findAll(v=>v.isNumber)\n+    chw_usage: chw_usage_numbers.fold(sum)\n+    chw_cost:   try chwGrid.colToList(\"chw_cost\").findAll(v=>v.isNumber).fold(sum) catch \"Costs unavailable\"\n+    chwMessage: errSites+\" errs, \"+ (goodSites - chw_usage_numbers.size)+\" missing\"\n+    card = card.merge({\"chwUse\": chw_usage, \"chwCost\": chw_cost/*, \"chwMessage\":chwMessage*/})\n+  end\n+  */\n+  chw_usage_col_names : grid.colNames.findAll(r => r.contains(\"chw_usage\")).add(\"ts\")\n+  x= grid.keepCols(chw_usage_col_names)\n+  chw_usage: try x.hisFoldCols(sum).colToList(\"v0\").fold(sum) catch \"Incompatible Units\"\n+  chw_cost_col_names : grid.colNames.findAll(r => r.contains(\"chw_cost\")).add(\"ts\")\n+  y= grid.keepCols(chw_cost_col_names)\n+  chw_cost: y.hisFoldCols(sum).colToList(\"v0\").fold(sum)\n+  card = card.merge({\"chwUse\": chw_usage, \"chwCost\": chw_cost/*, \"hwMessage\":hwMessage*/})\n+  /*\n+  if(grid.colNames.contains(\"steamMeta\")) do\n+    steamGrid: grid.colToList(\"steamMeta\").findAll(v=>v!=null).toGrid.map(r=>r.get(\"effectiveRollup\"))\n+    steam_usage_numbers:  steamGrid.colToList(\"steam_usage\").findAll(v=>v.isNumber).map(s => return s.to(\"klb/h\"))\n+    steam_usage: steam_usage_numbers.fold(sum).as(\"klb\")\n+    steam_cost:   try steamGrid.colToList(\"steam_cost\").findAll(v=>v.isNumber).fold(sum) catch \"Costs unavailable\"\n+    steamMessage: errSites+\" errs, \"+ (goodSites - steam_usage_numbers.size)+\" missing\"\n+    card = card.merge({\"steamUse\": steam_usage, \"steamCost\": steam_cost/*, \"steamMessage\":steamMessage*/})\n+  end\n+  */\n+  steam_usage_col_names : grid.colNames.findAll(r => r.contains(\"steam_usage\")).add(\"ts\")\n+  x= grid.keepCols(steam_usage_col_names)\n+  steam_usage: try x.hisFoldCols(sum).colToList(\"v0\").fold(sum) catch \"Incompatible Units\"\n+  steam_cost_col_names : grid.colNames.findAll(r => r.contains(\"steam_cost\")).add(\"ts\")\n+  y= grid.keepCols(steam_cost_col_names)\n+  steam_cost: y.hisFoldCols(sum).colToList(\"v0\").fold(sum)\n+  card = card.merge({\"steamUse\": steam_usage, \"steamCost\": steam_cost/*, \"steamMessage\":steamMessage*/})\n \n   out: card.toGrid\n-  try out=out.addColMeta(\"elecCost\",       {viz:\"barCell\", color:\"orange\", dis: \"Electricity Cost\", }) catch null\n-  try out=out.addColMeta(\"elecUse\",        {color:\"orange\", dis: \"Electricity Use\", }) catch null\n-  try out=out.addColMeta(\"elecMessage\",    {color:\"lightGray\", dis: \"Warning\", }) catch null\n-  try out=out.addColMeta(\"gasCost\",        {viz:\"barCell\", color:\"purple\", dis: \"Gas Cost\", }) catch null\n-  try out=out.addColMeta(\"gasUse\",         {color:\"purple\", dis: \"Gas Use\", }) catch null\n-  try out=out.addColMeta(\"gasMessage\",    {color:\"lightGray\", dis: \"Warning\", }) catch null\n-  try out=out.addMeta({vizByUnit}) catch null\n+  try out=out.addColMetaClean(\"elecCost\",       {viz:\"barCell\", color:\"orange\", dis: \"Electricity Cost\", }) catch null\n+  try out=out.addColMetaClean(\"elecUse\",        {color:\"orange\", dis: \"Electricity Use\", }) catch null\n+  try out=out.addColMetaClean(\"elecMessage\",    {color:\"lightGray\", dis: \"Electricity Warning\", }) catch null\n+  try out=out.addColMetaClean(\"hwCost\",       {viz:\"barCell\", color:\"orangeRed\", dis: \"Hot Water Cost\", }) catch null\n+  try out=out.addColMetaClean(\"hwUse\",        {color:\"orangeRed\", dis: \"Hot Water Use\", }) catch null\n+  try out=out.addColMetaClean(\"hwMessage\",    {color:\"lightGray\", dis: \"Hot Water Warning\", }) catch null\n+  try out=out.addColMetaClean(\"chwCost\",       {viz:\"barCell\", color:\"skyBlue\", dis: \"Chilled Water Cost\", }) catch null\n+  try out=out.addColMetaClean(\"chwUse\",        {color:\"skyBlue\", dis: \"Chilled Water Use\", }) catch null\n+  try out=out.addColMetaClean(\"chwMessage\",    {color:\"lightGray\", dis: \"Chilled Water Warning\", }) catch null\n+  try out=out.addColMetaClean(\"steamCost\",       {viz:\"barCell\", color:\"slateBlue\", dis: \"Steam Cost\", }) catch null\n+  try out=out.addColMetaClean(\"steamUse\",        {color:\"slateBlue\", dis: \"Steam Use\", }) catch null\n+  try out=out.addColMetaClean(\"steamMessage\",    {color:\"lightGray\", dis: \"Steam Warning\", }) catch null\n+  try out=out.reorderKeepCols([\"elecUse\", \"elecCost\", \"chwUse\", \"chwCost\", \"steamUse\", \"steamCost\"]) catch null\n \n-  return out\n+  return out//.sort()\n \n end"
    },
    {
      "name": "wdg_portfolio_card_energySavings",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "//todo add emissions data\n\n(dates, opts:{}) => do\n  dates = dates.toSpan\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //opts\n  debugVal: opts.get(\"debug\")\n  debug: debugVal!=\"Disable\" and debugVal!=null\n  //debug:true\n\n  //get sites\n  grid   :  xq().xqProjs(navProjNames())\n                .xqDefine(\"dates\",dates)\n                .xqReadAll(site)\n                .xqMap(\"\"\"site => do\n                          dict: {site:site->id}\n                          grid: wdg_site_card_costSavings(site,dates)\n                          try         dict=dict.merge(grid.first)\n                          catch (err) dict=dict.merge({err:err, output:grid})\n                          end\n                          \"\"\")\n                .xqExecute\n\n  totalSites: grid.size\n  errSites: grid.findAll(r=>r.has(\"err\")).size\n  goodSites: grid.findAll(r=>r.missing(\"err\")).size\n  if (debug==\"Enable\") return grid.reorderKeepCols([\"site\",\"err\"])\n\n  //split it up into columns\n  gasCost_numbers:  grid.colToList(\"gasCost\").findAll(v=>v.isNumber and v>0)\n  gasCost:   gasCost_numbers.fold(sum)\n\n  elecCost_numbers:  grid.colToList(\"elecCost\").findAll(v=>v.isNumber and v>0)\n  elecCost:   elecCost_numbers.fold(sum)\n\n  savingsIncentives_numbers:  grid.colToList(\"incentives\").findAll(v=>v.isNumber and v>0)\n  savingsIncentives:   savingsIncentives_numbers.fold(sum)\n\n  savingsExtendedLife_numbers:  grid.colToList(\"savingsExtendedLife\").findAll(v=>v.isNumber and v>0)\n  savingsExtendedLife:   savingsExtendedLife_numbers.fold(sum)\n\n  savingsAvoidedOutsourcing_numbers:  grid.colToList(\"savingsAvoidedOutsourcing\").findAll(v=>v.isNumber and v>0)\n  savingsAvoidedOutsourcing:   savingsAvoidedOutsourcing_numbers.fold(sum)\n\n  totalCost: [gasCost,elecCost,savingsIncentives,savingsExtendedLife,savingsAvoidedOutsourcing].findAll(v=>v.isNumber and v>0).fold(sum)\n\n  //put data into card and convert to grid\n  card :  {title:\"Cost Savings\",\n           totalCost       :    totalCost,\n           elecCost        :    elecCost,\n           gasCost         :    gasCost,\n           savingsExtendedLife:       savingsExtendedLife,\n           savingsAvoidedOutsourcing: savingsAvoidedOutsourcing,\n           incentives:                savingsIncentives\n           }\n  //return card\n  out: card.toGrid\n           .addMeta({vizByUnit})\n           .reorderKeepCols([\"totalCost\",\"elecCost\",\"gasCost\"])\n\n  out=out.addColMetaClean(\"totalCost\",{dis:\"Total Cost Savings\", viz:\"barCell\", color:\"gray\"})\n         .addColMetaClean(\"elecCost\", {dis:\"Elec Cost Savings\", viz:\"barCell\", color:\"orange\"})\n         .addColMetaClean(\"gasCost\",  {dis:\"Gas Cost Savings\", viz:\"barCell\", color:\"purple\"})\n         .addColMetaClean(\"savingsExtendedLife\",  {dis:\"Extended Life\", viz:\"barCell\", color:\"green\"})\n         .addColMetaClean(\"savingsAvoidedOutsourcing\",  {dis:\"Avoided Outsourcing\", viz:\"barCell\", color:\"blue\"})\n         .addColMetaClean(\"incentives\",  {dis:\"Incentives Paid\", viz:\"barCell\", color:\"pink\"})\n\n  //check if anything is not a number and alter the colMeta so it shows up\n  return out\nend\n  /*\n\n(dates) => do\n\n  sites: readAll(arc and projectSite and projectTrackerState==\"complete\")\n    .colToList(\"projectSite\")\n    .unique\n    .findAll(v=>v!=null)\n    .toRecList\n\n  grids: []\n  sites.each s => do\n    try grids=grids.add(wdg_site_energySavingsCard(s->id,dates))\n    catch null\n  end\n\n  data: grids.first\n  grids.each grd => data=data.addRow(grd.first)\n\n  data= sites.map(s => wdg_site_energySavingsCard(s->id,dates).first).toGrid\n\n  elecCost: data.colToList(\"elecCost\").findAll(v=>v>0).fold(sum)\n  gasCost:  data.colToList(\"gasCost\").findAll(v=>v>0).fold(sum)\n  savingsExtendedLife: data.colToList(\"savingsExtendedLife\").fold(sum)\n  savingsAvoidedOutsourcing: data.colToList(\"savingsAvoidedOutsourcing\").fold(sum)\n  incentives: data.colToList(\"incentives\").fold(sum)\n  totalCost: [elecCost,gasCost,savingsExtendedLife,savingsAvoidedOutsourcing, incentives].fold(sum)\n\n  savingsDate: today()\n  sites.each site => do\n    date: getSiteKickoffDate(site)\n    if (date!=null and date < savingsDate) savingsDate = date\n    end\n\n  card :  {title:\"Cost Savings\",\n           subtitle        :    savingsDate.toStr+\" to date\",\n           totalCost       :    totalCost.round.as(1),\n           elecCost        :    elecCost.round.as(1),\n           gasCost         :    gasCost.round.as(1),\n           savingsExtendedLife: savingsExtendedLife.as(1),\n           savingsAvoidedOutsourcing: savingsAvoidedOutsourcing.as(1),\n           incentives: incentives.as(1)\n           }\n  return card.toGrid\n             .addMeta({vizByUnit})\n             .addColMeta(\"totalCost\",{dis:\"Total Cost Savings\", viz:\"barCell\", color:\"gray\", format:\"\\$###,###\"})\n             .addColMeta(\"elecCost\", {dis:\"Elec Cost Savings\", viz:\"barCell\", color:\"orange\", format:\"\\$###,###\"})\n             .addColMeta(\"gasCost\",  {dis:\"Gas Cost Savings\", viz:\"barCell\", color:\"purple\", format:\"\\$###,###\"})\n             .addColMeta(\"savingsExtendedLife\",  {dis:\"Extended Life\", viz:\"barCell\", color:\"green\", format:\"\\$###,###\"})\n             .addColMeta(\"savingsAvoidedOutsourcing\",  {dis:\"Avoided Outsourcing\", viz:\"barCell\", color:\"blue\", format:\"\\$###,###\"})\n             .addColMeta(\"incentives\",  {dis:\"Incentives Paid\", viz:\"barCell\", color:\"pink\", format:\"\\$###,###\"})\n             .reorderKeepCols([\"totalCost\",\"elecCost\",\"gasCost\"])\nend\n*/",
      "local_src": "//todo add emissions data\n\n(dates, opts:{}) => do\n\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //opts\n  debugVal: opts.get(\"debug\")\n  debug: debugVal!=\"Disable\" and debugVal!=null\n  //debug:true\n\n  //get sites\n  grid   :  xq().xqTimeout(5min).xqProjs(navProjNames())\n                .xqDefine(\"dates\",dates)\n                .xqReadAll(site and workedOn)\n                .xqMap(\"\"\"site => do\n                          dict: {site:site->id}\n                          grid: wdg_site_card_costSavings(site,dates)\n                          try         dict=dict.merge(grid.first)\n                          catch (err) dict=dict.merge({err:err, output:grid})\n                          end\n                          \"\"\")\n                .xqExecute\n\n  totalSites: grid.size\n  errSites: grid.findAll(r=>r.has(\"err\")).size\n  goodSites: grid.findAll(r=>r.missing(\"err\")).size\n  //return goodSites\n  if (debug==\"Enable\") return grid.reorderKeepCols([\"site\",\"err\"])\n  //return grid\n  //split it up into columns\n  chwCost_numbers:  grid.colToList(\"chwCost\").findAll(v=>v.isNumber)\n  chwCost:   chwCost_numbers.fold(sum)\n  \n  steamCost_numbers:  grid.colToList(\"steamCost\").findAll(v=>v.isNumber)\n  steamCost:   steamCost_numbers.fold(sum)\n  \n  elecCost_numbers:  grid.colToList(\"elecCost\").findAll(v=>v.isNumber)\n  elecCost:   elecCost_numbers.fold(sum)\n\n\n  totalCost: [elecCost,chwCost,steamCost].findAll(v=>v.isNumber).fold(sum)\n\n  //put data into card and convert to grid\n  card :  {title:\"Cost Savings\",\n           totalCost       :    totalCost,\n           elecCost        :    elecCost,\n           chwCost         :    chwCost,\n           steamCost       :    steamCost,\n           }\n  //return card\n  out: card.toGrid\n           .addMeta({vizByUnit})\n           .reorderKeepCols([\"totalCost\",\"elecCost\",\"chwCost\",\"steamCost\"])\n\n  out=out.addColMetaClean(\"totalCost\",{dis:\"Total Cost Savings\", viz:\"barCell\", color:\"gray\"})\n         .addColMetaClean(\"elecCost\", {dis:\"Elec Cost Savings\", viz:\"barCell\", color:\"orange\"})\n         .addColMetaClean(\"chwCost\", {dis:\"Chilled Water Cost Savings\", viz:\"barCell\", color:\"skyBlue\"})\n         .addColMetaClean(\"steamCost\",  {dis:\"Steam Cost Savings\", viz:\"barCell\", color:\"slateBlue\"})\n  //check if anything is not a number and alter the colMeta so it shows up\n  return out\nend\n  /*\n\n(dates) => do\n\n  sites: readAll(arc and projectSite and projectTrackerState==\"complete\")\n    .colToList(\"projectSite\")\n    .unique\n    .findAll(v=>v!=null)\n    .toRecList\n\n  grids: []\n  sites.each s => do\n    try grids=grids.add(wdg_site_energySavingsCard(s->id,dates))\n    catch null\n  end\n\n  data: grids.first\n  grids.each grd => data=data.addRow(grd.first)\n\n  data= sites.map(s => wdg_site_energySavingsCard(s->id,dates).first).toGrid\n\n  elecCost: data.colToList(\"elecCost\").findAll(v=>v>0).fold(sum)\n  gasCost:  data.colToList(\"gasCost\").findAll(v=>v>0).fold(sum)\n  savingsExtendedLife: data.colToList(\"savingsExtendedLife\").fold(sum)\n  savingsAvoidedOutsourcing: data.colToList(\"savingsAvoidedOutsourcing\").fold(sum)\n  incentives: data.colToList(\"incentives\").fold(sum)\n  totalCost: [elecCost,gasCost,savingsExtendedLife,savingsAvoidedOutsourcing, incentives].fold(sum)\n\n  savingsDate: today()\n  sites.each site => do\n    date: getSiteKickoffDate(site)\n    if (date!=null and date < savingsDate) savingsDate = date\n    end\n\n  card :  {title:\"Cost Savings\",\n           subtitle        :    savingsDate.toStr+\" to date\",\n           totalCost       :    totalCost.round.as(1),\n           elecCost        :    elecCost.round.as(1),\n           gasCost         :    gasCost.round.as(1),\n           savingsExtendedLife: savingsExtendedLife.as(1),\n           savingsAvoidedOutsourcing: savingsAvoidedOutsourcing.as(1),\n           incentives: incentives.as(1)\n           }\n  return card.toGrid\n             .addMeta({vizByUnit})\n             .addColMeta(\"totalCost\",{dis:\"Total Cost Savings\", viz:\"barCell\", color:\"gray\", format:\"\\$###,###\"})\n             .addColMeta(\"elecCost\", {dis:\"Elec Cost Savings\", viz:\"barCell\", color:\"orange\", format:\"\\$###,###\"})\n             .addColMeta(\"gasCost\",  {dis:\"Gas Cost Savings\", viz:\"barCell\", color:\"purple\", format:\"\\$###,###\"})\n             .addColMeta(\"savingsExtendedLife\",  {dis:\"Extended Life\", viz:\"barCell\", color:\"green\", format:\"\\$###,###\"})\n             .addColMeta(\"savingsAvoidedOutsourcing\",  {dis:\"Avoided Outsourcing\", viz:\"barCell\", color:\"blue\", format:\"\\$###,###\"})\n             .addColMeta(\"incentives\",  {dis:\"Incentives Paid\", viz:\"barCell\", color:\"pink\", format:\"\\$###,###\"})\n             .reorderKeepCols([\"totalCost\",\"elecCost\",\"gasCost\"])\nend\n*/",
      "diff": "@@ -1,7 +1,7 @@ //todo add emissions data\n \n (dates, opts:{}) => do\n-  dates = dates.toSpan\n+\n \n   //validate and authorize current user\n   kwLinkValidate(\"analytics\")\n@@ -12,9 +12,9 @@   //debug:true\n \n   //get sites\n-  grid   :  xq().xqProjs(navProjNames())\n+  grid   :  xq().xqTimeout(5min).xqProjs(navProjNames())\n                 .xqDefine(\"dates\",dates)\n-                .xqReadAll(site)\n+                .xqReadAll(site and workedOn)\n                 .xqMap(\"\"\"site => do\n                           dict: {site:site->id}\n                           grid: wdg_site_card_costSavings(site,dates)\n@@ -27,47 +27,38 @@   totalSites: grid.size\n   errSites: grid.findAll(r=>r.has(\"err\")).size\n   goodSites: grid.findAll(r=>r.missing(\"err\")).size\n+  //return goodSites\n   if (debug==\"Enable\") return grid.reorderKeepCols([\"site\",\"err\"])\n+  //return grid\n+  //split it up into columns\n+  chwCost_numbers:  grid.colToList(\"chwCost\").findAll(v=>v.isNumber)\n+  chwCost:   chwCost_numbers.fold(sum)\n \n-  //split it up into columns\n-  gasCost_numbers:  grid.colToList(\"gasCost\").findAll(v=>v.isNumber and v>0)\n-  gasCost:   gasCost_numbers.fold(sum)\n+  steamCost_numbers:  grid.colToList(\"steamCost\").findAll(v=>v.isNumber)\n+  steamCost:   steamCost_numbers.fold(sum)\n \n-  elecCost_numbers:  grid.colToList(\"elecCost\").findAll(v=>v.isNumber and v>0)\n+  elecCost_numbers:  grid.colToList(\"elecCost\").findAll(v=>v.isNumber)\n   elecCost:   elecCost_numbers.fold(sum)\n \n-  savingsIncentives_numbers:  grid.colToList(\"incentives\").findAll(v=>v.isNumber and v>0)\n-  savingsIncentives:   savingsIncentives_numbers.fold(sum)\n \n-  savingsExtendedLife_numbers:  grid.colToList(\"savingsExtendedLife\").findAll(v=>v.isNumber and v>0)\n-  savingsExtendedLife:   savingsExtendedLife_numbers.fold(sum)\n-\n-  savingsAvoidedOutsourcing_numbers:  grid.colToList(\"savingsAvoidedOutsourcing\").findAll(v=>v.isNumber and v>0)\n-  savingsAvoidedOutsourcing:   savingsAvoidedOutsourcing_numbers.fold(sum)\n-\n-  totalCost: [gasCost,elecCost,savingsIncentives,savingsExtendedLife,savingsAvoidedOutsourcing].findAll(v=>v.isNumber and v>0).fold(sum)\n+  totalCost: [elecCost,chwCost,steamCost].findAll(v=>v.isNumber).fold(sum)\n \n   //put data into card and convert to grid\n   card :  {title:\"Cost Savings\",\n            totalCost       :    totalCost,\n            elecCost        :    elecCost,\n-           gasCost         :    gasCost,\n-           savingsExtendedLife:       savingsExtendedLife,\n-           savingsAvoidedOutsourcing: savingsAvoidedOutsourcing,\n-           incentives:                savingsIncentives\n+           chwCost         :    chwCost,\n+           steamCost       :    steamCost,\n            }\n   //return card\n   out: card.toGrid\n            .addMeta({vizByUnit})\n-           .reorderKeepCols([\"totalCost\",\"elecCost\",\"gasCost\"])\n+           .reorderKeepCols([\"totalCost\",\"elecCost\",\"chwCost\",\"steamCost\"])\n \n   out=out.addColMetaClean(\"totalCost\",{dis:\"Total Cost Savings\", viz:\"barCell\", color:\"gray\"})\n          .addColMetaClean(\"elecCost\", {dis:\"Elec Cost Savings\", viz:\"barCell\", color:\"orange\"})\n-         .addColMetaClean(\"gasCost\",  {dis:\"Gas Cost Savings\", viz:\"barCell\", color:\"purple\"})\n-         .addColMetaClean(\"savingsExtendedLife\",  {dis:\"Extended Life\", viz:\"barCell\", color:\"green\"})\n-         .addColMetaClean(\"savingsAvoidedOutsourcing\",  {dis:\"Avoided Outsourcing\", viz:\"barCell\", color:\"blue\"})\n-         .addColMetaClean(\"incentives\",  {dis:\"Incentives Paid\", viz:\"barCell\", color:\"pink\"})\n-\n+         .addColMetaClean(\"chwCost\", {dis:\"Chilled Water Cost Savings\", viz:\"barCell\", color:\"skyBlue\"})\n+         .addColMetaClean(\"steamCost\",  {dis:\"Steam Cost Savings\", viz:\"barCell\", color:\"slateBlue\"})\n   //check if anything is not a number and alter the colMeta so it shows up\n   return out\n end\n"
    },
    {
      "name": "wdg_portfolio_card_sitesConnected",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "() => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //get all sites\n  sites: xq().xqReadAll(site).xqExecute()\n  sitesWithPoints: sites.map(r => {site:r->id, points:xq().xqReadAll(point and hisEnd and siteRef==r->id).xqExecute().size}).findAll(r => r.get(\"points\")>0)\n\n  card: {primary: sitesWithPoints.size.toStr+\"/\"+sites.size.toStr,\n         title: \"Sites Connected\"\n         }\n  out: card.toGrid\nend",
      "local_src": "(opts:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //opts\n  projSel: opts.get(\"projSel\")\n\n  //get all sites\n  //sites: xq().xqReadAll(site).xqExecute()\n  sites: readById(projSel).get(\"siteList\").toRecList\n  sitesWithPoints: sites.map(r => {site:r->id, points:xq().xqReadAll(point and hisEnd and siteRef==r->id).xqExecute().size}).findAll(r => r.get(\"points\")>0)\n\n  card: {primary: sitesWithPoints.size.toStr+\"/\"+sites.size.toStr,\n         title: \"Sites Connected\"\n         }\n  out: card.toGrid\nend",
      "diff": "@@ -1,10 +1,14 @@-() => do\n+(opts:{}) => do\n \n   //validate and authorize current user\n   kwLinkValidate(\"analytics\")\n \n+  //opts\n+  projSel: opts.get(\"projSel\")\n+\n   //get all sites\n-  sites: xq().xqReadAll(site).xqExecute()\n+  //sites: xq().xqReadAll(site).xqExecute()\n+  sites: readById(projSel).get(\"siteList\").toRecList\n   sitesWithPoints: sites.map(r => {site:r->id, points:xq().xqReadAll(point and hisEnd and siteRef==r->id).xqExecute().size}).findAll(r => r.get(\"points\")>0)\n \n   card: {primary: sitesWithPoints.size.toStr+\"/\"+sites.size.toStr,\n"
    },
    {
      "name": "wdg_portfolio_chart_cumulativeSavings",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(dates, opts:{}) => do\n  dates = dates.toSpan\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //opts\n  debugVal: opts.get(\"debug\")\n  debug: debugVal!=null and debugVal!=\"Disable\"\n\n  //get arcs and sites\n  sites: xq().xqReadAll(site).xqExecute\n  if (sites.size==0) return blankChart(\"No sites\", \"Please add sites to get started\")\n\n  portfolio: xq().xqProjs(navProjNames())\n                 .xqDefine(\"dates\",dates)\n                 .xqDefine(\"preDate\",dates.start - 1s)\n                 .xqReadAll(site)\n                 .xqMap(\"\"\"site => do\n                           try {site: site->id, output:wdg_site_chart_cumulativeSavings(site->id,dates).hisFoldCols(sum).addRow({ts:preDate,v0:0\\$}).addColMeta(\\\"v0\\\",{dis:site.dis}).renameCol(\\\"v0\\\",site.dis.toTagName)}\n                           catch (err) {site:site->id, err:err}\n                           end\n                           \"\"\")\n                 .xqExecute()\n\n  //handle errors\n  if (debug) return portfolio\n  goodSites: portfolio.findAll(r=>r.missing(\"err\") and r.has(\"output\") and r.get(\"output\").meta.has(\"hisStart\"))\n  badSites:  portfolio.findAll(r=>r.has(\"err\") and r.has(\"output\") and r.get(\"output\").meta.missing(\"hisStart\"))\n  totalSites: portfolio.size\n  if (goodSites.size==0) return blankChart(\"Cannot calculated cumulative savings.\",\"Please check and ensure the correct points are set up\")\n\n  //collect data\n  goodData: goodSites.colToList(\"output\")\n                     .hisJoin\n                     .addColMetaAll({hisMode:\"cov\"})\n\n\n  //output\n  try combinedHis: goodData.hisInterpolate.stackedAreaChart.hisRollup(avg,1mo).addColMetaAll({-hisMode})\n  catch (err) return blankChart(\"Error - please report.\", err.get(\"dis\"))\n  out: combinedHis.addMeta({title:\"Cumulative Cost Savings\", subtitle:\"Gas and Electric - \"+goodSites.size+\" / \"+totalSites+\" sites\", -chartLegend})\n\n  return out\nend",
      "local_src": "(dates, opts:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n  if(dates.end.date > today()) dates= (dates.start..(today()-10day)).toSpan\n\n  //opts\n  debugVal: opts.get(\"debug\")\n  debug: debugVal!=null and debugVal!=\"Disable\"\n  projSel: opts.get(\"projSel\")\n  projTypeName: readById(projSel).get(\"projName\")\n  siteList: readById(projSel).get(\"siteList\")\n  //return siteList\n  //get arcs and sites\n  sites: xq().xqTimeout(5min).xqReadAll(site).xqExecute\n  if (sites.size==0) return blankChart(\"No sites\", \"Please add sites to get started\")\n\n\n  portfolio: xq().xqTimeout(5min)\n                 .xqProjs(navProjNames())\n                 .xqDefine(\"dates\",dates)\n                 .xqDefine(\"preDate\",dates.start - 1s)\n                 .xqReadByIds(siteList)\n                 .xqMap(\"\"\"site => do\n                           try {site: site->id, output:wdg_site_chart_cumulativeSavings(site->id,dates).hisInterpolate().hisFoldCols(sum).addRow({ts:preDate,v0:0\\$}).addColMeta(\\\"v0\\\",{dis:site.dis}).renameCol(\\\"v0\\\",site.dis.toTagName)}\n                           catch (err) {site:site->id, err:err}\n                           end\n                           \"\"\")\n                 .xqExecute()\n\n  //handle errors\n  //return portfolio\n  if (debug) return portfolio\n  goodSites: portfolio.findAll(r=>r.missing(\"err\") and r.has(\"output\") and r.get(\"output\").meta.has(\"hisStart\"))\n  badSites:  portfolio.findAll(r=>r.has(\"err\") and r.has(\"output\") and r.get(\"output\").meta.missing(\"hisStart\"))\n  totalSites: portfolio.size\n  if (goodSites.size==0) return blankChart(\"Cannot calculated cumulative savings.\",\"Please check and ensure the correct points are set up\")\n\n  //collect data\n  goodData: goodSites.colToList(\"output\")\n                     .hisJoin\n                     .addColMetaAll({hisMode:\"cov\"})\n\n  //return goodData\n  //output\n  try combinedHis: goodData.hisInterpolate.stackedAreaChart.hisRollup(avg,1day).addColMetaAll({-hisMode})\n  catch (err) return blankChart(\"Error - please report.\", err.get(\"dis\"))\n  //return combinedHis//.hisFoldCols(sum)\n  out: combinedHis.addMeta({title: \"Project: \" + projTypeName + \" Cumulative Cost Savings\", subtitle:\"Electric, Hot Water, Chilled Water, Steam - \"+goodSites.size+\" / \"+totalSites+\" sites\", -chartLegend})\n\n  return out\nend",
      "diff": "@@ -1,28 +1,35 @@ (dates, opts:{}) => do\n-  dates = dates.toSpan\n+\n   //validate and authorize current user\n   kwLinkValidate(\"analytics\")\n+  if(dates.end.date > today()) dates= (dates.start..(today()-10day)).toSpan\n \n   //opts\n   debugVal: opts.get(\"debug\")\n   debug: debugVal!=null and debugVal!=\"Disable\"\n-\n+  projSel: opts.get(\"projSel\")\n+  projTypeName: readById(projSel).get(\"projName\")\n+  siteList: readById(projSel).get(\"siteList\")\n+  //return siteList\n   //get arcs and sites\n-  sites: xq().xqReadAll(site).xqExecute\n+  sites: xq().xqTimeout(5min).xqReadAll(site).xqExecute\n   if (sites.size==0) return blankChart(\"No sites\", \"Please add sites to get started\")\n \n-  portfolio: xq().xqProjs(navProjNames())\n+\n+  portfolio: xq().xqTimeout(5min)\n+                 .xqProjs(navProjNames())\n                  .xqDefine(\"dates\",dates)\n                  .xqDefine(\"preDate\",dates.start - 1s)\n-                 .xqReadAll(site)\n+                 .xqReadByIds(siteList)\n                  .xqMap(\"\"\"site => do\n-                           try {site: site->id, output:wdg_site_chart_cumulativeSavings(site->id,dates).hisFoldCols(sum).addRow({ts:preDate,v0:0\\$}).addColMeta(\\\"v0\\\",{dis:site.dis}).renameCol(\\\"v0\\\",site.dis.toTagName)}\n+                           try {site: site->id, output:wdg_site_chart_cumulativeSavings(site->id,dates).hisInterpolate().hisFoldCols(sum).addRow({ts:preDate,v0:0\\$}).addColMeta(\\\"v0\\\",{dis:site.dis}).renameCol(\\\"v0\\\",site.dis.toTagName)}\n                            catch (err) {site:site->id, err:err}\n                            end\n                            \"\"\")\n                  .xqExecute()\n \n   //handle errors\n+  //return portfolio\n   if (debug) return portfolio\n   goodSites: portfolio.findAll(r=>r.missing(\"err\") and r.has(\"output\") and r.get(\"output\").meta.has(\"hisStart\"))\n   badSites:  portfolio.findAll(r=>r.has(\"err\") and r.has(\"output\") and r.get(\"output\").meta.missing(\"hisStart\"))\n@@ -34,11 +41,12 @@                      .hisJoin\n                      .addColMetaAll({hisMode:\"cov\"})\n \n-\n+  //return goodData\n   //output\n-  try combinedHis: goodData.hisInterpolate.stackedAreaChart.hisRollup(avg,1mo).addColMetaAll({-hisMode})\n+  try combinedHis: goodData.hisInterpolate.stackedAreaChart.hisRollup(avg,1day).addColMetaAll({-hisMode})\n   catch (err) return blankChart(\"Error - please report.\", err.get(\"dis\"))\n-  out: combinedHis.addMeta({title:\"Cumulative Cost Savings\", subtitle:\"Gas and Electric - \"+goodSites.size+\" / \"+totalSites+\" sites\", -chartLegend})\n+  //return combinedHis//.hisFoldCols(sum)\n+  out: combinedHis.addMeta({title: \"Project: \" + projTypeName + \" Cumulative Cost Savings\", subtitle:\"Electric, Hot Water, Chilled Water, Steam - \"+goodSites.size+\" / \"+totalSites+\" sites\", -chartLegend})\n \n   return out\n end"
    },
    {
      "name": "wdg_portfolio_chart_energy",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(dates, opts:{}) => do\n  dates = dates.toSpan\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //opts\n  debugVal: opts.get(\"debug\")\n  debug: debugVal!=null and debugVal!=\"Disable\"\n\n  //get arcs and sites\n  sites: xq().xqReadAll(site).xqExecute\n  if (sites.size==0) return blankChart(\"No sites\", \"Please add sites to get started\")\n\n  //get energy data\n  portfolio: xq().xqProjs(navProjNames())\n                 .xqDefine(\"dates\",dates)\n                 .xqReadAll(site)\n                 .xqMap(\"\"\"site => do\n                           output: try getUtilityMeterSummary(site->id,dates).keepCols(\"ts,elec_usage,gas_usage\".split(\",\")).hisRollupAuto\n                                   catch (err) err\n                           try output=output.addColMeta(\"elec_usage\", {chartGroup:\"Elec\", dis:site.dis+\" (Elec)\", -color, subtitle:\"Electricity\"}) catch null\n                           try output=output.addColMeta(\"gas_usage\",  {chartGroup:\"Gas\",  dis:site.dis+\" (Gas)\", -color, subtitle:\"Gas\"}) catch null\n                           if (output.isDict) {site:site->id, err:output}\n                           else {site: site->id, output:output}\n                           end\n                           \"\"\")\n                 .xqExecute()\n\n  //handle errors\n  if (debug) return portfolio\n  goodSites: portfolio.findAll(r=>r.missing(\"err\") and r.has(\"output\") and r.get(\"output\").meta.has(\"hisStart\"))\n  badSites:  portfolio.findAll(r=>r.has(\"err\") and r.has(\"output\") and r.get(\"output\").meta.missing(\"hisStart\"))\n  totalSites: portfolio.size\n  if (goodSites.size==0) return blankChart(\"Cannot calculated cumulative savings.\",\"Please check and ensure the correct points are set up\")\n\n  //collect data\n  goodData: goodSites.colToList(\"output\")\n                     .hisJoin\n\n  //output\n  out: goodData.addMeta({title:\"Energy Usage\", subtitle:\"Gas and Electric - \"+goodSites.size+\" / \"+totalSites+\" sites\"})\n\n  return out\nend",
      "local_src": "(dates, opts:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //opts\n  debugVal: opts.get(\"debug\")\n  debug: debugVal!=null and debugVal!=\"Disable\"\n\n  //get arcs and sites\n  sites: xq().xqTimeout(5min).xqReadAll(site).xqExecute\n  if (sites.size==0) return blankChart(\"No sites\", \"Please add sites to get started\")\n\n  //get energy data\n  portfolio: xq().xqTimeout(5min)\n                 .xqProjs(navProjNames())\n                 .xqDefine(\"dates\",dates)\n                 .xqReadAll(site)\n                 .xqMap(\"\"\"site => do\n                           output: try getUtilityMeterSummary(site->id,dates).keepCols(\"ts,elec_usage,hw_usage,chw_usage,steam_usage\".split(\",\")).hisRollupAuto\n                                   catch (err) err\n                           try output=output.addColMeta(\"elec_usage\", {chartGroup:\"Elec\", dis:site.dis+\" (Elec)\", -color, subtitle:\"Electricity\"}) catch null\n                           try output=output.addColMeta(\"hw_usage\",  {chartGroup:\"Hot Water\",  dis:site.dis+\" (HW)\", -color, subtitle:\"Hot Water\"}) catch null\n                           try output=output.addColMeta(\"chw_usage\",  {chartGroup:\"Chilled Water\",  dis:site.dis+\" (CHW)\", -color, subtitle:\"Chilled Water\"}) catch null\n                           try output=output.addColMeta(\"steam_usage\",  {chartGroup:\"Steam\",  dis:site.dis+\" (Steam)\", -color, subtitle:\"Steam\"}) catch null\n                           if (output.isDict) {site:site->id, err:output}\n                           else {site: site->id, output:output}\n                           end\n                           \"\"\")\n                 .xqExecute()\n\n  //handle errors\n  if (debug) return portfolio.table\n  goodSites: portfolio.findAll(r=>r.missing(\"err\") and r.has(\"output\") and r.get(\"output\").meta.has(\"hisStart\"))\n  badSites:  portfolio.findAll(r=>r.has(\"err\") and r.has(\"output\") and r.get(\"output\").meta.missing(\"hisStart\"))\n  totalSites: portfolio.size\n  if (goodSites.size==0) return blankChart(\"Cannot calculated cumulative savings.\",\"Please check and ensure the correct points are set up\")\n\n  //collect data\n  goodData: goodSites.colToList(\"output\")\n                     .hisJoin\n\n  //output\n  out: goodData.addMeta({title:\"Energy Usage\", subtitle:\"Electric, Steam, Hot Water, and Chilled Water - \"+goodSites.size+\" / \"+totalSites+\" sites\"})\n\n  return out.addMeta({chartLegend:\"hide\"}).hisClip()\nend",
      "diff": "@@ -1,5 +1,5 @@ (dates, opts:{}) => do\n-  dates = dates.toSpan\n+\n   //validate and authorize current user\n   kwLinkValidate(\"analytics\")\n \n@@ -8,18 +8,21 @@   debug: debugVal!=null and debugVal!=\"Disable\"\n \n   //get arcs and sites\n-  sites: xq().xqReadAll(site).xqExecute\n+  sites: xq().xqTimeout(5min).xqReadAll(site).xqExecute\n   if (sites.size==0) return blankChart(\"No sites\", \"Please add sites to get started\")\n \n   //get energy data\n-  portfolio: xq().xqProjs(navProjNames())\n+  portfolio: xq().xqTimeout(5min)\n+                 .xqProjs(navProjNames())\n                  .xqDefine(\"dates\",dates)\n                  .xqReadAll(site)\n                  .xqMap(\"\"\"site => do\n-                           output: try getUtilityMeterSummary(site->id,dates).keepCols(\"ts,elec_usage,gas_usage\".split(\",\")).hisRollupAuto\n+                           output: try getUtilityMeterSummary(site->id,dates).keepCols(\"ts,elec_usage,hw_usage,chw_usage,steam_usage\".split(\",\")).hisRollupAuto\n                                    catch (err) err\n                            try output=output.addColMeta(\"elec_usage\", {chartGroup:\"Elec\", dis:site.dis+\" (Elec)\", -color, subtitle:\"Electricity\"}) catch null\n-                           try output=output.addColMeta(\"gas_usage\",  {chartGroup:\"Gas\",  dis:site.dis+\" (Gas)\", -color, subtitle:\"Gas\"}) catch null\n+                           try output=output.addColMeta(\"hw_usage\",  {chartGroup:\"Hot Water\",  dis:site.dis+\" (HW)\", -color, subtitle:\"Hot Water\"}) catch null\n+                           try output=output.addColMeta(\"chw_usage\",  {chartGroup:\"Chilled Water\",  dis:site.dis+\" (CHW)\", -color, subtitle:\"Chilled Water\"}) catch null\n+                           try output=output.addColMeta(\"steam_usage\",  {chartGroup:\"Steam\",  dis:site.dis+\" (Steam)\", -color, subtitle:\"Steam\"}) catch null\n                            if (output.isDict) {site:site->id, err:output}\n                            else {site: site->id, output:output}\n                            end\n@@ -27,7 +30,7 @@                  .xqExecute()\n \n   //handle errors\n-  if (debug) return portfolio\n+  if (debug) return portfolio.table\n   goodSites: portfolio.findAll(r=>r.missing(\"err\") and r.has(\"output\") and r.get(\"output\").meta.has(\"hisStart\"))\n   badSites:  portfolio.findAll(r=>r.has(\"err\") and r.has(\"output\") and r.get(\"output\").meta.missing(\"hisStart\"))\n   totalSites: portfolio.size\n@@ -38,7 +41,7 @@                      .hisJoin\n \n   //output\n-  out: goodData.addMeta({title:\"Energy Usage\", subtitle:\"Gas and Electric - \"+goodSites.size+\" / \"+totalSites+\" sites\"})\n+  out: goodData.addMeta({title:\"Energy Usage\", subtitle:\"Electric, Steam, Hot Water, and Chilled Water - \"+goodSites.size+\" / \"+totalSites+\" sites\"})\n \n-  return out\n+  return out.addMeta({chartLegend:\"hide\"}).hisClip()\n end"
    },
    {
      "name": "wdg_portfolio_chart_savings",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(dates, opts:{}) => do\n  dates = dates.toSpan\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //opts\n  debugVal: opts.get(\"debug\")\n  debug: debugVal!=null and debugVal!=\"Disable\"\n\n  //get arcs and sites\n  sites: xq().xqReadAll(site).xqExecute\n  if (sites.size==0) return blankChart(\"No sites\", \"Please add sites to get started\")\n\n\n  //get energy data\n  portfolio: xq().xqProjs(navProjNames())\n                 .xqDefine(\"dates\",dates)\n                 .xqReadAll(site)\n                 .xqMap(\"\"\"site => do\n                           output: try getUtilityMeterSummary(site->id,dates).keepCols(\"ts,elec_savings,gas_savings\".split(\",\")).hisRollupAuto\n                                   catch (err) err\n                           try output=output.addColMeta(\"elec_savings\", {chartGroup:\"Elec\", dis:site.dis+\" (Elec)\", -color, subtitle:\"Electricity\"}) catch null\n                           try output=output.addColMeta(\"gas_savings\",  {chartGroup:\"Gas\",  dis:site.dis+\" (Gas)\", -color, subtitle:\"Gas\"}) catch null\n                           if (output.isDict) {site:site->id, err:output}\n                           else {site: site->id, output:output}\n                           end\n                           \"\"\")\n                 .xqExecute()\n\n  //handle errors\n  if (debug) return portfolio\n  goodSites: portfolio.findAll(r=>r.missing(\"err\") and r.has(\"output\") and r.get(\"output\").meta.has(\"hisStart\"))\n  badSites:  portfolio.findAll(r=>r.has(\"err\") and r.has(\"output\") and r.get(\"output\").meta.missing(\"hisStart\"))\n  totalSites: portfolio.size\n  if (goodSites.size==0) return blankChart(\"Cannot calculated cumulative savings.\",\"Please check and ensure the correct points are set up\")\n\n  //collect data\n  goodData: goodSites.colToList(\"output\")\n                     .hisJoin\n\n  //output\n  out: goodData.addMeta({title:\"Portfolio Savings By Site - \"+goodSites.size+\" / \"+totalSites+\" sites\"})\n\n  return out\nend",
      "local_src": "(dates, opts:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //opts\n  debugVal: opts.get(\"debug\")\n  debug: debugVal!=null and debugVal!=\"Disable\"\n\n  //get arcs and sites\n  sites: xq().xqTimeout(5min).xqReadAll(site).xqExecute\n  if (sites.size==0) return blankChart(\"No sites\", \"Please add sites to get started\")\n\n\n  //get energy data\n  portfolio: xq().xqTimeout(5min)\n                 .xqProjs(navProjNames())\n                 .xqDefine(\"dates\",dates)\n                 .xqReadAll(site)\n                 .xqMap(\"\"\"site => do\n                           output: try getUtilityMeterSummary(site->id,dates).keepCols(\"ts,elec_savings,steam_savings,hw_savings,chw_savings\".split(\",\")).hisRollupAuto\n                                   catch (err) err\n                           try output=output.addColMeta(\"elec_savings\", {chartGroup:\"Elec\", dis:site.dis+\" (Elec)\", -color, subtitle:\"Electricity\"}) catch null\n                           try output=output.addColMeta(\"steam_savings\",  {chartGroup:\"Steam\",  dis:site.dis+\" (Steam)\", -color, subtitle:\"Steam\"}) catch null\n                           try output=output.addColMeta(\"hw_savings\",  {chartGroup:\"HW\",  dis:site.dis+\" (HW)\", -color, subtitle:\"Hot Water\"}) catch null\n                           try output=output.addColMeta(\"chw_savings\",  {chartGroup:\"CHW\",  dis:site.dis+\" (CHW)\", -color, subtitle:\"Chilled Water\"}) catch null\n                           if (output.isDict) {site:site->id, err:output}\n                           else {site: site->id, output:output}\n                           end\n                           \"\"\")\n                 .xqExecute()\n\n  //handle errors\n  if (debug) return portfolio\n  goodSites: portfolio.findAll(r=>r.missing(\"err\") and r.has(\"output\") and r.get(\"output\").meta.has(\"hisStart\"))\n  badSites:  portfolio.findAll(r=>r.has(\"err\") and r.has(\"output\") and r.get(\"output\").meta.missing(\"hisStart\"))\n  totalSites: portfolio.size\n  if (goodSites.size==0) return blankChart(\"Cannot calculated cumulative savings.\",\"Please check and ensure the correct points are set up\")\n\n  //collect data\n  goodData: goodSites.colToList(\"output\")\n                     .hisJoin\n\n  //output\n  out: goodData.addMeta({title:\"Portfolio Savings By Site - \"+goodSites.size+\" / \"+totalSites+\" sites\"})\n\n  return out\nend",
      "diff": "@@ -1,5 +1,5 @@ (dates, opts:{}) => do\n-  dates = dates.toSpan\n+\n   //validate and authorize current user\n   kwLinkValidate(\"analytics\")\n \n@@ -8,19 +8,22 @@   debug: debugVal!=null and debugVal!=\"Disable\"\n \n   //get arcs and sites\n-  sites: xq().xqReadAll(site).xqExecute\n+  sites: xq().xqTimeout(5min).xqReadAll(site).xqExecute\n   if (sites.size==0) return blankChart(\"No sites\", \"Please add sites to get started\")\n \n \n   //get energy data\n-  portfolio: xq().xqProjs(navProjNames())\n+  portfolio: xq().xqTimeout(5min)\n+                 .xqProjs(navProjNames())\n                  .xqDefine(\"dates\",dates)\n                  .xqReadAll(site)\n                  .xqMap(\"\"\"site => do\n-                           output: try getUtilityMeterSummary(site->id,dates).keepCols(\"ts,elec_savings,gas_savings\".split(\",\")).hisRollupAuto\n+                           output: try getUtilityMeterSummary(site->id,dates).keepCols(\"ts,elec_savings,steam_savings,hw_savings,chw_savings\".split(\",\")).hisRollupAuto\n                                    catch (err) err\n                            try output=output.addColMeta(\"elec_savings\", {chartGroup:\"Elec\", dis:site.dis+\" (Elec)\", -color, subtitle:\"Electricity\"}) catch null\n-                           try output=output.addColMeta(\"gas_savings\",  {chartGroup:\"Gas\",  dis:site.dis+\" (Gas)\", -color, subtitle:\"Gas\"}) catch null\n+                           try output=output.addColMeta(\"steam_savings\",  {chartGroup:\"Steam\",  dis:site.dis+\" (Steam)\", -color, subtitle:\"Steam\"}) catch null\n+                           try output=output.addColMeta(\"hw_savings\",  {chartGroup:\"HW\",  dis:site.dis+\" (HW)\", -color, subtitle:\"Hot Water\"}) catch null\n+                           try output=output.addColMeta(\"chw_savings\",  {chartGroup:\"CHW\",  dis:site.dis+\" (CHW)\", -color, subtitle:\"Chilled Water\"}) catch null\n                            if (output.isDict) {site:site->id, err:output}\n                            else {site: site->id, output:output}\n                            end\n"
    },
    {
      "name": "wdg_siteMeterManager_missingPoints",
      "pod": "kwLinkVirtualExt",
      "pod_src": "/*\n*    Description :\n*     This function is a widget that shows the missing points in the siteMeter (if any).\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    siteMeter       - The siteMeter selected to generate points.\n*\n*   Work :\n*     Who                    When            Change\n*     Thomas Kuhrke Limia    1/10/2025       - Initial Creation\n*/\n(siteMeter) => do\n  smRec: siteMeter.toRec\n  smId: smRec.get(\"id\")\n  out: siteMeter_retrieveRequiredVirtualPoints(smId, {pointType: \"missing\"})\n  out = out.map(r => return r.set(\"mixyRefs\", smRec.get(\"siteRef\").toStr+\"|\"+smId.toStr+\"|\"+r.get(\"id\")))\n  if (out.isEmpty) out = {info: \"No Missing Points found.\"}.toGrid.addColMeta(\"info\", {dis: \"Information\"})\n  return out.addMeta({title: \"\u274c Missing Points \u274c\"}).addColMeta(\"mixyRefs\", {hide, hidden})\nend",
      "local_src": "/*    \n*    Description :\n*     This function is a widget that shows the missing points in the siteMeter (if any).\n*\n*   Parameters :\n*     Type                   Name            Description\n*     Ref                    siteMeter       - The siteMeter selected to generate points.\n*\n*   Work :\n*     Who                    When            Change \n*     Thomas Kuhrke Limia    1/10/2025       - Initial Creation\n*/\n(siteMeter) => do\n  smRec: siteMeter.toRec\n  smId: smRec.get(\"id\")\n  out: siteMeter_retrieveRequiredVirtualPoints(smId, {pointType: \"missing\"})\n  out = out.map(r => return r.set(\"mixyRefs\", smRec.get(\"siteRef\").toStr+\"|\"+smId.toStr+\"|\"+r.get(\"id\")))\n  if (out.isEmpty) out = {info: \"No Missing Points found.\"}.toGrid.addColMeta(\"info\", {dis: \"Information\"})\n  return out.addMeta({title: \"\u274c Missing Points \u274c\"})\nend",
      "diff": "@@ -16,5 +16,5 @@   out: siteMeter_retrieveRequiredVirtualPoints(smId, {pointType: \"missing\"})\n   out = out.map(r => return r.set(\"mixyRefs\", smRec.get(\"siteRef\").toStr+\"|\"+smId.toStr+\"|\"+r.get(\"id\")))\n   if (out.isEmpty) out = {info: \"No Missing Points found.\"}.toGrid.addColMeta(\"info\", {dis: \"Information\"})\n-  return out.addMeta({title: \"\u274c Missing Points \u274c\"}).addColMeta(\"mixyRefs\", {hide, hidden})\n+  return out.addMeta({title: \"\u274c Missing Points \u274c\"})\n end"
    },
    {
      "name": "wdg_site_card_costSavings",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(site, dates, opts:{}, debug:false) => do\n  dates = dates.toSpan\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n\n  //opts\n\n  //get site kickoff date, when savings started\n  savingsDate: getSiteKickoffDate(site)\n  //if no savings date, just take the first day of this year\n  if (savingsDate==null) savingsDate = (dates.end - 1day).year.toSpan.start.date\n\n  //get utility meter summary\n  data: try getUtilityMeterSummary(site, dates).meta catch (err) return {title:\"Error! Action Required\", error:(err.get(\"dis\")), action:err.get(\"action\")}\n  elecMeta: data.get(\"elecMeta\")\n  gasMeta:  data.get(\"gasMeta\")\n\n  //get utility rates\n  elecRate: if (siteRec.has(\"elecRate\")) siteRec.get(\"elecRate\")\n            else if (elecMeta.get(\"effectiveRollup\").get(\"elec_rate\").isNumber) elecMeta.get(\"effectiveRollup\").get(\"elec_rate\")\n            else null\n  gasRate:  if (siteRec.has(\"gasRate\")) siteRec.get(\"gasRate\")\n            else if (gasMeta.get(\"effectiveRollup\").get(\"gas_rate\").isNumber) gasMeta.get(\"effectiveRollup\").get(\"gas_rate\")\n            else null\n\n  //get additional savings\n  year: (dates.end - 1day).year.toSpan\n  arcs: taskRun(xq().xqReadAll(arc and projectTracker).xqExecute()).futureGet\n        .filter(projectSite==siteId or siteRef==siteId)\n\n  additionalSavings: getArcAdditionalSavings(year, arcs)\n\n  //put data together for card\n  elecCost: data.get(\"elecMeta\").get(\"effectiveRollup\").get(\"elec_savingsCost\")\n  gasCost:  data.get(\"gasMeta\").get(\"effectiveRollup\").get(\"gas_savingsCost\")\n  savingsExtendedLife:       additionalSavings.get(\"savingsExtendedLife\")\n  savingsAvoidedOutsourcing: additionalSavings.get(\"savingsAvoidedOutsourcing\")\n  savingsIncentives:         additionalSavings.get(\"savingsIncentives\")\n  totalCost: [elecCost, gasCost, savingsExtendedLife, savingsAvoidedOutsourcing, savingsIncentives].findAll(v=>v.isNumber and v>0).fold(sum)\n  if (not totalCost.isNumber) totalCost = 0.as(\"\\$\")\n\n  //round numbers\n  if (elecCost.isNumber) elecCost = elecCost.round\n    else \"missing\"\n  if (gasCost.isNumber) gasCost = gasCost.round\n    else \"missing\"\n  if (savingsExtendedLife.isNumber) savingsExtendedLife = savingsExtendedLife.round\n  if (savingsAvoidedOutsourcing.isNumber) savingsAvoidedOutsourcing = savingsAvoidedOutsourcing.round\n  if (savingsIncentives.isNumber) savingsIncentives = savingsIncentives.round\n  if (totalCost.isNumber) totalCost = totalCost.round\n\n  //put data into card and convert to grid\n  card :  {title:\"Cost Savings\",\n           subtitle        :    savingsDate.format(\"MMM DD YYYY\").toStr+\" to date\",\n           totalCost       :    totalCost,\n           elecCost        :    elecCost,\n           gasCost         :    gasCost,\n           savingsExtendedLife:       savingsExtendedLife,\n           savingsAvoidedOutsourcing: savingsAvoidedOutsourcing,\n           incentives:                savingsIncentives\n           }\n  out: card.toGrid\n           .addMeta({vizByUnit})\n           .reorderKeepCols([\"totalCost\",\"elecCost\",\"gasCost\"])\n           .addColMetaClean(\"totalCost\",{dis:\"Total Cost Savings\", viz:\"barCell\", color:\"gray\"})\n           .addColMetaClean(\"elecCost\", {dis:\"Elec Cost Savings\", viz:\"barCell\", color:\"orange\"})\n           .addColMetaClean(\"gasCost\",  {dis:\"Gas Cost Savings\", viz:\"barCell\", color:\"purple\"})\n           .addColMetaClean(\"savingsExtendedLife\",  {dis:\"Extended Life\", viz:\"barCell\", color:\"green\"})\n           .addColMetaClean(\"savingsAvoidedOutsourcing\",  {dis:\"Avoided Outsourcing\", viz:\"barCell\", color:\"blue\"})\n           .addColMetaClean(\"incentives\",  {dis:\"Incentives Paid\", viz:\"barCell\", color:\"pink\"})\n\n  //check if anything is not a number and alter the colMeta so it shows up\n  if (not elecCost.isNumber) out = out.addColMetaClean(\"elecCost\",{-viz})\n  if (not gasCost.isNumber) out = out.addColMetaClean(\"gasCost\",{-viz})\n\n  return out\n\nend",
      "local_src": "(site, dates, opts:{}, debug:false) => do\n \n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n \n  //normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n \n  //opts\n \n  //get site kickoff date, when savings started\n  savingsDate: null//getSiteKickoffDate(site)\n  //if no savings date, just take the first day of this year\n  if (savingsDate==null) savingsDate = (dates.start.date)\n \n  //get utility meter summary\n  data: try getUtilityMeterSummary(site, dates).meta catch (err) return {title:\"Error! Action Required\", error:(err.get(\"dis\")), action:err.get(\"action\")}\n//  elecMeta: data.get(\"elecMeta\")\n//  gasMeta:  data.get(\"gasMeta\")\n \n  //get utility rates\n \n  //get additional savings\n  year: (dates.end - 1day).year.toSpan\n\n  //return data\n  //additionalSavings: getArcAdditionalSavings(year, arcs)\n \n  //put data together for card\n  elecCost: try data.get(\"elecMeta\").get(\"effectiveRollup\").get(\"elec_savingsCost\") catch null\n//  hwCost: data.meta.get(\"effectiveRollup\").get(\"hw_savingsCost\")\n  chwCost: try data.get(\"chwMeta\").get(\"effectiveRollup\").get(\"chw_savingsCost\") catch null\n  steamCost: try data.get(\"steamMeta\").get(\"effectiveRollup\").get(\"steam_savingsCost\") catch null\n  //gasCost:  data.get(\"gasMeta\").get(\"effectiveRollup\").get(\"gas_savingsCost\")\n  //savingsExtendedLife:       additionalSavings.get(\"savingsExtendedLife\")\n  //savingsAvoidedOutsourcing: additionalSavings.get(\"savingsAvoidedOutsourcing\")\n  //savingsIncentives:         additionalSavings.get(\"savingsIncentives\")\n  totalCost: [elecCost, chwCost, steamCost].findAll(v=>v.isNumber).fold(sum)\n  if (not totalCost.isNumber) totalCost = 0.as(\"\\$\")\n \n  //round numbers\n  if (elecCost.isNumber) elecCost = elecCost.round\n    else \"missing\"\n  if (chwCost.isNumber) chwCost = chwCost.round\n    else \"missing\"\n  if (steamCost.isNumber) steamCost = steamCost.round\n    else \"missing\"\n\n  if (totalCost.isNumber) totalCost = totalCost.round\n \n  //put data into card and convert to grid\n  card :  {title:\"Cost Savings\",\n           subtitle        :    savingsDate.format(\"MMM DD YYYY\").toStr+\" to date\",\n           totalCost       :    totalCost,\n           elecCost        :    elecCost,\n           chwCost         :    chwCost,\n           steamCost       :    steamCost,\n           }\n  out: card.toGrid\n           .addMeta({vizByUnit})\n           .reorderKeepCols([\"totalCost\",\"elecCost\",\"chwCost\",\"steamCost\"])\n           .addColMetaClean(\"totalCost\",{dis:\"Total Cost Savings\", viz:\"barCell\", color:\"gray\", format:\"U#,###.\"})\n           .addColMetaClean(\"elecCost\", {dis:\"Elec Cost Savings\", viz:\"barCell\", color:\"orange\", format:\"U#,###.\"})\n           .addColMetaClean(\"chwCost\", {dis:\"Chilled Water Cost Savings\", viz:\"barCell\", color:\"skyBlue\", format:\"U#,###.\"})\n           .addColMetaClean(\"steamCost\", {dis:\"Steam Cost Savings\", viz:\"barCell\", color:\"slateBlue\", format:\"U#,###.\"})\n \n  //check if anything is not a number and alter the colMeta so it shows up\n  if (not elecCost.isNumber) out = out.addColMetaClean(\"elecCost\",{-viz})\n  if (not chwCost.isNumber) out = out.addColMetaClean(\"chwCost\",{-viz})\n  if (not steamCost.isNumber) out = out.addColMetaClean(\"steamCost\",{-viz})\n \n  return out\n \nend",
      "diff": "@@ -1,5 +1,5 @@ (site, dates, opts:{}, debug:false) => do\n-  dates = dates.toSpan\n+\n   //validate and authorize current user\n   kwLinkValidate(\"analytics\")\n \n@@ -10,47 +10,43 @@   //opts\n \n   //get site kickoff date, when savings started\n-  savingsDate: getSiteKickoffDate(site)\n+  savingsDate: null//getSiteKickoffDate(site)\n   //if no savings date, just take the first day of this year\n-  if (savingsDate==null) savingsDate = (dates.end - 1day).year.toSpan.start.date\n+  if (savingsDate==null) savingsDate = (dates.start.date)\n \n   //get utility meter summary\n   data: try getUtilityMeterSummary(site, dates).meta catch (err) return {title:\"Error! Action Required\", error:(err.get(\"dis\")), action:err.get(\"action\")}\n-  elecMeta: data.get(\"elecMeta\")\n-  gasMeta:  data.get(\"gasMeta\")\n+//  elecMeta: data.get(\"elecMeta\")\n+//  gasMeta:  data.get(\"gasMeta\")\n \n   //get utility rates\n-  elecRate: if (siteRec.has(\"elecRate\")) siteRec.get(\"elecRate\")\n-            else if (elecMeta.get(\"effectiveRollup\").get(\"elec_rate\").isNumber) elecMeta.get(\"effectiveRollup\").get(\"elec_rate\")\n-            else null\n-  gasRate:  if (siteRec.has(\"gasRate\")) siteRec.get(\"gasRate\")\n-            else if (gasMeta.get(\"effectiveRollup\").get(\"gas_rate\").isNumber) gasMeta.get(\"effectiveRollup\").get(\"gas_rate\")\n-            else null\n \n   //get additional savings\n   year: (dates.end - 1day).year.toSpan\n-  arcs: taskRun(xq().xqReadAll(arc and projectTracker).xqExecute()).futureGet\n-        .filter(projectSite==siteId or siteRef==siteId)\n \n-  additionalSavings: getArcAdditionalSavings(year, arcs)\n+  //return data\n+  //additionalSavings: getArcAdditionalSavings(year, arcs)\n \n   //put data together for card\n-  elecCost: data.get(\"elecMeta\").get(\"effectiveRollup\").get(\"elec_savingsCost\")\n-  gasCost:  data.get(\"gasMeta\").get(\"effectiveRollup\").get(\"gas_savingsCost\")\n-  savingsExtendedLife:       additionalSavings.get(\"savingsExtendedLife\")\n-  savingsAvoidedOutsourcing: additionalSavings.get(\"savingsAvoidedOutsourcing\")\n-  savingsIncentives:         additionalSavings.get(\"savingsIncentives\")\n-  totalCost: [elecCost, gasCost, savingsExtendedLife, savingsAvoidedOutsourcing, savingsIncentives].findAll(v=>v.isNumber and v>0).fold(sum)\n+  elecCost: try data.get(\"elecMeta\").get(\"effectiveRollup\").get(\"elec_savingsCost\") catch null\n+//  hwCost: data.meta.get(\"effectiveRollup\").get(\"hw_savingsCost\")\n+  chwCost: try data.get(\"chwMeta\").get(\"effectiveRollup\").get(\"chw_savingsCost\") catch null\n+  steamCost: try data.get(\"steamMeta\").get(\"effectiveRollup\").get(\"steam_savingsCost\") catch null\n+  //gasCost:  data.get(\"gasMeta\").get(\"effectiveRollup\").get(\"gas_savingsCost\")\n+  //savingsExtendedLife:       additionalSavings.get(\"savingsExtendedLife\")\n+  //savingsAvoidedOutsourcing: additionalSavings.get(\"savingsAvoidedOutsourcing\")\n+  //savingsIncentives:         additionalSavings.get(\"savingsIncentives\")\n+  totalCost: [elecCost, chwCost, steamCost].findAll(v=>v.isNumber).fold(sum)\n   if (not totalCost.isNumber) totalCost = 0.as(\"\\$\")\n \n   //round numbers\n   if (elecCost.isNumber) elecCost = elecCost.round\n     else \"missing\"\n-  if (gasCost.isNumber) gasCost = gasCost.round\n+  if (chwCost.isNumber) chwCost = chwCost.round\n     else \"missing\"\n-  if (savingsExtendedLife.isNumber) savingsExtendedLife = savingsExtendedLife.round\n-  if (savingsAvoidedOutsourcing.isNumber) savingsAvoidedOutsourcing = savingsAvoidedOutsourcing.round\n-  if (savingsIncentives.isNumber) savingsIncentives = savingsIncentives.round\n+  if (steamCost.isNumber) steamCost = steamCost.round\n+    else \"missing\"\n+\n   if (totalCost.isNumber) totalCost = totalCost.round\n \n   //put data into card and convert to grid\n@@ -58,24 +54,21 @@            subtitle        :    savingsDate.format(\"MMM DD YYYY\").toStr+\" to date\",\n            totalCost       :    totalCost,\n            elecCost        :    elecCost,\n-           gasCost         :    gasCost,\n-           savingsExtendedLife:       savingsExtendedLife,\n-           savingsAvoidedOutsourcing: savingsAvoidedOutsourcing,\n-           incentives:                savingsIncentives\n+           chwCost         :    chwCost,\n+           steamCost       :    steamCost,\n            }\n   out: card.toGrid\n            .addMeta({vizByUnit})\n-           .reorderKeepCols([\"totalCost\",\"elecCost\",\"gasCost\"])\n-           .addColMetaClean(\"totalCost\",{dis:\"Total Cost Savings\", viz:\"barCell\", color:\"gray\"})\n-           .addColMetaClean(\"elecCost\", {dis:\"Elec Cost Savings\", viz:\"barCell\", color:\"orange\"})\n-           .addColMetaClean(\"gasCost\",  {dis:\"Gas Cost Savings\", viz:\"barCell\", color:\"purple\"})\n-           .addColMetaClean(\"savingsExtendedLife\",  {dis:\"Extended Life\", viz:\"barCell\", color:\"green\"})\n-           .addColMetaClean(\"savingsAvoidedOutsourcing\",  {dis:\"Avoided Outsourcing\", viz:\"barCell\", color:\"blue\"})\n-           .addColMetaClean(\"incentives\",  {dis:\"Incentives Paid\", viz:\"barCell\", color:\"pink\"})\n+           .reorderKeepCols([\"totalCost\",\"elecCost\",\"chwCost\",\"steamCost\"])\n+           .addColMetaClean(\"totalCost\",{dis:\"Total Cost Savings\", viz:\"barCell\", color:\"gray\", format:\"U#,###.\"})\n+           .addColMetaClean(\"elecCost\", {dis:\"Elec Cost Savings\", viz:\"barCell\", color:\"orange\", format:\"U#,###.\"})\n+           .addColMetaClean(\"chwCost\", {dis:\"Chilled Water Cost Savings\", viz:\"barCell\", color:\"skyBlue\", format:\"U#,###.\"})\n+           .addColMetaClean(\"steamCost\", {dis:\"Steam Cost Savings\", viz:\"barCell\", color:\"slateBlue\", format:\"U#,###.\"})\n \n   //check if anything is not a number and alter the colMeta so it shows up\n   if (not elecCost.isNumber) out = out.addColMetaClean(\"elecCost\",{-viz})\n-  if (not gasCost.isNumber) out = out.addColMetaClean(\"gasCost\",{-viz})\n+  if (not chwCost.isNumber) out = out.addColMetaClean(\"chwCost\",{-viz})\n+  if (not steamCost.isNumber) out = out.addColMetaClean(\"steamCost\",{-viz})\n \n   return out\n \n"
    },
    {
      "name": "wdg_site_card_energyCosts",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(site, dates, opts:{}) => do\n  dates = dates.toSpan\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n\n  //opts\n\n  //get data\n  data: try getUtilityMeterSummary(site, dates).meta catch (err) return {title:\"Error! Action Required\", error:(err.get(\"dis\")), action:err.get(\"action\")}\n  elecMeta: data.get(\"elecMeta\")\n  gasMeta: data.get(\"gasMeta\")\n\n  //get costs from meta\n  elecCost: try elecMeta.get(\"effectiveRollup\").get(\"elec_cost\") catch \"missing\"\n  gasCost:  try gasMeta.get(\"effectiveRollup\").get(\"gas_cost\") catch \"missing\"\n  totalCost: [elecCost, gasCost].findAll(v=>v.isNumber).fold(sum)\n\n  //get hisEnd from effective points\n  asOfElecDate: elecMeta.get(\"effectivePoints\").get(\"elec_cost\").toRec.get(\"hisEnd\").date\n  if (asOfElecDate > dates.end.date) asOfElecDate = dates.end.date\n  elecDateMsg: try \"(as of \"+asOfElecDate.format(\"MMM D YYYY\") +\")\"\n               catch \"(no data)\"\n  asOfGasDate: gasMeta.get(\"effectivePoints\").get(\"gas_cost\").toRec.get(\"hisEnd\").date\n  if (asOfGasDate > dates.end.date) asOfGasDate = dates.end.date\n  gasDateMsg:  try \"(as of \"+asOfGasDate.format(\"MMM D YYYY\")  +\")\"\n               catch \"(no data)\"\n\n  //round numbers\n  if (elecCost.isNumber) elecCost = elecCost.round.as(1)\n  if (gasCost.isNumber) gasCost = gasCost.round.as(1)\n  if (totalCost.isNumber) totalCost = totalCost.round.as(1)\n  else if (totalCost.isNull) totalCost = 0\n\n  //dates string\n  dateString: if (dates.end > now()) dates.start.date.format(\"MMM YYYY\").toStr + \" to \" + \"date\"\n              else dates.start.date.format(\"MMM YYYY\").toStr + \" to \" + dates.end.date.format(\"MMM-YYYY\").toStr\n\n   card : {title:\"Energy Costs\",\n           subtitle        : dateString,\n           totalCost       :    \"\",\n           totalCostVal     :    totalCost,\n           elecCost        :    elecDateMsg,\n           elecCostVal     :    elecCost,\n           gasCost         :    gasDateMsg,\n           gasCostVal     :    gasCost,\n           }\n\n\n  out: card.toGrid.addMeta({vizByUnit})\n               .addColMeta(\"totalCost\",{dis:\"Total Energy Costs\"})\n               .addColMeta(\"elecCost\",{dis:\"Elec Cost \", color:\"lightGray\"})\n               .addColMeta(\"gasCost\",{dis:\"Gas Cost \", color:\"lightGray\"})\n               .addColMeta(\"totalCostVal\",{dis:\"\", viz:\"barCell\", color:\"gray\", format:\"\\$#,###,###\"})\n               .addColMeta(\"elecCostVal\",{dis:\"\", viz:\"barCell\", color:\"orange\", format:\"\\$#,###,###\"})\n               .addColMeta(\"gasCostVal\",{dis:\"\", viz:\"barCell\", color:\"purple\", format:\"\\$#,###,###\"})\n               .reorderKeepCols([\"totalCost\",\"totalCostVal\",\"elecCost\",\"elecCostVal\",\"gasCost\",\"gasCostVal\"])\nend",
      "local_src": "(site, dates, opts:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n\n  //opts\n\n  //get data\n  data: try getUtilityMeterSummary(site, dates).meta catch (err) return {title:\"Error! Action Required\", error:(err.get(\"dis\")), action:err.get(\"action\")}\n  elecMeta: data.get(\"elecMeta\")\n  steamMeta: data.get(\"steamMeta\")\n  chwMeta: data.get(\"chwMeta\")\n\n\n  //get costs from meta\n  elecCost: try elecMeta.get(\"effectiveRollup\").get(\"elec_cost\") catch \"missing\"\n  steamCost:  try steamMeta.get(\"effectiveRollup\").get(\"steam_cost\") catch \"missing\"\n  chwCost:  try chwMeta.get(\"effectiveRollup\").get(\"chw_cost\") catch \"missing\"\n  totalCost: [elecCost, steamCost, chwCost].findAll(v=>v.isNumber).fold(sum)\n\n  //get hisEnd from effective points\n  asOfElecDate: try elecMeta.get(\"effectivePoints\").get(\"elec_cost\").toRec.get(\"hisEnd\").date catch \"missing\"\n  if (asOfElecDate!=\"missing\" and asOfElecDate > dates.end.date) asOfElecDate = dates.end.date\n  elecDateMsg: try \"(as of \"+asOfElecDate.format(\"MMM D YYYY\") +\")\"\n               catch \"(no data)\"\n  \n  asOfsteamDate: try steamMeta.get(\"effectivePoints\").get(\"steam_cost\").toRec.get(\"hisEnd\").date catch \"missing\"\n  if (asOfsteamDate!=\"missing\" and asOfsteamDate > dates.end.date) asOfsteamDate = dates.end.date\n  steamDateMsg:  try \"(as of \"+asOfsteamDate.format(\"MMM D YYYY\")  +\")\"\n               catch \"(no data)\"\n               \n  asOfchwDate: try chwMeta.get(\"effectivePoints\").get(\"chw_cost\").toRec.get(\"hisEnd\").date catch \"missing\"\n  if (asOfchwDate!=\"missing\" and asOfchwDate > dates.end.date) asOfchwDate = dates.end.date\n  chwDateMsg:  try \"(as of \"+asOfchwDate.format(\"MMM D YYYY\")  +\")\"\n               catch \"(no data)\"\n  //round numbers\n  if (elecCost.isNumber) elecCost = elecCost.round.as(1)\n  if (steamCost.isNumber) steamCost = steamCost.round.as(1)\n  if (chwCost.isNumber) chwCost = chwCost.round.as(1)\n\n  if (totalCost.isNumber) totalCost = totalCost.round.as(1)\n  else if (totalCost.isNull) totalCost = 0\n\n  //dates string\n  dateString: if (dates.end > now()) dates.start.date.format(\"MMM YYYY\").toStr + \" to \" + \"date\"\n              else dates.start.date.format(\"MMM DD YYYY\").toStr + \" to \" + dates.end.date.format(\"MMM-YYYY\").toStr\n\n   card : {title:\"Energy Costs\",\n           subtitle        : dateString,\n           totalCost       : \"\",\n           totalCostVal    : totalCost,\n           elecCost        : elecDateMsg,\n           elecCostVal     : elecCost,\n           steamCost       : steamDateMsg,\n           steamCostVal    : steamCost,\n           chwCost         : chwDateMsg,\n           chwCostVal      : chwCost,\n           }\n\n\n  out: card.toGrid.addMeta({vizByUnit})\n               .addColMetaClean(\"totalCost\",{dis:\"Total Energy Costs\"})\n               .addColMetaClean(\"elecCost\",{dis:\"Elec Cost \", color:\"lightGray\"})\n               .addColMetaClean(\"steamCost\",{dis:\"Steam Cost \", color:\"lightGray\"})\n               .addColMetaClean(\"chwCost\",{dis:\"CHW Cost \", color:\"lightGray\"})\n               .addColMetaClean(\"totalCostVal\",{dis:\"\", viz:\"barCell\", color:\"gray\", format:\"\\$#,###,###\"})\n               .addColMetaClean(\"elecCostVal\",{dis:\"\", viz:\"barCell\", color:\"orange\", format:\"\\$#,###,###\"})\n               .addColMetaClean(\"steamCostVal\",{dis:\"\", viz:\"barCell\", color:\"slateBlue\", format:\"\\$#,###,###\"})\n               .addColMetaClean(\"chwCostVal\",{dis:\"\", viz:\"barCell\", color:\"skyBlue\", format:\"\\$#,###,###\"})\n               .reorderKeepCols([\"totalCost\",\"totalCostVal\",\"elecCost\",\"elecCostVal\",\"steamCost\",\"steamCostVal\",\"chwCost\",\"chwCostVal\"])\nend",
      "diff": "@@ -1,5 +1,5 @@ (site, dates, opts:{}) => do\n-  dates = dates.toSpan\n+\n   //validate and authorize current user\n   kwLinkValidate(\"analytics\")\n \n@@ -12,50 +12,64 @@   //get data\n   data: try getUtilityMeterSummary(site, dates).meta catch (err) return {title:\"Error! Action Required\", error:(err.get(\"dis\")), action:err.get(\"action\")}\n   elecMeta: data.get(\"elecMeta\")\n-  gasMeta: data.get(\"gasMeta\")\n+  steamMeta: data.get(\"steamMeta\")\n+  chwMeta: data.get(\"chwMeta\")\n+\n \n   //get costs from meta\n   elecCost: try elecMeta.get(\"effectiveRollup\").get(\"elec_cost\") catch \"missing\"\n-  gasCost:  try gasMeta.get(\"effectiveRollup\").get(\"gas_cost\") catch \"missing\"\n-  totalCost: [elecCost, gasCost].findAll(v=>v.isNumber).fold(sum)\n+  steamCost:  try steamMeta.get(\"effectiveRollup\").get(\"steam_cost\") catch \"missing\"\n+  chwCost:  try chwMeta.get(\"effectiveRollup\").get(\"chw_cost\") catch \"missing\"\n+  totalCost: [elecCost, steamCost, chwCost].findAll(v=>v.isNumber).fold(sum)\n \n   //get hisEnd from effective points\n-  asOfElecDate: elecMeta.get(\"effectivePoints\").get(\"elec_cost\").toRec.get(\"hisEnd\").date\n-  if (asOfElecDate > dates.end.date) asOfElecDate = dates.end.date\n+  asOfElecDate: try elecMeta.get(\"effectivePoints\").get(\"elec_cost\").toRec.get(\"hisEnd\").date catch \"missing\"\n+  if (asOfElecDate!=\"missing\" and asOfElecDate > dates.end.date) asOfElecDate = dates.end.date\n   elecDateMsg: try \"(as of \"+asOfElecDate.format(\"MMM D YYYY\") +\")\"\n                catch \"(no data)\"\n-  asOfGasDate: gasMeta.get(\"effectivePoints\").get(\"gas_cost\").toRec.get(\"hisEnd\").date\n-  if (asOfGasDate > dates.end.date) asOfGasDate = dates.end.date\n-  gasDateMsg:  try \"(as of \"+asOfGasDate.format(\"MMM D YYYY\")  +\")\"\n+\n+  asOfsteamDate: try steamMeta.get(\"effectivePoints\").get(\"steam_cost\").toRec.get(\"hisEnd\").date catch \"missing\"\n+  if (asOfsteamDate!=\"missing\" and asOfsteamDate > dates.end.date) asOfsteamDate = dates.end.date\n+  steamDateMsg:  try \"(as of \"+asOfsteamDate.format(\"MMM D YYYY\")  +\")\"\n                catch \"(no data)\"\n \n+  asOfchwDate: try chwMeta.get(\"effectivePoints\").get(\"chw_cost\").toRec.get(\"hisEnd\").date catch \"missing\"\n+  if (asOfchwDate!=\"missing\" and asOfchwDate > dates.end.date) asOfchwDate = dates.end.date\n+  chwDateMsg:  try \"(as of \"+asOfchwDate.format(\"MMM D YYYY\")  +\")\"\n+               catch \"(no data)\"\n   //round numbers\n   if (elecCost.isNumber) elecCost = elecCost.round.as(1)\n-  if (gasCost.isNumber) gasCost = gasCost.round.as(1)\n+  if (steamCost.isNumber) steamCost = steamCost.round.as(1)\n+  if (chwCost.isNumber) chwCost = chwCost.round.as(1)\n+\n   if (totalCost.isNumber) totalCost = totalCost.round.as(1)\n   else if (totalCost.isNull) totalCost = 0\n \n   //dates string\n   dateString: if (dates.end > now()) dates.start.date.format(\"MMM YYYY\").toStr + \" to \" + \"date\"\n-              else dates.start.date.format(\"MMM YYYY\").toStr + \" to \" + dates.end.date.format(\"MMM-YYYY\").toStr\n+              else dates.start.date.format(\"MMM DD YYYY\").toStr + \" to \" + dates.end.date.format(\"MMM-YYYY\").toStr\n \n    card : {title:\"Energy Costs\",\n            subtitle        : dateString,\n-           totalCost       :    \"\",\n-           totalCostVal     :    totalCost,\n-           elecCost        :    elecDateMsg,\n-           elecCostVal     :    elecCost,\n-           gasCost         :    gasDateMsg,\n-           gasCostVal     :    gasCost,\n+           totalCost       : \"\",\n+           totalCostVal    : totalCost,\n+           elecCost        : elecDateMsg,\n+           elecCostVal     : elecCost,\n+           steamCost       : steamDateMsg,\n+           steamCostVal    : steamCost,\n+           chwCost         : chwDateMsg,\n+           chwCostVal      : chwCost,\n            }\n \n \n   out: card.toGrid.addMeta({vizByUnit})\n-               .addColMeta(\"totalCost\",{dis:\"Total Energy Costs\"})\n-               .addColMeta(\"elecCost\",{dis:\"Elec Cost \", color:\"lightGray\"})\n-               .addColMeta(\"gasCost\",{dis:\"Gas Cost \", color:\"lightGray\"})\n-               .addColMeta(\"totalCostVal\",{dis:\"\", viz:\"barCell\", color:\"gray\", format:\"\\$#,###,###\"})\n-               .addColMeta(\"elecCostVal\",{dis:\"\", viz:\"barCell\", color:\"orange\", format:\"\\$#,###,###\"})\n-               .addColMeta(\"gasCostVal\",{dis:\"\", viz:\"barCell\", color:\"purple\", format:\"\\$#,###,###\"})\n-               .reorderKeepCols([\"totalCost\",\"totalCostVal\",\"elecCost\",\"elecCostVal\",\"gasCost\",\"gasCostVal\"])\n+               .addColMetaClean(\"totalCost\",{dis:\"Total Energy Costs\"})\n+               .addColMetaClean(\"elecCost\",{dis:\"Elec Cost \", color:\"lightGray\"})\n+               .addColMetaClean(\"steamCost\",{dis:\"Steam Cost \", color:\"lightGray\"})\n+               .addColMetaClean(\"chwCost\",{dis:\"CHW Cost \", color:\"lightGray\"})\n+               .addColMetaClean(\"totalCostVal\",{dis:\"\", viz:\"barCell\", color:\"gray\", format:\"\\$#,###,###\"})\n+               .addColMetaClean(\"elecCostVal\",{dis:\"\", viz:\"barCell\", color:\"orange\", format:\"\\$#,###,###\"})\n+               .addColMetaClean(\"steamCostVal\",{dis:\"\", viz:\"barCell\", color:\"slateBlue\", format:\"\\$#,###,###\"})\n+               .addColMetaClean(\"chwCostVal\",{dis:\"\", viz:\"barCell\", color:\"skyBlue\", format:\"\\$#,###,###\"})\n+               .reorderKeepCols([\"totalCost\",\"totalCostVal\",\"elecCost\",\"elecCostVal\",\"steamCost\",\"steamCostVal\",\"chwCost\",\"chwCostVal\"])\n end"
    },
    {
      "name": "wdg_site_chart_baselineEnergy",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(site, dates) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  siteDis: try site.toRec.dis catch null\n  dates = dates.toSpan\n\n  //opts\n  rollup: null //1hr\n  rollupFunc: avg\n  chartType: \"line\"\n  strokeDasharray:\"1,1\"\n\n  //get kickoff date for site\n  kickoffDate: getSiteKickoffDate(site)\n  dt: dateTime(kickoffDate, 00:00:00, site.toRec.get(\"tz\"))\n\n  //get data\n  try grid: getUtilityMeterSummary(site, dates)\n  catch (err) return blankChart(\"Error: \"+err.get(\"dis\"), err.get(\"action\"))\n  colNames: grid.colNames.findAll(r => r.contains(\"_usage\") or r.contains(\"_model\")).add(\"ts\")\n  grid=grid.keepCols(colNames)\n\n  if (grid==null) return blankChart(\"Energy Compared To Baseline\", \"No data found for \"+siteDis)\n\n  out: grid.toGrid.addMeta({title:\"Energy Compared To Baseline\", subtitle:siteDis})\n           .hisRollupAuto(rollup, his=>rollupFunc)\n\n  if (out.meta.get(\"hisEnd\") > now()) out=out.addMeta({hisEnd:now()})\n\n  out= out.addColMetaClean(\"elec_usage\", {chartType:chartType})\n          .addColMetaClean(\"elec_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n          .addColMetaClean(\"gas_usage\", {chartType:chartType})\n          .addColMetaClean(\"gas_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n          .addColMetaClean(\"hw_usage\", {chartType:chartType})\n          .addColMetaClean(\"hw_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n          .addColMetaClean(\"chw_usage\", {chartType:chartType})\n          .addColMetaClean(\"chw_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n          .addColMetaClean(\"steam_usage\", {chartType:chartType})\n          .addColMetaClean(\"steam_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n\n\n  return out\nend",
      "local_src": "(site, dates) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n  hisEndVal : if (dates.end > now()) now() else dates.end\n  dates = (dates.start..hisEndVal).toSpan\n\n  \n\n  siteDis: try site.toRec.dis catch null\n  //opts\n  rollup: null //1hr\n  rollupFunc: avg\n  chartType: \"line\"\n  strokeDasharray:\"8,5\"\n\n  //get kickoff date for site\n  kickoffDate: getSiteKickoffDate(site)\n  dt: dateTime(kickoffDate, 00:00:00, site.toRec.get(\"tz\"))\n\n  //get data\n  try grid: getUtilityMeterSummary(site, dates)\n  catch (err) return blankChart(\"Error: \"+err.get(\"dis\"), err.get(\"action\"))\n\n  grid=grid.keepCols([\"ts\",\"elec_usage\",\"elec_model\", \"steam_usage\",\"steam_model\", \"chw_usage\",\"chw_model\", \"hw_usage\", \"hw_model\"])\n  //grid = grid.addMeta({hisEnd:hisEndVal})\n  if (grid==null) return blankChart(\"Energy Compared To Baseline\", \"No data found for \"+siteDis)\n\n  out: grid.toGrid.addMeta({title:\"Energy Compared To Baseline\", subtitle:siteDis})\n           .hisRollupAuto(rollup, his=>rollupFunc)\n\n  //if (out.meta.get(\"hisEnd\") > now()) out=out.addMeta({hisEnd:now()})\n\n  out= out.addColMetaClean(\"elec_usage\", {chartType:chartType, color: \"orange\"})\n          .addColMetaClean(\"elec_model\", {chartType:chartType, strokeDasharray:strokeDasharray, color: \"orange\"})\n          .addColMetaClean(\"steam_usage\", {chartType:chartType, color: \"slateBlue\"})\n          .addColMetaClean(\"steam_model\", {chartType:chartType, strokeDasharray:strokeDasharray, color: \"slateBlue\"})\n          .addColMetaClean(\"chw_usage\", {chartType:chartType, color: \"skyBlue\"})\n          .addColMetaClean(\"chw_model\", {chartType:chartType, strokeDasharray:strokeDasharray, color: \"skyBlue\"})\n          .addColMetaClean(\"hw_usage\", {chartType:chartType})\n          .addColMetaClean(\"hw_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n          \n  //g: wdg_site_chart_cumulativeSavings(site, dates)//.addMeta({chartGroup: \"savings\"})\n  //outFinal : hisJoin([out, g])\n  \n  return out//.addMeta({hisEnd:hisEndVal})\n  \n  \nend",
      "diff": "@@ -2,15 +2,17 @@ \n   //validate and authorize current user\n   kwLinkValidate(\"analytics\")\n+  hisEndVal : if (dates.end > now()) now() else dates.end\n+  dates = (dates.start..hisEndVal).toSpan\n+\n+\n \n   siteDis: try site.toRec.dis catch null\n-  dates = dates.toSpan\n-\n   //opts\n   rollup: null //1hr\n   rollupFunc: avg\n   chartType: \"line\"\n-  strokeDasharray:\"1,1\"\n+  strokeDasharray:\"8,5\"\n \n   //get kickoff date for site\n   kickoffDate: getSiteKickoffDate(site)\n@@ -19,27 +21,29 @@   //get data\n   try grid: getUtilityMeterSummary(site, dates)\n   catch (err) return blankChart(\"Error: \"+err.get(\"dis\"), err.get(\"action\"))\n-  colNames: grid.colNames.findAll(r => r.contains(\"_usage\") or r.contains(\"_model\")).add(\"ts\")\n-  grid=grid.keepCols(colNames)\n \n+  grid=grid.keepCols([\"ts\",\"elec_usage\",\"elec_model\", \"steam_usage\",\"steam_model\", \"chw_usage\",\"chw_model\", \"hw_usage\", \"hw_model\"])\n+  //grid = grid.addMeta({hisEnd:hisEndVal})\n   if (grid==null) return blankChart(\"Energy Compared To Baseline\", \"No data found for \"+siteDis)\n \n   out: grid.toGrid.addMeta({title:\"Energy Compared To Baseline\", subtitle:siteDis})\n            .hisRollupAuto(rollup, his=>rollupFunc)\n \n-  if (out.meta.get(\"hisEnd\") > now()) out=out.addMeta({hisEnd:now()})\n+  //if (out.meta.get(\"hisEnd\") > now()) out=out.addMeta({hisEnd:now()})\n \n-  out= out.addColMetaClean(\"elec_usage\", {chartType:chartType})\n-          .addColMetaClean(\"elec_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n-          .addColMetaClean(\"gas_usage\", {chartType:chartType})\n-          .addColMetaClean(\"gas_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n+  out= out.addColMetaClean(\"elec_usage\", {chartType:chartType, color: \"orange\"})\n+          .addColMetaClean(\"elec_model\", {chartType:chartType, strokeDasharray:strokeDasharray, color: \"orange\"})\n+          .addColMetaClean(\"steam_usage\", {chartType:chartType, color: \"slateBlue\"})\n+          .addColMetaClean(\"steam_model\", {chartType:chartType, strokeDasharray:strokeDasharray, color: \"slateBlue\"})\n+          .addColMetaClean(\"chw_usage\", {chartType:chartType, color: \"skyBlue\"})\n+          .addColMetaClean(\"chw_model\", {chartType:chartType, strokeDasharray:strokeDasharray, color: \"skyBlue\"})\n           .addColMetaClean(\"hw_usage\", {chartType:chartType})\n           .addColMetaClean(\"hw_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n-          .addColMetaClean(\"chw_usage\", {chartType:chartType})\n-          .addColMetaClean(\"chw_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n-          .addColMetaClean(\"steam_usage\", {chartType:chartType})\n-          .addColMetaClean(\"steam_model\", {chartType:chartType, strokeDasharray:strokeDasharray})\n+\n+  //g: wdg_site_chart_cumulativeSavings(site, dates)//.addMeta({chartGroup: \"savings\"})\n+  //outFinal : hisJoin([out, g])\n+\n+  return out//.addMeta({hisEnd:hisEndVal})\n \n \n-  return out\n end"
    },
    {
      "name": "wdg_site_chart_cumulativeSavings",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "// 6/3/24 JG fix bug on line 37, convert dates to span\n\n(site, dates) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n  dates = dates.toSpan\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n\n  //opts\n  showKickoffDate: true\n\n  //get kickoff date\n  savingsDate: getSiteKickoffDate(siteId)\n\n  //get data\n  data: try getUtilityMeterSummary(site, dates) catch (err) return blankChart(\"Error: \"+err.get(\"dis\"), err.get(\"action\"))\n\n  //keep savings for gas/elec\n  savings: data.keepCols([\"ts\",\"elec_savingsCost\",\"gas_savingsCost\"])\n               .hisRollup(sum, 1day)\n\n  //if both savings columns are empty, return blank grid\n  if (savings.isColBlank(\"gas_savingsCost\") and savings.isColBlank(\"elec_savingsCost\"))\n    return blankChart(\"Cost savings data is unavailable\", \"Check cost savings points\", dates)\n\n  //add cumulative sum columns\n  out:   savings.addMeta({title:\"Cumulative Cost Savings\", subtitle:readById(site).dis})\n            .findAll(r => r->ts.date >= savingsDate)\n            .addColCuSum(\"gasSum\",\"gas_savingsCost\")\n            .addColCuSum(\"elecSum\",\"elec_savingsCost\")\n            .keepCols([\"ts\",\"gasSum\",\"elecSum\"])\n\n  if (out.isColBlank(\"gasSum\") and out.isColBlank(\"elecSum\")) return blankChart(\"Cost savings data is unavailable\", \"No data after kickoff date\", dates)\n\n  try out = out.stackedAreaChart() catch out\n  out=out.addColMetaClean(\"gasSum\",{chartGroup:\"main\", color:\"purple\", dis:\"Gas Savings\"})\n         .addColMetaClean(\"elecSum\",{chartGroup:\"main\", color:\"orange\", dis:\"Electricity Savings\", title:\"Cumulative Cost Savings - \"+site.toRec.dis})\n\n  //fix hisEnd so chart axes are correct\n  if (dates.end > now()) out=out.addMeta({hisEnd:now()})\n\n  //add kickoff date line\n  if (showKickoffDate) do\n    kickoffData: [{ts: dateTime(savingsDate, 0:00:00), kickoff:0}, {ts: dateTime(savingsDate, 0:00:01), kickoff:1}].toGrid\n    out = out.addRows(kickoffData)\n             .addColMetaClean(\"kickoff\",{chartGroup:\"main\", dis:\"Kickoff Date\", strokeDasharray:\"2,4\", color:\"black\", format:\"#\"})\n    end\n\n  return out\nend",
      "local_src": "// 6/3/24 JG fix bug on line 37, convert dates to span\n\n(site, dates, opts:{showStackedAreaChart:false}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n  dates = dates//.toSpan\n\n  //normalize inputs\n  siteRec: site.toRec\n  siteId: siteRec->id\n\n  //opts\n  showKickoffDate: false\n  showStackedAreaChart: opts.get(\"showStackedAreaChart\")\n\n  //get kickoff date \n  savingsDate: getSiteKickoffDate(siteId)\n\n  //get data\n  data: try getUtilityMeterSummary(site, dates) catch (err) return blankChart(\"Error: \"+err.get(\"dis\"), err.get(\"action\"))\n\n  //keep savings for gas/elec\n  savings: data.keepCols([\"ts\",\"elec_savingsCost\",\"hw_savingsCost\",\"chw_savingsCost\",\"steam_savingsCost\"])\n               .hisRollup(sum, 1day)\n\n  //if both savings columns are empty, return blank grid\n  if (savings.isColBlank(\"hw_savingsCost\") and savings.isColBlank(\"elec_savingsCost\") and savings.isColBlank(\"chw_savingsCost\") and savings.isColBlank(\"steam_savingsCost\"))\n    return blankChart(\"Cost savings data is unavailable\", \"Check cost savings points\", dates)\n  hisEndVal : if (dates.end > now()) now() else savings.meta.get(\"hisEnd\")\n  //add cumulative sum columns\n  out:   savings.addMeta({title:\"Cumulative Cost Savings\", subtitle:readById(site).dis, hisEnd:hisEndVal})\n            //.findAll(r => r->ts.date >= savingsDate)\n            .addColCuSum(\"steamSum\",\"steam_savingsCost\")\n            .addColCuSum(\"chwSum\",\"chw_savingsCost\")\n            //.addColCuSum(\"hwSum\",\"hw_savingsCost\")\n            .addColCuSum(\"elecSum\",\"elec_savingsCost\")\n            .keepCols([\"ts\",\"chwSum\",\"elecSum\",\"steamSum\"])\n  if (out.isColBlank(\"steamSum\") and out.isColBlank(\"elecSum\") and out.isColBlank(\"chwSum\") and out.isColBlank(\"hwSum\")) return blankChart(\"Cost savings data is unavailable\", \"No data after kickoff date\", dates)\n\n  if(showStackedAreaChart) try out = out.stackedAreaChart() catch out\n  out=out.addColMetaClean(\"steamSum\",{chartGroup:\"savings\", color:\"slateBlue\", dis:\"Steam Savings\"})\n         .addColMetaClean(\"elecSum\",{chartGroup:\"savings\", color:\"orange\", dis:\"Electricity Savings\"})\n         .addColMetaClean(\"chwSum\",{chartGroup:\"savings\", color:\"skyBlue\", dis:\"Chilled Water Savings\"})\n         //.addColMetaClean(\"hwSum\",{chartGroup:\"main\", color:\"orange\", dis:\"Hot Water Savings\", title:\"Cumulative Cost Savings - \"+site.toRec.dis})\n\n  //fix hisEnd so chart axes are correct\n  // ERRORING IN 3.1.11 \n  //if (dates.toDateSpan.end > now()) out=out.addMeta({hisEnd:out.last.get(\"ts\")})\n\n  //add kickoff date line\n  if (showKickoffDate) do\n    kickoffData: [{ts: dateTime(savingsDate, 0:00:00), kickoff:0}, {ts: dateTime(savingsDate, 0:00:01), kickoff:1}].toGrid\n    out = out.addRows(kickoffData)\n             .addColMetaClean(\"kickoff\",{chartGroup:\"main\", dis:\"Kickoff Date\", strokeDasharray:\"2,4\", color:\"black\", format:\"#\"})\n    end\n\n  return out.addMeta({chartNoScroll})//hisJoin([out, wdg_site_chart_baselineEnergy(site, dates)]).addMeta({chartLegendNoSort})\nend",
      "diff": "@@ -1,17 +1,18 @@ // 6/3/24 JG fix bug on line 37, convert dates to span\n \n-(site, dates) => do\n+(site, dates, opts:{showStackedAreaChart:false}) => do\n \n   //validate and authorize current user\n   kwLinkValidate(\"analytics\")\n-  dates = dates.toSpan\n+  dates = dates//.toSpan\n \n   //normalize inputs\n   siteRec: site.toRec\n   siteId: siteRec->id\n \n   //opts\n-  showKickoffDate: true\n+  showKickoffDate: false\n+  showStackedAreaChart: opts.get(\"showStackedAreaChart\")\n \n   //get kickoff date\n   savingsDate: getSiteKickoffDate(siteId)\n@@ -20,28 +21,32 @@   data: try getUtilityMeterSummary(site, dates) catch (err) return blankChart(\"Error: \"+err.get(\"dis\"), err.get(\"action\"))\n \n   //keep savings for gas/elec\n-  savings: data.keepCols([\"ts\",\"elec_savingsCost\",\"gas_savingsCost\"])\n+  savings: data.keepCols([\"ts\",\"elec_savingsCost\",\"hw_savingsCost\",\"chw_savingsCost\",\"steam_savingsCost\"])\n                .hisRollup(sum, 1day)\n \n   //if both savings columns are empty, return blank grid\n-  if (savings.isColBlank(\"gas_savingsCost\") and savings.isColBlank(\"elec_savingsCost\"))\n+  if (savings.isColBlank(\"hw_savingsCost\") and savings.isColBlank(\"elec_savingsCost\") and savings.isColBlank(\"chw_savingsCost\") and savings.isColBlank(\"steam_savingsCost\"))\n     return blankChart(\"Cost savings data is unavailable\", \"Check cost savings points\", dates)\n+  hisEndVal : if (dates.end > now()) now() else savings.meta.get(\"hisEnd\")\n+  //add cumulative sum columns\n+  out:   savings.addMeta({title:\"Cumulative Cost Savings\", subtitle:readById(site).dis, hisEnd:hisEndVal})\n+            //.findAll(r => r->ts.date >= savingsDate)\n+            .addColCuSum(\"steamSum\",\"steam_savingsCost\")\n+            .addColCuSum(\"chwSum\",\"chw_savingsCost\")\n+            //.addColCuSum(\"hwSum\",\"hw_savingsCost\")\n+            .addColCuSum(\"elecSum\",\"elec_savingsCost\")\n+            .keepCols([\"ts\",\"chwSum\",\"elecSum\",\"steamSum\"])\n+  if (out.isColBlank(\"steamSum\") and out.isColBlank(\"elecSum\") and out.isColBlank(\"chwSum\") and out.isColBlank(\"hwSum\")) return blankChart(\"Cost savings data is unavailable\", \"No data after kickoff date\", dates)\n \n-  //add cumulative sum columns\n-  out:   savings.addMeta({title:\"Cumulative Cost Savings\", subtitle:readById(site).dis})\n-            .findAll(r => r->ts.date >= savingsDate)\n-            .addColCuSum(\"gasSum\",\"gas_savingsCost\")\n-            .addColCuSum(\"elecSum\",\"elec_savingsCost\")\n-            .keepCols([\"ts\",\"gasSum\",\"elecSum\"])\n-\n-  if (out.isColBlank(\"gasSum\") and out.isColBlank(\"elecSum\")) return blankChart(\"Cost savings data is unavailable\", \"No data after kickoff date\", dates)\n-\n-  try out = out.stackedAreaChart() catch out\n-  out=out.addColMetaClean(\"gasSum\",{chartGroup:\"main\", color:\"purple\", dis:\"Gas Savings\"})\n-         .addColMetaClean(\"elecSum\",{chartGroup:\"main\", color:\"orange\", dis:\"Electricity Savings\", title:\"Cumulative Cost Savings - \"+site.toRec.dis})\n+  if(showStackedAreaChart) try out = out.stackedAreaChart() catch out\n+  out=out.addColMetaClean(\"steamSum\",{chartGroup:\"savings\", color:\"slateBlue\", dis:\"Steam Savings\"})\n+         .addColMetaClean(\"elecSum\",{chartGroup:\"savings\", color:\"orange\", dis:\"Electricity Savings\"})\n+         .addColMetaClean(\"chwSum\",{chartGroup:\"savings\", color:\"skyBlue\", dis:\"Chilled Water Savings\"})\n+         //.addColMetaClean(\"hwSum\",{chartGroup:\"main\", color:\"orange\", dis:\"Hot Water Savings\", title:\"Cumulative Cost Savings - \"+site.toRec.dis})\n \n   //fix hisEnd so chart axes are correct\n-  if (dates.end > now()) out=out.addMeta({hisEnd:now()})\n+  // ERRORING IN 3.1.11\n+  //if (dates.toDateSpan.end > now()) out=out.addMeta({hisEnd:out.last.get(\"ts\")})\n \n   //add kickoff date line\n   if (showKickoffDate) do\n@@ -50,5 +55,5 @@              .addColMetaClean(\"kickoff\",{chartGroup:\"main\", dis:\"Kickoff Date\", strokeDasharray:\"2,4\", color:\"black\", format:\"#\"})\n     end\n \n-  return out\n+  return out.addMeta({chartNoScroll})//hisJoin([out, wdg_site_chart_baselineEnergy(site, dates)]).addMeta({chartLegendNoSort})\n end"
    },
    {
      "name": "wdg_site_table_monthlyUsage",
      "pod": "kwLinkEnergyAgentExt",
      "pod_src": "(siteSelector, dates, opts: {}) => do\n\n  // Edge-case Handling\n  nullList: [@null]\n  if (siteSelector==@null or siteSelector==nullList or siteSelector==null) return {info: \"Select a site.\"}.toGrid.addColMeta(\"info\", {dis:\"Information\"})\n\n  // Normalize Inputs\n  sites: siteSelector.navResolve()\n  siteRefs: siteSelector.navResolve().map(r=>r.toRec.get(\"id\"))\n  utilType: opts.get(\"utilType\")\n  dates = dates.toSpan()\n  his: siteRefs.map(r => do\n    if      (utilType == \"all\")    try readAll(interval and his and point and siteRef==r).hisRead(dates).hisRollup(sum, 1mo) catch null\n    else if (utilType == \"elec\")   try pointQuery(\"elec_intervalPwr\",          {readAll, siteRef: r, checked: false}).colToList(\"id\").hisRead(dates).hisRollup(sum, 1mo) catch null\n    else if (utilType == \"gas\")    try pointQuery(\"natGas_intervalCons\",       {readAll, siteRef: r, checked: false}).colToList(\"id\").hisRead(dates).hisRollup(sum, 1mo) catch null\n    else if (utilType == \"steam\")  try pointQuery(\"steam_intervalCons\",        {readAll, siteRef: r, checked: false}).colToList(\"id\").hisRead(dates).hisRollup(sum, 1mo) catch null\n    else if (utilType == \"chw\")    try pointQuery(\"chilledWater_intervalCons\", {readAll, siteRef: r, checked: false}).colToList(\"id\").hisRead(dates).hisRollup(sum, 1mo) catch null\n  end).findAll(r=>r.isNonNull)\n\n  if (his.isEmpty) return {info: \"No Usage Data found for the selected site.\"}.toGrid.addColMeta(\"info\", {dis: \"Information\"}).addMeta({title: \"Monthly Usage\", subtitle: dates.start.format(\"MMM YYYY\") + \" - \" + dates.end.format(\"MMM YYYY\")})\n\n  out : hisJoin(his).hisFindAll(r=>r.isNonNull).table\n\n  if (out.isEmpty) return {info: \"No Usage Data found for the selected site.\"}.toGrid.addColMeta(\"info\", {dis: \"Information\"}).addMeta({title: \"Monthly Usage\", subtitle: dates.start.format(\"MMM YYYY\") + \" - \" + dates.end.format(\"MMM YYYY\")})\n\n  dateStart: out.first.get(\"ts\")\n  dateEnd: out.last.get(\"ts\")\n  out = out.addColMetaAll({viz:\"barCell\"}).addColMeta(\"ts\", {format:\"MMM-YYYY\"}).addMeta({view:\"chart\",\n                                                       chartType: \"bar\",\n                                                       chartLegend: \"hide\",\n                                                       title: title: \"Monthly Usage\",\n                                                       subtitle: if (dateStart==dateEnd)  dateStart.format(\"MMM YYYY\")\n                                                                 else dateStart.format(\"MMM YYYY\") + \" - \" + dateEnd.format(\"MMM YYYY\")})\n out.colNames.map(v => if      (out.col(v).meta.get(\"unit\")==\"therm\") out = out.addColMeta(v, {color: \"purple\"}) // dis: out.col(v).meta.get(\"siteRef\").toRec.get(\"dis\") + \" - Natural Gas Use\"\n                       else if (out.col(v).meta.get(\"unit\")==\"kW\")    out = out.addColMeta(v, {color: \"orange\"}))//, dis: out.col(v).meta.get(\"siteRef\").toRec.get(\"dis\") + \" - Electricity Use\"\n\n\n\n  return out\nend",
      "local_src": "(siteSelector, dates, opts: {}) => do\n    \n  // Edge-case Handling\n  nullList: [@null]\n  if (siteSelector==@null or siteSelector==nullList or siteSelector==null) return {info: \"Select a site.\"}.toGrid.addColMeta(\"info\", {dis:\"Information\"})\n  \n  // Normalize Inputs\n  sites: siteSelector.navResolve()\n  siteRefs: siteSelector.navResolve().map(r=>r.toRec.get(\"id\"))\n  utilType: opts.get(\"utilType\")\n  dates = dates.toSpan()\n  his: siteRefs.map(r => do\n    if      (utilType == \"all\")    try readAll(interval and his and point and siteRef==r).hisRead(dates).hisRollup(sum, 1mo) catch null\n    else if (utilType == \"elec\")   try pointQuery(\"elec_intervalPwr\",          {readAll, siteRef: r, checked: false}).colToList(\"id\").hisRead(dates).hisRollup(sum, 1mo) catch null\n    else if (utilType == \"gas\")    try pointQuery(\"natGas_intervalCons\",       {readAll, siteRef: r, checked: false}).colToList(\"id\").hisRead(dates).hisRollup(sum, 1mo) catch null\n    else if (utilType == \"steam\")  try pointQuery(\"steam_intervalCons\",        {readAll, siteRef: r, checked: false}).colToList(\"id\").hisRead(dates).hisRollup(sum, 1mo) catch null\n    else if (utilType == \"chw\")    try pointQuery(\"chilledWater_intervalCons\", {readAll, siteRef: r, checked: false}).colToList(\"id\").hisRead(dates).hisRollup(sum, 1mo) catch null\n  end).findAll(r=>r.isNonNull)\n  \n  if (his.isEmpty) return {info: \"No Usage Data found for the selected site.\"}.toGrid.addColMeta(\"info\", {dis: \"Information\"}).addMeta({title: \"Monthly Usage\", subtitle: dates.start.format(\"MMM YYYY\") + \" - \" + dates.end.format(\"MMM YYYY\")})\n  \n  out : hisJoin(his).hisFindAll(r=>r.isNonNull).table\n  \n  if (out.isEmpty) return {info: \"No Usage Data found for the selected site.\"}.toGrid.addColMeta(\"info\", {dis: \"Information\"}).addMeta({title: \"Monthly Usage\", subtitle: dates.start.format(\"MMM YYYY\") + \" - \" + dates.end.format(\"MMM YYYY\")})\n\n  dateStart: out.first.get(\"ts\")\n  dateEnd: out.last.get(\"ts\")\n  out = out.addColMetaAll({viz:\"barCell\"}).addColMeta(\"ts\", {format:\"MMM-YYYY\"}).addMeta({view:\"chart\",\n                                                       chartType: \"bar\",\n                                                       chartLegend: \"hide\",\n                                                       title: title: sites.toRec.get(\"dis\") + \" Monthly Usage\",\n                                                       subtitle: if (dateStart==dateEnd)  dateStart.format(\"MMM YYYY\")\n                                                                 else dateStart.format(\"MMM YYYY\") + \" - \" + dateEnd.format(\"MMM YYYY\")})\n out.colNames.map(v => if      (out.col(v).meta.get(\"unit\")==\"therm\") out = out.addColMeta(v, {color: \"purple\"}) // dis: out.col(v).meta.get(\"siteRef\").toRec.get(\"dis\") + \" - Natural Gas Use\"\n                       else if (out.col(v).meta.get(\"unit\")==\"kW\")    out = out.addColMeta(v, {color: \"orange\"}))//, dis: out.col(v).meta.get(\"siteRef\").toRec.get(\"dis\") + \" - Electricity Use\"\n  \n // Shorten column names to only include navName instead of equipRef and navName\n elecList:[]\n gasList:[]\n steamList:[]\n chilledList:[]\n out.colNames.each()c => do\n   meta: out.col(c).meta\n   if(meta.has(\"elec\")) elecList = elecList.add(c)\n   if(meta.has(\"gas\")) gasList = gasList.add(c)\n   if(meta.has(\"steam\")) steamList = steamList.add(c)\n   if(meta.has(\"chilled\")) chilledList = chilledList.add(c)\n end\n \n \n out = out.colNames.reduce(out, (g, v) => do\n   meta: g.col(v).meta\n   if (meta.has(\"equipRef\")) do\n     equip: meta.get(\"equipRef\").toRec\n     navDis: equip.get(\"navName\")\n     g.setColMeta(v, meta.set(\"disMacro\", navDis + \" \\$navName\"))\n   else\n     g\n end)\n \n \n out = out.map() r => do\n   out.colNames.each() c => do\n     if(c == \"ts\") return null\n     if(r.get(c).unit  == \"kW\") r = r.set(c, r.get(c).as(\"kWh\"))\n     else if(r.get(c).unit   == \"lb/h\") r = r.set(c, r.get(c).as(\"lb\"))\n     else if(r.get(c).unit   == \"tonref\") r = r.set(c, r.get(c).as(\"tonrefh\"))\n   end\n   return r = r\n end\n \n  \n  colOrder: [[\"ts\"], elecList, gasList, steamList, chilledList].flatten//.toList\n  return out.reorderKeepCols(colOrder)\nend",
      "diff": "@@ -28,13 +28,48 @@   out = out.addColMetaAll({viz:\"barCell\"}).addColMeta(\"ts\", {format:\"MMM-YYYY\"}).addMeta({view:\"chart\",\n                                                        chartType: \"bar\",\n                                                        chartLegend: \"hide\",\n-                                                       title: title: \"Monthly Usage\",\n+                                                       title: title: sites.toRec.get(\"dis\") + \" Monthly Usage\",\n                                                        subtitle: if (dateStart==dateEnd)  dateStart.format(\"MMM YYYY\")\n                                                                  else dateStart.format(\"MMM YYYY\") + \" - \" + dateEnd.format(\"MMM YYYY\")})\n  out.colNames.map(v => if      (out.col(v).meta.get(\"unit\")==\"therm\") out = out.addColMeta(v, {color: \"purple\"}) // dis: out.col(v).meta.get(\"siteRef\").toRec.get(\"dis\") + \" - Natural Gas Use\"\n                        else if (out.col(v).meta.get(\"unit\")==\"kW\")    out = out.addColMeta(v, {color: \"orange\"}))//, dis: out.col(v).meta.get(\"siteRef\").toRec.get(\"dis\") + \" - Electricity Use\"\n \n+ // Shorten column names to only include navName instead of equipRef and navName\n+ elecList:[]\n+ gasList:[]\n+ steamList:[]\n+ chilledList:[]\n+ out.colNames.each()c => do\n+   meta: out.col(c).meta\n+   if(meta.has(\"elec\")) elecList = elecList.add(c)\n+   if(meta.has(\"gas\")) gasList = gasList.add(c)\n+   if(meta.has(\"steam\")) steamList = steamList.add(c)\n+   if(meta.has(\"chilled\")) chilledList = chilledList.add(c)\n+ end\n \n \n-  return out\n+ out = out.colNames.reduce(out, (g, v) => do\n+   meta: g.col(v).meta\n+   if (meta.has(\"equipRef\")) do\n+     equip: meta.get(\"equipRef\").toRec\n+     navDis: equip.get(\"navName\")\n+     g.setColMeta(v, meta.set(\"disMacro\", navDis + \" \\$navName\"))\n+   else\n+     g\n+ end)\n+\n+\n+ out = out.map() r => do\n+   out.colNames.each() c => do\n+     if(c == \"ts\") return null\n+     if(r.get(c).unit  == \"kW\") r = r.set(c, r.get(c).as(\"kWh\"))\n+     else if(r.get(c).unit   == \"lb/h\") r = r.set(c, r.get(c).as(\"lb\"))\n+     else if(r.get(c).unit   == \"tonref\") r = r.set(c, r.get(c).as(\"tonrefh\"))\n+   end\n+   return r = r\n+ end\n+\n+\n+  colOrder: [[\"ts\"], elecList, gasList, steamList, chilledList].flatten//.toList\n+  return out.reorderKeepCols(colOrder)\n end"
    },
    {
      "name": "wdg_terminalUnits_chart_damperPositions",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "// Updated 2025-08-27 by Kyle Wolfe to add occ/AHU status to display if occDisp option is true\n(ahu, dates, opts:{}, vavs:null) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  ahuRec: try ahu.toRec catch null\n  ahuId: try ahuRec->id catch null\n  vavRecList: if (vavs!=null) vavs.toRecList\n              else null\n  dates = dates.toSpan\n\n  if (ahuRec.isNull and vavRecList.isNull) return blankChart(\"Cannot determine vav's\")\n  if (ahu==\"undefined\") return blankChart(\"Could not calculate VAV Reheat Load\", \"Undefined selection not supported\")\n\n  //opts\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  occOnly: opts.get(\"occOnly\")\n  showUpstreamData: if (opts.get(\"showUpstreamData\")==true) true else false\n  rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n  if (rollupOverride.isNull) do\n    days: (dates.end - dates.start).to(1day)\n    if      (days<=1day)  rollupOverride=1hr\n    else if (days<=8day)  rollupOverride=4hr\n    else if (days<=32day) rollupOverride=12hr\n    else                  rollupOverride=1day\n  end\n  opts = opts.set(\"rollupOverride\",rollupOverride)\n\n  occDisp: opts.optNorm(\"occDisp\", false)\n\n  //logic\n  vavs= if (vavRecList!=null) vavRecList\n        else if (ahuId.isRef) readAll(equip and vav and airRef==ahuId)\n        else null\n  if (vavs.isNull) return blankChart(\"No valid vav's found under selection\")\n\n  avgGrid:[].toGrid\n\n  //read all damper points\n  //points: vavs.map row => read(equipRef==row->id and point and damper and cmd, false)\n  points: vavs.map(row => \"dmpr_cmd\".pointQuery({equipRef: row->id}))\n  missingPoints: points.findAll(v=>v.isNull).size\n  goodPoints: points.findAll(v=>not v.isNull).toRecList\n\n  his: goodPoints.hisRead(dates, {-limit})\n                 .hisMap(v => if (v.isNaN()) null else v)\n                 .hisRollupAuto(rollupOverride)\n\n  rogue:0\n\n  his.cols.each colm => do\n    if (colm.name !=\"ts\") do\n      avg: his.colToList(colm.name).fold(avg,\"*\")\n      pointMeta: colm.meta\n      color: \"#B2E1FA\"\n      strokeWidth:0.25\n      if (avg>90) do\n        color = \"red\"\n        strokeWidth=2\n        rogue=rogue+1\n        end\n      dis: colm.meta.get(\"equipRef\").readById.dis\n      his = his.addColMetaClean(colm.name,colm.meta.merge({dis:dis, color:color, strokeWidth:strokeWidth, chartGroup:\"dampers\"}))\n      avgGrid = avgGrid.addRow({id:pointMeta->id,avg:avg, mod:pointMeta->mod})\n      end\n  end\n  rogueStr: \"(\"+rogue+\"/\"+points.size+\") rogue zones in red\"\n\n  his = his.addCol(\"average\", row => row.vals[1..-1].fold(avg))\n           .addColMetaClean(\"average\",{color:\"#007BFE\", strokeWidth:2, chartMin:0%, chartMax:100%, chartGroup:\"dampers\", title:\"VAV Damper Position\", subtitle:\"Average shown in blue, \"+rogueStr})\n\n  if (occDisp) do\n    if (ahuId.isNonNull) ahuIds: [ahuId]\n    else ahuIds: vavRecList.map(v => v.get(\"airRef\")).unique.findAll(v => v.isNonNull)\n    occHis: ahuIds.map(v => v.logic_ahu_occPeriods(dates, {}, {}))\n    his = [his].addAll(occHis).hisJoin\n  end\n\n  out: his.addMeta({\"chartLegend\":\"hide\"})\n\n  //format\n  if (showUpstreamData) try out = [out,  wdg_terminalUnits_chart_upstreamData(goodPoints.first.toRec.get(\"equipRef\"), dates, opts)].hisJoin catch null\n\n  //return out\n  return out\n\nend",
      "local_src": "// Updated 2025-08-27 by Kyle Wolfe to add occ/AHU status to display if occDisp option is true\n(ahu, dates, opts:{}, vavs:null) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  ahuRec: try ahu.toRec catch null\n  ahuId: try ahuRec->id catch null\n  vavRecList: if (vavs!=null) vavs.toRecList\n              else null\n  dates = dates.toSpan\n\n  if (ahuRec.isNull and vavRecList.isNull) return blankChart(\"Cannot determine vav's\")\n  if (ahu==\"undefined\") return blankChart(\"Could not calculate VAV Reheat Load\", \"Undefined selection not supported\")\n\n  //opts\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  occOnly: opts.get(\"occOnly\")\n  showUpstreamData: if (opts.get(\"showUpstreamData\")==true) true else false\n  rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n  if (rollupOverride.isNull) do\n    days: (dates.end - dates.start).to(1day)\n    if      (days<=1day)  rollupOverride=1hr\n    else if (days<=8day)  rollupOverride=4hr\n    else if (days<=32day) rollupOverride=12hr\n    else                  rollupOverride=1day\n  end\n  opts = opts.set(\"rollupOverride\",rollupOverride)\n\n  occDisp: opts.optNorm(\"occDisp\", false)\n\n  //logic\n  vavs= if (vavRecList!=null) vavRecList\n        else if (ahuId.isRef) readAll(equip and vav and airRef==ahuId)\n        else null\n  if (vavs.isNull) return blankChart(\"No valid vav's found under selection\")\n\n  avgGrid:[].toGrid\n\n  //read all damper points\n  //points: vavs.map row => read(equipRef==row->id and point and damper and cmd, false)\n  points: vavs.map(row => \"dmpr_cmd\".pointQuery({equipRef: row->id}))\n  missingPoints: points.findAll(v=>v.isNull).size\n  goodPoints: points.findAll(v=>not v.isNull).toRecList\n\n  his: goodPoints.hisRead(dates, {-limit})\n                 .hisMap(v => if (v.isNaN()) null else v)\n                 .hisRollupAuto(rollupOverride)\n\n  rogue:0\n\n  his.cols.each colm => do\n    if (colm.name !=\"ts\") do\n      avg: his.colToList(colm.name).fold(avg,\"*\")\n      pointMeta: colm.meta\n      //color: \"#B2f1FA\"\n      color: \"#007BFE\"\n      strokeWidth: 0.25\n      if (avg>90) do\n        color = \"red\"\n        strokeWidth=2\n        rogue=rogue+1\n        end\n      dis: colm.meta.get(\"equipRef\").readById.dis\n      his = his.addColMetaClean(colm.name,colm.meta.merge({dis:dis, color:color, strokeWidth:strokeWidth, chartGroup:\"dampers\"}))\n      avgGrid = avgGrid.addRow({id:pointMeta->id,avg:avg, mod:pointMeta->mod})\n      end\n  end\n  rogueStr: \"(\"+rogue+\"/\"+points.size+\") rogue zones in red\"\n\n  his = his.addCol(\"average\", row => row.vals[1..-1].fold(avg))\n           .addColMetaClean(\"average\",{color:\"#007BFE\", strokeWidth:2, chartMin:0%, chartMax:100%, chartGroup:\"dampers\", title:\"VAV Damper Position\", subtitle:\"Average shown in blue, \"+rogueStr})\n\n  if (occDisp) do\n    if (ahuId.isNonNull) ahuIds: [ahuId]\n    else ahuIds: vavRecList.map(v => v.get(\"airRef\")).unique.findAll(v => v.isNonNull)\n    occHis: ahuIds.map(v => v.logic_ahu_occPeriods(dates, {}, {}))\n    his = [his].addAll(occHis).hisJoin\n  end\n\n  out: his.addMeta({\"chartLegend\":\"hide\"})\n\n  //format\n  if (showUpstreamData) try out = [out,  wdg_terminalUnits_chart_upstreamData(goodPoints.first.toRec.get(\"equipRef\"), dates, opts)].hisJoin catch null\n\n  //return out\n  return out\n\nend",
      "diff": "@@ -54,8 +54,9 @@     if (colm.name !=\"ts\") do\n       avg: his.colToList(colm.name).fold(avg,\"*\")\n       pointMeta: colm.meta\n-      color: \"#B2E1FA\"\n-      strokeWidth:0.25\n+      //color: \"#B2f1FA\"\n+      color: \"#007BFE\"\n+      strokeWidth: 0.25\n       if (avg>90) do\n         color = \"red\"\n         strokeWidth=2\n"
    },
    {
      "name": "wdg_terminalUnits_chart_hotColdZones",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "(ahu, dates, opts:{}, vavs:null) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  dates = dates.toSpan\n  ahuRec: try ahu.toRec catch null\n  ahuId: try ahuRec->id catch null\n  vavRecList: if (vavs!=null) vavs.toRecList\n              else try readAll(equip and vav and airRef==ahuId) catch []\n  if (vavRecList.size==0 or vavRecList.isEmpty) vavRecList=null\n\n  if (ahuRec.isNull and vavRecList.isNull) return blankChart(\"Cannot determine vav's\")\n  if (ahu==\"undefined\") return blankChart(\"Could not calculate\", \"Undefined selection not supported\")\n\n  //opts\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  occOnly: opts.get(\"occOnly\")\n  showUpstreamData: if (opts.get(\"showUpstreamData\")==true) true else false\n  rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n  if (rollupOverride.isNull) do\n    days: (dates.end - dates.start).to(1day)\n    if      (days<=1day)  rollupOverride=1hr\n    else if (days<=8day)  rollupOverride=4hr\n    else if (days<=32day) rollupOverride=12hr\n    else                  rollupOverride=1day\n  end\n  opts = opts.set(\"rollupOverride\",rollupOverride)\n\n  occDisp: opts.optNorm(\"occDisp\", false)\n\n  //logic\n\n\n  //check status function\n  checkStatus: (val) => do\n    if      (val==null) \"unknown\"\n    else if (val > 0)   \"too hot\"\n    else if (val < 0)   \"too cold\"\n    else                \"good\"\n  end\n\n  //create output grid\n  grid: []\n  errGrid: []\n\n  //loop through vavs\n  vavRecList.each rec => do\n\n    currentVavHis: try logic_vav_tempSpDelta(rec, dates, opts) catch null\n    if (not currentVavHis.isGrid) return null\n\n    //rename col to dis\n    colName: rec.toStr.toTagName\n    try do\n      statusHis: currentVavHis.keepCols([\"ts\",\"delta\"])\n                              .hisMap(v=>v.checkStatus)\n                              .renameCol(\"delta\", colName)\n      grid = grid.add(statusHis.reorderCols([\"ts\",colName]))\n    end\n    catch errGrid=errGrid.add(currentVavHis)\n  end //end each  vav loop\n\n  out: grid.hisJoin.map row => do\n    vals: row.vals[1..-1]\n    {ts: row->ts,\n     cold:    vals.findAll(v=>v==\"too cold\").size,\n     good:    vals.findAll(v=>v==\"good\").size,\n     hot:     vals.findAll(v=>v==\"too hot\").size,\n     unknown: vals.findAll(v=>v==\"unknown\").size\n     }\n  end\n  //add total cannot calculate vav number\n  out = out.map(r=> do\n    cold: r.get(\"cold\")\n    good: r.get(\"good\")\n    hot: r.get(\"hot\")\n    unknown: r.get(\"unknown\")\n    totalCalculated: cold + good + hot + unknown\n    totalVavs: readAll(equip and vav and siteRef == vavs.first->siteRef).size\n    r = r.set(\"cannotCalc\", totalVavs-totalCalculated)\n  end)\n\n  //format\n  if (showUpstreamData) try out = [out,  wdg_terminalUnits_chart_upstreamData(goodPoints.first.toRec.get(\"equipRef\"), dates, opts)].hisJoin catch null\n\nreturn  out = out.stream\n           .reorderCols([\"ts\",\"cold\",\"good\",\"hot\", \"unknown\", \"cannotCalc\"])\n           .addColMetaClean(\"cold\", {dis:\"Too Cold\", chartType:\"area\", chartAreaMode:\"zero\", color:\"blue\", chartMin:0, chartMax:vavRecList.size, title:\"VAV Zone Status\", subtitle:\"Blue = cold, Red = hot, Green = good, Gray = unknown, Purple = cannot calculate\"})\n           .addColMetaClean(\"good\", {dis:\"Good\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"green\"})\n           .addColMetaClean(\"hot\", {dis:\"Too Hot\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"red\"})\n           .addColMetaClean(\"unknown\", {dis:\"Unknown\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"gray\"})\n           .addColMetaClean(\"cannotCalc\", {dis:\"Cannot Calculate\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"purple\"})\n           .collect\n           .stackedAreaChart\n\n  if (occDisp) do\n    if (ahuId.isNonNull) ahuIds: [ahuId]\n    else ahuIds: vavRecList.map(v => v.get(\"airRef\")).unique.findAll(v => v.isNonNull)\n    occHis: ahuIds.map(v => v.logic_ahu_occPeriods(dates, {}, {}))\n    out = [out].addAll(occHis).hisJoin\n  end\n\n  return out\nend",
      "local_src": "(ahu, dates, opts:{}, vavs:null) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  dates = dates.toSpan\n  ahuRec: try ahu.toRec catch null\n  ahuId: try ahuRec->id catch null\n  vavRecList: if (vavs!=null) vavs.toRecList\n              else try readAll(equip and vav and airRef==ahuId) catch []\n  if (vavRecList.size==0 or vavRecList.isEmpty) vavRecList=null\n\n  if (ahuRec.isNull and vavRecList.isNull) return blankChart(\"Cannot determine vav's\")\n  if (ahu==\"undefined\") return blankChart(\"Could not calculate\", \"Undefined selection not supported\")\n\n  //opts\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  occOnly: opts.get(\"occOnly\")\n  showUpstreamData: if (opts.get(\"showUpstreamData\")==true) true else false\n  rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n  if (rollupOverride.isNull) do\n    days: (dates.end - dates.start).to(1day)\n    if      (days<=1day)  rollupOverride=1hr\n    else if (days<=8day)  rollupOverride=4hr\n    else if (days<=32day) rollupOverride=12hr\n    else                  rollupOverride=1day\n  end\n  opts = opts.set(\"rollupOverride\",rollupOverride)\n\n  //logic\n\n\n  //check status function\n  checkStatus: (val) => do\n    if      (val==null) \"unkown\"\n    else if (val > 0)   \"too hot\"\n    else if (val < 0)   \"too cold\"\n    else                \"good\"\n  end\n  if(vavRecList.isNull or vavRecList.isEmpty) return \"No Data\"\n  //create output grid\n  grid: []\n  errGrid: []\n\n  //loop through vavs\n  vavRecList.each rec => do\n\n    currentVavHis: try logic_vav_tempSpDelta(rec, dates, opts) catch null\n    if (not currentVavHis.isGrid) return null\n\n    //rename col to dis\n    colName: rec.toStr.toTagName\n    try do\n      statusHis: currentVavHis.keepCols([\"ts\",\"delta\"])\n                              .hisMap(v=>v.checkStatus)\n                              .renameCol(\"delta\", colName)\n      grid = grid.add(statusHis.reorderCols([\"ts\",colName]))\n    end\n    catch errGrid=errGrid.add(currentVavHis)\n  end //end each  vav loop\n\n  out: grid.hisJoin.map row => do\n    vals: row.vals[1..-1]\n    {ts: row->ts,\n     cold:    vals.findAll(v=>v==\"too cold\").size,\n     good:    vals.findAll(v=>v==\"good\").size,\n     hot:     vals.findAll(v=>v==\"too hot\").size,\n     unknown: vals.findAll(v=>v==\"unknown\").size\n     }\n  end\n\n  //format\n  if (showUpstreamData) try out = [out,  wdg_terminalUnits_chart_upstreamData(goodPoints.first.toRec.get(\"equipRef\"), dates, opts)].hisJoin catch null\n\n  out = out.stream\n           .reorderCols([\"ts\",\"cold\",\"good\",\"hot\", \"unknown\"])\n           .addColMetaClean(\"cold\", {dis:\"Too Cold\", chartType:\"area\", chartAreaMode:\"zero\", color:\"blue\", chartMin:0, chartMax:grid.size, title:\"VAV Zone Status\", subtitle:\"Blue = cold, Red = hot, Green = good, Gray = unknown\"})\n           .addColMetaClean(\"good\", {dis:\"Good\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"green\"})\n           .addColMetaClean(\"hot\", {dis:\"Too Hot\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"red\"})\n           .addColMetaClean(\"unknown\", {dis:\"Unknown\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"gray\"})\n           .collect\n           .stackedAreaChart\n  return out\nend //end func\n",
      "diff": "@@ -28,19 +28,17 @@   end\n   opts = opts.set(\"rollupOverride\",rollupOverride)\n \n-  occDisp: opts.optNorm(\"occDisp\", false)\n-\n   //logic\n \n \n   //check status function\n   checkStatus: (val) => do\n-    if      (val==null) \"unknown\"\n+    if      (val==null) \"unkown\"\n     else if (val > 0)   \"too hot\"\n     else if (val < 0)   \"too cold\"\n     else                \"good\"\n   end\n-\n+  if(vavRecList.isNull or vavRecList.isEmpty) return \"No Data\"\n   //create output grid\n   grid: []\n   errGrid: []\n@@ -71,36 +69,17 @@      unknown: vals.findAll(v=>v==\"unknown\").size\n      }\n   end\n-  //add total cannot calculate vav number\n-  out = out.map(r=> do\n-    cold: r.get(\"cold\")\n-    good: r.get(\"good\")\n-    hot: r.get(\"hot\")\n-    unknown: r.get(\"unknown\")\n-    totalCalculated: cold + good + hot + unknown\n-    totalVavs: readAll(equip and vav and siteRef == vavs.first->siteRef).size\n-    r = r.set(\"cannotCalc\", totalVavs-totalCalculated)\n-  end)\n \n   //format\n   if (showUpstreamData) try out = [out,  wdg_terminalUnits_chart_upstreamData(goodPoints.first.toRec.get(\"equipRef\"), dates, opts)].hisJoin catch null\n \n-return  out = out.stream\n-           .reorderCols([\"ts\",\"cold\",\"good\",\"hot\", \"unknown\", \"cannotCalc\"])\n-           .addColMetaClean(\"cold\", {dis:\"Too Cold\", chartType:\"area\", chartAreaMode:\"zero\", color:\"blue\", chartMin:0, chartMax:vavRecList.size, title:\"VAV Zone Status\", subtitle:\"Blue = cold, Red = hot, Green = good, Gray = unknown, Purple = cannot calculate\"})\n+  out = out.stream\n+           .reorderCols([\"ts\",\"cold\",\"good\",\"hot\", \"unknown\"])\n+           .addColMetaClean(\"cold\", {dis:\"Too Cold\", chartType:\"area\", chartAreaMode:\"zero\", color:\"blue\", chartMin:0, chartMax:grid.size, title:\"VAV Zone Status\", subtitle:\"Blue = cold, Red = hot, Green = good, Gray = unknown\"})\n            .addColMetaClean(\"good\", {dis:\"Good\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"green\"})\n            .addColMetaClean(\"hot\", {dis:\"Too Hot\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"red\"})\n            .addColMetaClean(\"unknown\", {dis:\"Unknown\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"gray\"})\n-           .addColMetaClean(\"cannotCalc\", {dis:\"Cannot Calculate\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"purple\"})\n            .collect\n            .stackedAreaChart\n-\n-  if (occDisp) do\n-    if (ahuId.isNonNull) ahuIds: [ahuId]\n-    else ahuIds: vavRecList.map(v => v.get(\"airRef\")).unique.findAll(v => v.isNonNull)\n-    occHis: ahuIds.map(v => v.logic_ahu_occPeriods(dates, {}, {}))\n-    out = [out].addAll(occHis).hisJoin\n-  end\n-\n   return out\n-end+end //end func"
    },
    {
      "name": "wdg_terminalUnits_chart_zonesHeatingCooling",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "// Updated 2025-08-27 by Kyle Wolfe to add occ/AHU status to display if occDisp option is true\n(ahu, dates, opts:{}, vavs:null) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  dates = dates.toSpan\n  ahuRec: try ahu.toRec catch null\n  ahuId: try ahuRec->id catch null\n  vavRecList: if (vavs!=null) vavs.toRecList\n              else try readAll(equip and vav and airRef==ahuId) catch []\n  if (vavRecList.size==0 or vavRecList.isEmpty) vavRecList=null\n\n  if (ahuRec.isNull and vavRecList.isNull) return blankChart(\"Cannot determine vav's\")\n  if (ahu==\"undefined\") return blankChart(\"Could not calculate\", \"Undefined selection not supported\")\n\n  //opts\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  occOnly: opts.get(\"occOnly\")\n  showUpstreamData: if (opts.get(\"showUpstreamData\")==true) true else false\n  rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n  if (rollupOverride.isNull) do\n    days: (dates.end - dates.start).to(1day)\n    if      (days<=1day)  rollupOverride=1hr\n    else if (days<=8day)  rollupOverride=4hr\n    else if (days<=32day) rollupOverride=12hr\n    else                  rollupOverride=1day\n  end\n  opts = opts.set(\"rollupOverride\",rollupOverride)\n\n  occDisp: opts.optNorm(\"occDisp\", false)\n\n  //logic\n\n  //create output grid\n  grid: []\n  errGrid: []\n\n  //loop through vavs\n  vavRecList.each rec => do\n\n    currentVavHis: try logic_vav_heatingOrCooling(rec, dates, opts) catch null\n    if (not currentVavHis.isGrid) return null\n\n    //rename col to dis\n    colName: rec.toStr.toTagName\n    try do\n      statusHis: currentVavHis.keepCols([\"ts\",\"status\"])\n                              .renameCol(\"status\", colName)\n      grid = grid.add(statusHis.reorderCols([\"ts\",colName]))\n    end\n    catch errGrid=errGrid.add(currentVavHis)\n  end //end each  vav loop\n\n  out: grid.hisJoin.map row => do\n    vals: row.vals[1..-1]\n    {ts: row->ts,\n     cooling:    vals.findAll(v=>v==\"cooling\").size,\n     heating:    vals.findAll(v=>v==\"heating\").size,\n     ventilating:vals.findAll(v=>v==\"ventilating\").size,\n     unknown:    vals.findAll(v=>v==\"unknown\").size\n     }\n  end\n\n  //format\n  if (showUpstreamData) try out = [out,  wdg_terminalUnits_chart_upstreamData(goodPoints.first.toRec.get(\"equipRef\"), dates, opts)].hisJoin catch null\n\n  out = out.stream\n           .reorderCols([\"ts\",\"cooling\",\"ventilating\",\"heating\", \"unknown\"])\n           .addColMetaClean(\"cooling\", {dis:\"Cooling\", chartType:\"area\", chartAreaMode:\"zero\", color:\"blue\", chartMin:0, chartMax:grid.size, title:\"VAV Heating/Cooling Status\"})\n           .addColMetaClean(\"ventilating\", {dis:\"Ventilating Only\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"lightGray\"})\n           .addColMetaClean(\"heating\", {dis:\"Heating\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"red\"})\n           .addColMetaClean(\"unknown\", {dis:\"Unknown\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"gray\"})\n           .collect\n           .stackedAreaChart\n\n  if (occDisp) do\n    if (ahuId.isNonNull) ahuIds: [ahuId]\n    else ahuIds: vavRecList.map(v => v.get(\"airRef\")).unique.findAll(v => v.isNonNull)\n    occHis: ahuIds.map(v => v.logic_ahu_occPeriods(dates, {}, {}))\n    out = [out].addAll(occHis).hisJoin\n  end\n\n  return out\nend",
      "local_src": "(ahu, dates, opts:{}, vavs:null) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //normalize inputs\n  dates = dates.toSpan\n  ahuRec: try ahu.toRec catch null\n  ahuId: try ahuRec->id catch null\n  vavRecList: if (vavs!=null) vavs.toRecList\n              else try readAll(equip and vav and airRef==ahuId) catch []\n  if (vavRecList.size==0 or vavRecList.isEmpty) vavRecList=null\n\n  if (ahuRec.isNull and vavRecList.isNull) return blankChart(\"Cannot determine vav's\")\n  if (ahu==\"undefined\") return blankChart(\"Could not calculate\", \"Undefined selection not supported\")\n\n  //opts\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  occOnly: opts.get(\"occOnly\")\n  showUpstreamData: if (opts.get(\"showUpstreamData\")==true) true else false\n  rollupOverride: try if (opts.get(\"rollupOverride\").isNull) null else if (opts.get(\"rollupOverride\").isStr) opts.get(\"rollupOverride\").parseNumber else if (opts.get(\"rollupOverride\").isNumber) opts.get(\"rollupOverride\") else null catch null\n  if (rollupOverride.isNull) do\n    days: (dates.end - dates.start).to(1day)\n    if      (days<=1day)  rollupOverride=1hr\n    else if (days<=8day)  rollupOverride=4hr\n    else if (days<=32day) rollupOverride=12hr\n    else                  rollupOverride=1day\n  end\n  opts = opts.set(\"rollupOverride\",rollupOverride)\n\n  //logic\n  if(vavRecList.isNull or vavRecList.isEmpty) return \"No Data\"\n  //create output grid\n  grid: []\n  errGrid: []\n\n  //loop through vavs\n  vavRecList.each rec => do\n\n    currentVavHis: try logic_vav_heatingOrCooling(rec, dates, opts) catch null\n    if (not currentVavHis.isGrid) return null\n\n    //rename col to dis\n    colName: rec.toStr.toTagName\n    try do\n      statusHis: currentVavHis.keepCols([\"ts\",\"status\"])\n                              .renameCol(\"status\", colName)\n      grid = grid.add(statusHis.reorderCols([\"ts\",colName]))\n    end\n    catch errGrid=errGrid.add(currentVavHis)\n  end //end each  vav loop\n\n  out: grid.hisJoin.map row => do\n    vals: row.vals[1..-1]\n    {ts: row->ts,\n     cooling:    vals.findAll(v=>v==\"cooling\").size,\n     heating:    vals.findAll(v=>v==\"heating\").size,\n     ventilating:vals.findAll(v=>v==\"ventilating\").size,\n     unknown:    vals.findAll(v=>v==\"unknown\").size\n     }\n  end\n\n  //format\n  if (showUpstreamData) try out = [out,  wdg_terminalUnits_chart_upstreamData(goodPoints.first.toRec.get(\"equipRef\"), dates, opts)].hisJoin catch null\n\n  out = out.stream\n           .reorderCols([\"ts\",\"cooling\",\"ventilating\",\"heating\", \"unknown\"])\n           .addColMetaClean(\"cooling\", {dis:\"Cooling\", chartType:\"area\", chartAreaMode:\"zero\", color:\"blue\", chartMin:0, chartMax:grid.size, title:\"VAV Heating/Cooling Status\"})\n           .addColMetaClean(\"ventilating\", {dis:\"Ventilating Only\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"lightGray\"})\n           .addColMetaClean(\"heating\", {dis:\"Heating\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"red\"})\n           .addColMetaClean(\"unknown\", {dis:\"Unknown\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"gray\"})\n           .collect\n           .stackedAreaChart\n  return out\nend //end func\n",
      "diff": "@@ -1,4 +1,3 @@-// Updated 2025-08-27 by Kyle Wolfe to add occ/AHU status to display if occDisp option is true\n (ahu, dates, opts:{}, vavs:null) => do\n \n   //validate and authorize current user\n@@ -29,10 +28,8 @@   end\n   opts = opts.set(\"rollupOverride\",rollupOverride)\n \n-  occDisp: opts.optNorm(\"occDisp\", false)\n-\n   //logic\n-\n+  if(vavRecList.isNull or vavRecList.isEmpty) return \"No Data\"\n   //create output grid\n   grid: []\n   errGrid: []\n@@ -74,13 +71,5 @@            .addColMetaClean(\"unknown\", {dis:\"Unknown\", chartType:\"area\", chartAreaMode:\"prevSeries\", color:\"gray\"})\n            .collect\n            .stackedAreaChart\n-\n-  if (occDisp) do\n-    if (ahuId.isNonNull) ahuIds: [ahuId]\n-    else ahuIds: vavRecList.map(v => v.get(\"airRef\")).unique.findAll(v => v.isNonNull)\n-    occHis: ahuIds.map(v => v.logic_ahu_occPeriods(dates, {}, {}))\n-    out = [out].addAll(occHis).hisJoin\n-  end\n-\n   return out\n-end+end //end func"
    },
    {
      "name": "wdg_terminalUnits_table_ahuSummary",
      "pod": "kwLinkAnalyticsExt",
      "pod_src": "// Updated by Kyle Wolfe on 2025-08-27 to get sparks from both the individual points on the AHU,\n// and the AHU rec itself\n(site, dates, opts:{}) => do\n\n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //INPUTS\n  siteRec: site.toRec\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  //opts\n  rollupOverride: if (opts.has(\"rollupOverride\")) opts.get(\"rollupOverride\") else null\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  occOnly: if (opts.has(\"occOnly\")) opts.get(\"occOnly\") else true\n\n  //AHU LOOP\n  ahus: readAll(ahu and siteRef==siteId and equip and not vavSummaryNoShow)\n  if (ahus.size==0) return \"No AHUs at \"+siteRec.dis\n\n  grid: ahus.map row => do\n\n    ahuId: row.get(\"id\")\n\n    //get occ periods\n    if (occOnly) occPeriods: logic_ahu_occPeriods(ahuId, dates)\n\n    //get downstream units\n    downstream: readAll(equip and airRef==row->id)\n    downstreamIds: try downstream.colToList(\"id\") catch null\n\n    rules: readAll(rule and not disabled)\n    downstreamPoints: toPoints(downstream)\n    downstreamVavs: toRecList(downstream)\n    downstreamPointSparks: ruleSparksCount(downstreamPoints, dates, rules)\n    downstreamVavSparks: ruleSparksCount(downstreamVavs, dates, rules)\n    downstreamSparks: downstreamPointSparks + downstreamVavSparks\n\n    //put together table\n    {id: row->id,\n     downstreamUnits: downstream.size,\n     downstreamSparks: downstreamSparks,\n     mod:row->mod,\n     contextVariables: {vavs:downstreamIds, ahu:ahuId, site:siteId}\n     }\n  end\n\n  //get undefined and all units\n  undefined: try readAll(siteRef==siteId and vav and not airRef and equip).colToList(\"id\")\n             catch []\n  all: try readAll(siteRef==siteId and equip and vav).colToList(\"id\")\n       catch []\n\n    out: grid.sort(\"id\")\n             .addRow({id:\"Undefined\", downstreamUnits:undefined.size, contextVariables:{ahu:\"undefined\", site:siteId, vavs:undefined}})\n             .addRow({id:\"ALL UNITS\", contextVariables:{site:siteId, vavs:all}})\n             .stream\n             .addColMetaClean(\"id\",{dis:\"AHU\"})\n             .addColMetaClean(\"downstreamSparks\", {dis:\"Downstream Faults\", viz:\"barCell\", color:\"red\"})\n             .addColMetaClean(\"downstreamUnits\",{dis:\"Downstream Units\", viz:\"barCell\"})\n             .collect\n             .hideCols([\"mod\",\"contextVariables\"])\n\n    out = out\n\n    return out\nend\n",
      "local_src": "// Updated by Kyle Wolfe on 2025-08-27 to get sparks from both the individual points on the AHU, and the AHU rec itself\n// Updated by Justin Brunner on 2026-01-23 to add supplyOnly opt that will filter all vav lists to be supply only vavs when true\n\n(site, dates, opts:{}) => do\n \n  //validate and authorize current user\n  kwLinkValidate(\"analytics\")\n\n  //INPUTS\n  siteRec: site.toRec\n  siteId: siteRec->id\n  dates = dates.toSpan\n\n  //opts\n  rollupOverride: if (opts.has(\"rollupOverride\")) opts.get(\"rollupOverride\") else null\n  debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n  occOnly: if (opts.has(\"occOnly\")) opts.get(\"occOnly\") else true\n  supplyOnly: if (opts.has(\"supplyOnly\")) opts.get(\"supplyOnly\") else false\n\n  //AHU LOOP\n  ahus: readAll(ahu and siteRef==siteId and equip and not vavSummaryNoShow)\n  if (ahus.size==0) return \"No AHUs at \"+siteRec.dis\n\n  grid: ahus.map row => do\n\n    ahuId: row.get(\"id\")\n\n    //get occ periods\n    if (occOnly) occPeriods: logic_ahu_occPeriods(ahuId, dates)\n\n    //get downstream units\n    downstream: readAll(equip and airRef==row->id)\n    if(supplyOnly) downstream = downstream.filter(supply)\n    downstreamIds: try downstream.colToList(\"id\") catch null\n\n    rules: readAll(rule and not disabled)\n    downstreamPoints: toPoints(downstream)\n    downstreamVavs: toRecList(downstream)\n    downstreamPointSparks: ruleSparksCount(downstreamPoints, dates, rules)\n    downstreamVavSparks: ruleSparksCount(downstreamVavs, dates, rules)\n    downstreamSparks: downstreamPointSparks + downstreamVavSparks\n\n    //put together table\n    {id: row->id,\n     downstreamUnits: downstream.size,\n     downstreamSparks: downstreamSparks,\n     mod:row->mod,\n     contextVariables: {vavs:downstreamIds, ahu:ahuId, site:siteId}\n     }\n  end\n\n  //get undefined and all units\n  undefined: try if(supplyOnly) readAll(siteRef==siteId and vav and not airRef and equip and supply).colToList(\"id\")\n                 else           readAll(siteRef==siteId and vav and not airRef and equip).colToList(\"id\")\n             catch []\n  all: try if(supplyOnly) readAll(siteRef==siteId and equip and vav and supply).colToList(\"id\")\n           else           readAll(siteRef==siteId and equip and vav).colToList(\"id\")\n       catch []\n\n    out: grid.sort(\"id\")\n             .addRow({id:\"Undefined\", downstreamUnits:undefined.size, contextVariables:{ahu:\"undefined\", site:siteId, vavs:undefined}})\n             .addRow({id:\"ALL UNITS\", contextVariables:{site:siteId, vavs:all}})\n             .stream\n             .addColMetaClean(\"id\",{dis:\"AHU\"})\n             .addColMetaClean(\"downstreamSparks\", {dis:\"Downstream Faults\", viz:\"barCell\", color:\"red\"})\n             .addColMetaClean(\"downstreamUnits\",{dis:\"Downstream Units\", viz:\"barCell\"})\n             .collect\n             .hideCols([\"mod\",\"contextVariables\"])\n\n    out = out\n\n    return out\nend\n",
      "diff": "@@ -1,5 +1,6 @@-// Updated by Kyle Wolfe on 2025-08-27 to get sparks from both the individual points on the AHU,\n-// and the AHU rec itself\n+// Updated by Kyle Wolfe on 2025-08-27 to get sparks from both the individual points on the AHU, and the AHU rec itself\n+// Updated by Justin Brunner on 2026-01-23 to add supplyOnly opt that will filter all vav lists to be supply only vavs when true\n+\n (site, dates, opts:{}) => do\n \n   //validate and authorize current user\n@@ -14,6 +15,7 @@   rollupOverride: if (opts.has(\"rollupOverride\")) opts.get(\"rollupOverride\") else null\n   debug: opts.get(\"debug\")!=null and opts.get(\"debug\")!=\"Disable\"\n   occOnly: if (opts.has(\"occOnly\")) opts.get(\"occOnly\") else true\n+  supplyOnly: if (opts.has(\"supplyOnly\")) opts.get(\"supplyOnly\") else false\n \n   //AHU LOOP\n   ahus: readAll(ahu and siteRef==siteId and equip and not vavSummaryNoShow)\n@@ -28,6 +30,7 @@ \n     //get downstream units\n     downstream: readAll(equip and airRef==row->id)\n+    if(supplyOnly) downstream = downstream.filter(supply)\n     downstreamIds: try downstream.colToList(\"id\") catch null\n \n     rules: readAll(rule and not disabled)\n@@ -47,9 +50,11 @@   end\n \n   //get undefined and all units\n-  undefined: try readAll(siteRef==siteId and vav and not airRef and equip).colToList(\"id\")\n+  undefined: try if(supplyOnly) readAll(siteRef==siteId and vav and not airRef and equip and supply).colToList(\"id\")\n+                 else           readAll(siteRef==siteId and vav and not airRef and equip).colToList(\"id\")\n              catch []\n-  all: try readAll(siteRef==siteId and equip and vav).colToList(\"id\")\n+  all: try if(supplyOnly) readAll(siteRef==siteId and equip and vav and supply).colToList(\"id\")\n+           else           readAll(siteRef==siteId and equip and vav).colToList(\"id\")\n        catch []\n \n     out: grid.sort(\"id\")\n"
    }
  ]
}