Autoscaling Profile Limits

autoscalingprofilelimitsmonitoringmanagement

Monitor and manage autoscaling profile limits and usage

typescriptautoscaling-profiles-limits.ts
/*
 * TagoIO - Analysis Example
 * Auto Scaling analysis
 *
 * Check out the SDK documentation on: https://js.sdk.tago.io
 *
 * Ths is a script to automatically check your current usage, and auto-scale your account if needed.
 * You can get the analysis template with all the Environment Variables here:
 *          https://admin.tago.io/template/62151212ec8d8f0012c52772
 *
 * In order to use this analysis, you must setup all the environment variables needed.
 * You're also required to create an Action of trigger type Schedule,
 *  and choose to run this analysis.
 * In the action you set how often you want to run this script to check your limits.
 * It can set to a minimum of 1 minute.
 *
 * Environment Variables
 * In order to use this analysis, you must setup the Environment Variable table.
 *   account_token: Your account token. Check the steps at the end to understand how to generate it.
 *   The 95 value will scale data input when it reach 95% of the usage.
 *   Keep it blank to not scale data input.
 *   input: 95
 *   output: 95
 *   data_records: 95
 *   analysis: 95
 *   sms: 95
 *   email: 95
 *   push_notification: 95
 *   file_storage: 95
 *
 * Steps to generate an account_token:
 * 1 - Enter the following link: https://admin.tago.io/account/
 * 2 - Select your Profile.
 * 3 - Enter Tokens tab.
 * 4 - Generate a new Token with Expires Never.
 * 5 - Press the Copy Button and place at the Environment Variables tab of this analysis.
 */

import type { AnalysisEnvironment, BillingPrices, TagoContext } from "npm:@tago-io/sdk";
import { Account, Analysis, Utils } from "npm:@tago-io/sdk";

/**
 * Check if service needs autoscaling
 * @param currentUsage current usage of the profile
 * @param allocated limit allocated of the profile
 * @param scale percentage of usage to allow scaling up
 */
function checkAutoScale(currentUsage: number, allocated: number, scale: number): boolean {
  if (!scale || !allocated) {
    return false;
  }
  const threshold = allocated * (scale * 0.01);

  return threshold <= currentUsage;
}

/**
 *  Get next valid service limit
 */
function getNextTier(serviceValues: { amount: number }[], accountLimit: number): number | undefined {
  if (!accountLimit) {
    return undefined;
  }
  const nextValue = serviceValues.sort((a, b) => a.amount - b.amount).find(({ amount }) => amount > accountLimit);

  return nextValue?.amount || undefined;
}

/**
 * Parses the current limit of the account
 */
function getAccountLimit(servicesLimit: Record<string, unknown>): Record<string, { limit: number }> {
  return Object.keys(servicesLimit).reduce((result: Record<string, { limit: number }>, key) => {
    result[key] = servicesLimit[key] as { limit: number };

    return result;
  }, {});
}

/**
 * Find the ID of the profile from the token being used.
 */
async function getProfileIDByToken(account: Account, token: string): Promise<string | false> {
  const profiles = await account.profiles.list();
  for (const profile of profiles) {
    const [token_exist] = await account.profiles.tokenList(profile.id, {
      filter: {
        token,
      },
    });
    if (token_exist) {
      return profile.id;
    }
  }
  return false;
}

/**
 * Calculate services to be scaled
 */
function calculateAutoScale(
  prices: Record<string, { amount: number }[]>,
  profileLimit: Record<string, number>,
  profileLimitUsed: Record<string, number>,
  accountLimit: Record<string, { limit: number }>,
  environment: AnalysisEnvironment
): Record<string, { limit: number }> | null {
  const autoScaleServices: Record<string, { limit: number }> = {};
  for (const statisticKey in profileLimit) {
    if (!environment[statisticKey]) {
      continue;
    }

    const scale = Number(environment[statisticKey]);
    if (scale <= 0) {
      continue;
    }

    if (Number.isNaN(scale)) {
      console.error(`[ERROR] Ignoring ${statisticKey}, because the environment variable value is not a number.\n`);
      continue;
    }

    const needAutoScale = checkAutoScale(profileLimitUsed[statisticKey], profileLimit[statisticKey], scale);

    if (!needAutoScale) {
      continue;
    }

    const nextTier = getNextTier(prices[statisticKey], accountLimit[statisticKey]?.limit);

    if (nextTier) {
      autoScaleServices[statisticKey] = { limit: nextTier };
    }
  }

  if (!Object.keys(autoScaleServices).length) {
    return null;
  }

  return autoScaleServices;
}

function reallocateProfiles(
  accountLimit: Record<string, { limit: number }>,
  autoScaleServices: Record<string, { limit: number }>,
  profileAllocation: Record<string, number>
): Record<string, number> | null {
  const newAllocation: Record<string, number> = {};

  for (const service in autoScaleServices) {
    const newAccountLimit = autoScaleServices?.[service]?.limit || 0;
    const oldAccountLimit = accountLimit?.[service]?.limit || 0;

    const difference = newAccountLimit - oldAccountLimit;

    if (Number.isNaN(difference) || difference <= 0) {
      continue;
    }

    const currentAllocation = profileAllocation?.[service] || 0;

    newAllocation[service] = difference + currentAllocation;
  }

  if (!Object.keys(newAllocation).length) {
    return null;
  }

  return newAllocation;
}

/**
 * Get the environment variables and parses it to a JSON
 */
function setupEnvironment(context: TagoContext): AnalysisEnvironment {
  const environment = Utils.envToJson(context.environment) as AnalysisEnvironment;
  if (!environment) {
    throw new Error("Environment variables not found");
  }

  if (!environment.account_token || environment.account_token.length !== 36) {
    throw new Error("[ERROR] You must enter a valid account_token in the environment variable");
  }

  return environment;
}

// This function will run when you execute your analysis
async function startAnalysis(context: TagoContext): Promise<void> {
  const environment = setupEnvironment(context);

  // Setup the account and get's the ID of the profile the account token belongs to.
  const account = new Account({ token: environment.account_token });
  const id = await getProfileIDByToken(account, environment.account_token);
  if (!id) {
    throw new Error("Profile not found for the account token in the environment variable");
  }

  // Get the current subscriptions of our account for all the services.
  const { services: servicesLimit } = await account.billing.getSubscription();
  const accountLimit = getAccountLimit(servicesLimit);

  // get current limit and used resources of the profile.
  const { limit, limit_used } = await account.profiles.summary(id);

  // get the tiers of all services, so we know the next tier for our limits.
  const billingPrices: BillingPrices = await account.billing.getPrices();

  // Transform billing data to the format expected by calculateAutoScale
  const billing: Record<string, { amount: number }[]> = {};
  if (billingPrices && typeof billingPrices === "object") {
    Object.entries(billingPrices).forEach(([key, value]) => {
      if (Array.isArray(value)) {
        billing[key] = value.map((item: unknown) => ({
          amount:
            typeof item === "object" && item !== null && "price" in item
              ? (item as { price: number }).price
              : typeof item === "object" && item !== null && "amount" in item
                ? (item as { amount: number }).amount
                : 0,
        }));
      }
    });
  }

  // Check each service to see if it needs scaling
  // Extract the limits from the ProfileLimit objects
  const profileLimits =
    (limit as { limits?: Record<string, number> })?.limits || (limit as unknown as Record<string, number>);
  const profileLimitsUsed =
    (limit_used as { limits?: Record<string, number> })?.limits || (limit_used as unknown as Record<string, number>);

  const autoScaleServices = calculateAutoScale(billing, profileLimits, profileLimitsUsed, accountLimit, environment);

  // Stop if no auto-scale needed
  if (!autoScaleServices) {
    console.info("Services are okay, no auto-scaling needed.");
    return;
  }

  console.info("Auto-scaling the services:");
  for (const service in autoScaleServices) {
    console.info(`${service} from ${accountLimit?.[service]?.limit} to ${autoScaleServices?.[service]?.limit}`);
  }

  // Update our subscription, so we are actually scaling the account.
  try {
    await account.billing.editSubscription({
      services: autoScaleServices,
    });
  } catch (error) {
    console.error("Failed to update subscription:", error);
    return;
  }

  // Stop here if account has only one profile. No need to reallocate resources
  const profiles = await account.profiles.list();
  if (profiles.length > 1) {
    // Wait purchase to be completed
    await new Promise((resolve) => {
      setTimeout(resolve, 2000);
    });

    // Make sure we reallocate only what we just subscribed
    const amountToReallocate = reallocateProfiles(accountLimit, autoScaleServices, profileLimits);

    console.info("New allocation:");
    if (amountToReallocate) {
      for (const service in amountToReallocate) {
        console.info(`${service} from ${profileLimits?.[service]} to ${amountToReallocate?.[service]}`);
      }

      // Allocate all the subscribed limit to the profile.
      await account.billing.editAllocation([
        {
          profile: id,
          ...amountToReallocate,
        },
      ]);
    }
  }
}

Analysis.use(startAnalysis);

Average, Minimum and Maximum

datacalculationaverageminmaxstatistics

Calculate minimum, maximum, and average values from device data

typescriptavg-min-max.ts
/*
 * Analysis Example
 * Minimum, maximum, and average
 *
 * Get the minimum, maximum, and the average value of the variable temperature from your device,
 * and save these values in new variables
 *
 * Instructions
 * To run this analysis you need to add a device token to the environment variables,
 * To do that, go to your device, then token and copy your token.
 * Go the the analysis, then environment variables,
 * type device_token on key, and paste your token on value
 */

import type { DataCreate, DataQuery, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Device, Utils } from "npm:@tago-io/sdk";

// The function myAnalysis will run when you execute your analysis
async function startAnalysis(context: TagoContext): Promise<void> {
  // reads the values from the environment and saves it in the variable env_vars
  const env_vars = Utils.envToJson(context.environment);
  if (!env_vars.device_token) {
    return context.log("Device token not found on environment parameters");
  }

  const device = new Device({ token: env_vars.device_token });

  // This is a filter to get the minimum value of the variable temperature in the last day
  const minFilter: DataQuery = {
    variables: "temperature",
    query: "min",
    start_date: "1 day",
  };

  // Now we use the filter for the device to get the data
  // check if the variable min has any value
  // if so, we crete a new object to send to TagoIO
  const [min] = await device.getData(minFilter);
  if (min) {
    const minValue: DataCreate = {
      variable: "temperature_minimum",
      value: min.value,
      unit: "F",
    };

    // Now we send the new object with the minimum value
    await device.sendData(minValue).then(() => context.log("Temperature Minimum Updated"));
  } else {
    context.log("Minimum value not found");
  }

  // This is a filter to get the maximum value of the variable temperature in the last day
  const maxFilter: DataQuery = {
    variables: "temperature",
    query: "max",
    start_date: "1 day",
  };

  const [max] = await device.getData(maxFilter);

  if (max) {
    const maxValue: DataCreate = {
      variable: "temperature_maximum",
      value: max.value,
      unit: "F",
    };

    await device.sendData(maxValue).then(() => context.log("Temperature Maximum Updated"));
  } else {
    context.log("Maximum value not found");
  }

  // This is a filter to get the last 1000 values of the variable temperature in the last day
  const avgFilter: DataQuery = {
    variables: "temperature",
    qty: 1000,
    start_date: "1 day",
  };

  const dataAvgArray = await device.getData(avgFilter);

  if (dataAvgArray.length) {
    let temperatureSum = dataAvgArray.reduce((previousValue, currentValue) => {
      return previousValue + Number(currentValue.value);
    }, 0);

    temperatureSum = temperatureSum / dataAvgArray.length;

    const avgValue: DataCreate = {
      variable: "temperature_average",
      value: temperatureSum,
      unit: "F",
    };

    await device.sendData(avgValue).then(() => context.log("Temperature Average Updated"));
  } else {
    context.log("No result found for the avg calculation");
  }
}

Analysis.use(startAnalysis);

// To run analysis on your machine (external)
// Analysis.use(myAnalysis, { token: "YOUR-TOKEN" });

AWS IoT Device Location

awsiotlocationintegrationtracking

AWS IoT Core Device Location service integration

typescriptaws-iot-device-location.ts
/*
 * TagoIO - Analysis Example
 * AWS IoT Device Location Integration
 *
 * This analysis demonstrates how to integrate with AWS IoT Core Device Location service
 * to estimate device location using GNSS, IP address, or WiFi access points data.
 *
 * Check out the SDK documentation on: https://js.sdk.tago.io
 *
 * Environment Variables needed:
 * - AWS_ACCESSKEYID: Your AWS access key ID
 * - AWS_SECRETACCESSKEY: Your AWS secret access key
 * - AWS_REGION: AWS region (e.g., us-east-1)
 * - DESIREABLE_ACCURACY_PERCENT: Desired accuracy percentage (e.g., 80)
 * - GNSS_SOLVER_VARIABLE: Variable name for GNSS data (default: gnss_solver)
 * - IP_ADDRESS_VARIABLE: Variable name for IP address data (default: ip_addresses)
 * - WIFI_ADDRESSES_VARIABLE: Variable name for WiFi addresses data (default: wifi_addresses)
 */

import { GetPositionEstimateCommand, IoTWirelessClient } from "npm:@aws-sdk/client-iot-wireless";
import type { Data, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Resources } from "npm:@tago-io/sdk";

interface EstimatedConfiguration {
  awsAccessKeyId: string;
  awsSecretAccessKey: string;
  awsRegion: string;
  desireableAccuracyPercent: string;
}

interface EstimatedLocationResponse {
  type: string;
  geometry: {
    type: string;
    coordinates: [number, number, number];
  };
  properties: {
    HorizontalAccuracy: number;
    VerticalAccuracy: number;
  };
}

interface AWSPayload {
  Timestamp: Date;
  Gnss?: {
    Payload: string;
  };
  Ip?: {
    IpAddress: string;
  };
  WiFiAccessPoints?: Array<{
    MacAddress: string;
    Rss: number;
  }>;
}

/**
 * Parse environment variables to get configuration
 */
function _getConfiguration(context: TagoContext): EstimatedConfiguration {
  const awsAccessKeyId = context.environment.find((x) => x.key === "AWS_ACCESSKEYID")?.value;
  const awsSecretAccessKey = context.environment.find((x) => x.key === "AWS_SECRETACCESSKEY")?.value;
  const awsRegion = context.environment.find((x) => x.key === "AWS_REGION")?.value;
  const desireableAccuracyPercent = context.environment.find((x) => x.key === "DESIREABLE_ACCURACY_PERCENT")?.value;

  if (!awsAccessKeyId) {
    throw new Error("Missing AWS_ACCESSKEYID in environment variables");
  }
  if (!awsSecretAccessKey) {
    throw new Error("Missing AWS_SECRETACCESSKEY in environment variables");
  }
  if (!awsRegion) {
    throw new Error("Missing AWS_REGION in environment variables");
  }
  if (!desireableAccuracyPercent) {
    throw new Error("Missing DESIREABLE_ACCURACY_PERCENT in environment variables");
  }

  return {
    awsAccessKeyId,
    awsSecretAccessKey,
    awsRegion,
    desireableAccuracyPercent,
  };
}

/**
 * Create AWS payload for position estimate command
 */
function _createAWSPayload(gnssValue?: string, ipAddress?: string, wifiAddresses?: Record<string, number>): AWSPayload {
  if (!gnssValue && !ipAddress && !wifiAddresses) {
    throw new Error("No data to create the payload");
  }

  let payload: AWSPayload = { Timestamp: new Date() };

  if (gnssValue) {
    payload = { ...payload, Gnss: { Payload: gnssValue } };
  }

  if (ipAddress) {
    payload = { ...payload, Ip: { IpAddress: ipAddress } };
  }

  if (wifiAddresses) {
    const wifiKeys = Object.keys(wifiAddresses);
    const wifiValues = Object.values(wifiAddresses);

    if (wifiKeys.length < 2) {
      throw new Error("Wifi Addresses must have at least 2 addresses");
    }

    payload = {
      ...payload,
      WiFiAccessPoints: [
        {
          MacAddress: wifiKeys[0],
          Rss: wifiValues[0],
        },
        {
          MacAddress: wifiKeys[1],
          Rss: wifiValues[1],
        },
      ],
    };
  }

  return payload;
}

/**
 * Extract estimated location from AWS response
 */
function _getEstimatedLocation(response: {
  GeoJsonPayload?: { transformToString?: () => string };
}): EstimatedLocationResponse {
  if (!response) {
    throw new Error("No response from AWS");
  }

  const estimatedLocation = JSON.parse(response.GeoJsonPayload?.transformToString?.() ?? "");

  if (!estimatedLocation) {
    throw new Error("No estimated location found");
  }

  return estimatedLocation;
}

/**
 * Create TagoIO data object from scope and estimated location
 */
function _createDataForDevice(
  scope: Data,
  desireableAccuracy: string,
  estimatedLocation: EstimatedLocationResponse
): Data {
  const [lng, lat] = estimatedLocation.geometry.coordinates;
  const horizontalAccuracy = estimatedLocation.properties?.HorizontalAccuracy;
  const verticalAccuracy = estimatedLocation.properties?.VerticalAccuracy;

  const accuracy =
    horizontalAccuracy >= parseFloat(desireableAccuracy) || verticalAccuracy >= parseFloat(desireableAccuracy);

  const dataReturn: Data = {
    variable: "estimated_location",
    value: lat + ";" + lng,
    location: {
      coordinates: [lng, lat],
      type: "Point",
    },
    metadata: {
      horizontalAccuracy,
      verticalAccuracy,
      color: accuracy ? "green" : "red",
    },
    group: scope.group,
    time: scope.time,
    device: scope.device,
    id: scope.id,
  };

  return dataReturn;
}

/**
 * Main analysis function for AWS IoT Device Location
 */
async function getEstimatedDeviceLocation(context: TagoContext, scope: Data[]): Promise<void> {
  console.log("Starting Analysis");

  let configuration: EstimatedConfiguration;
  try {
    configuration = _getConfiguration(context);
  } catch (error) {
    console.error((error as Error).message);
    return;
  }

  // Get variable names from environment or use defaults
  const gnssSolverVariable = context.environment.find((x) => x.key === "GNSS_SOLVER_VARIABLE")?.value || "gnss_solver";
  const ipAddressVariable = context.environment.find((x) => x.key === "IP_ADDRESS_VARIABLE")?.value || "ip_addresses";
  const wifiAdressesVariable =
    context.environment.find((x) => x.key === "WIFI_ADDRESSES_VARIABLE")?.value || "wifi_addresses";

  // Extract data from scope
  const gnssValue = scope.find((x) => x.variable === gnssSolverVariable)?.value as string;
  const ipAddressValue = scope.find((x) => x.variable === ipAddressVariable)?.value as string;
  const ipAddress = ipAddressValue?.split(";");
  const wifiAddresses = scope.find((x) => x.variable === wifiAdressesVariable)?.metadata as Record<string, number>;

  try {
    // Create payload for AWS position estimate
    const payload = _createAWSPayload(gnssValue, ipAddress?.[0], wifiAddresses);

    // Create AWS IoT Wireless client
    const client = new IoTWirelessClient({
      credentials: {
        accessKeyId: configuration.awsAccessKeyId,
        secretAccessKey: configuration.awsSecretAccessKey,
      },
      region: configuration.awsRegion,
    });

    // Send position estimate command
    const command = new GetPositionEstimateCommand(payload);
    const response = await client.send(command);

    // Extract estimated location from response
    const estimatedLocation = _getEstimatedLocation(response);

    // Send data to TagoIO device
    await Resources.devices.sendDeviceData(
      scope[0].device,
      _createDataForDevice(scope[0], configuration.desireableAccuracyPercent, estimatedLocation)
    );

    console.log("Analysis Finished");
  } catch (error) {
    console.error((error as Error).message);
  }
}

// Use analysis in production
Analysis.use(getEstimatedDeviceLocation);

Dynamic Last Value Configuration

configurationdynamiclast-valueparametersdisplay

Configuration parameters for dynamic last value displays

typescriptconfiguration-parameters-for-dynamic-last-value.ts
/*
 ** Analysis Example
 ** Configuration parameters for dynamic last value
 **
 ** Set the configurations parameters with the last value of a given variable,
 ** in this example it is the "temperature" variable
 **
 ** How to use:
 ** To analysis works, you need to add a new policy in your account. Steps to add a new policy:
 **  1 - Click the button "Add Policy" at this url: https://admin.tago.io/am;
 **  2 - In the Target selector, select the Analysis with the field set as "ID" and choose your Analysis in the list;
 **  3 - Click the "Click to add a new permission" element and select "Device" with the rule "Access" with the field as "Any";
 **  4 - To save your new Policy, click the save button in the bottom right corner;
 */

import type { ConfigurationParams, Data, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Resources } from "npm:@tago-io/sdk";

// set the timezone to show up on dashboard. TagoIO may handle ISOString automatically in a future update.
let timezone = "America/New_York";

const getParam = (params: ConfigurationParams[], key: string): ConfigurationParams =>
  params.find((x) => x.key === key) || { key, value: "-", sent: false };

async function applyDeviceCalculation({ id: deviceID, name }: { id: string; name: string }): Promise<void> {
  const deviceInfoText = `${name}(${deviceID})`;
  console.info(`Processing Device ${deviceInfoText}`);

  // Get the temperature variable inside the device bucket.
  // notice it will get the last record at the time the analysis is running.
  const dataResult = await Resources.devices.getDeviceData(deviceID, {
    variables: ["temperature"],
    query: "last_value",
  });
  if (!dataResult.length) {
    console.error(`No data found for ${deviceInfoText}`);
    return;
  }

  // Get configuration params list of the device
  const deviceParams = await Resources.devices.paramList(deviceID);

  // get the variable temperature from our dataResult array
  const temperature = dataResult.find((data) => data.variable === "temperature");
  if (temperature) {
    // get the config. parameter with key temperature
    const temperatureParam = getParam(deviceParams, "temperature");
    // get the config. parameter with key last_record_time
    const lastRecordParam = getParam(deviceParams, "last_record_time");

    // Format time using built-in Date methods instead of moment
    const timeString = new Date(temperature.time as unknown as string).toLocaleString("en-US", {
      timeZone: timezone,
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
      hour: "2-digit",
      minute: "2-digit",
      hour12: true,
    });

    // creates or edit the tempreature Param with the value of temperature.
    // creates or edit the last_record_time Param with the time of temperature.
    // Make sure to cast the value to STRING, otherwise you'll get an error.
    await Resources.devices.paramSet(deviceID, [
      { ...temperatureParam, value: String(temperature.value) },
      { ...lastRecordParam, value: timeString },
    ]);
  }
}

// Simple queue implementation to process devices with concurrency control
async function processDevicesWithQueue(
  devices: { id: string; name: string }[],
  concurrency: number = 5
): Promise<void> {
  const results: Promise<void>[] = [];
  let index = 0;

  async function processNext(): Promise<void> {
    if (index >= devices.length) return;

    const currentIndex = index++;
    const device = devices[currentIndex];

    await applyDeviceCalculation(device);

    // Process next device
    return processNext();
  }

  // Start initial batch of concurrent operations
  for (let i = 0; i < Math.min(concurrency, devices.length); i++) {
    results.push(processNext());
  }

  // Wait for all operations to complete
  await Promise.all(results);
}

// scope is not used for Schedule action.
async function startAnalysis(_context: TagoContext, _scope: Data[]): Promise<void> {
  // get timezone from the account
  const accountInfo = await Resources.account.info();
  if (accountInfo.timezone) {
    timezone = accountInfo.timezone;
  }

  // fetch device list filtered by tags.
  // Device list always return an Array with DeviceInfo object.
  const deviceList = await Resources.devices.list({
    amount: 500,
    fields: ["id", "name", "tags"],
    filter: {
      tags: [{ key: "type", value: "sensor" }],
    },
  });

  // Process devices with concurrency control (5 devices simultaneously)
  await processDevicesWithQueue(deviceList, 5);

  console.log("Finished processing all devices");
}

Analysis.use(startAnalysis);

Console Hello World

basicconsolehello-world

Hello World example with console output

typescriptconsole.ts
/*
 * TagoIO - Analysis Example
 * Hello World
 *
 * Check out the SDK documentation on: https://js.sdk.tago.io
 *
 * Learn how to send messages to the console located on the TagoIO analysis screen.
 * You can use this principle to show any information during and after development.
 */

import { Analysis } from "npm:@tago-io/sdk";

// The function myAnalysis will run when you execute your analysis
function myAnalysis() {
  console.log("Hello World");
}

Analysis.use(myAnalysis);

// To run analysis on your machine (external)
// Analysis.use(myAnalysis, { token: "YOUR-TOKEN" });

Create Device from Dashboard

devicecreatedashboarddynamicmanagement

Create new devices dynamically using dashboard interface

typescriptcreate-device.ts
/*
 * Example: Creating Devices via Dashboard
 * This example demonstrates how to create devices in your account using an Input Widget on the dashboard.
 *
 * Dashboard Template:
 * You can access the dashboard template needed for this operation here: https://admin.tago.io/template/6143555a314cef001871ec78
 * It's recommended to use a dummy HTTPS device alongside the dashboard for testing purposes.
 *
 * Usage Instructions:
 * For the analysis to function correctly, you must add a new policy to your account by following these steps:
 *  1. Navigate to https://admin.tago.io/am and click on the "Add Policy" button.
 *  2. In the Target selector, ensure the field is set to "ID", then select your Analysis from the list.
 *  3. Click on the "Click to add a new permission" option, choose "Device" as the type, and set the rule to "Access" with the scope as "Any".
 *  4. Finalize by clicking the save button located in the bottom right corner to apply your new Policy.
 */

import type { Data, DeviceCreateInfo, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Resources } from "npm:@tago-io/sdk";

async function startAnalysis(_context: TagoContext, scope: Data[]): Promise<void> {
  if (!scope[0]) {
    return console.log("The analysis must be triggered by a widget.");
  }

  console.log("Creating your device");

  // Get the variables sent by the widget/dashboard.
  const network_id = scope.find((x) => x.variable === "device_network");
  const connector_id = scope.find((x) => x.variable === "device_connector");
  const device_name = scope.find((x) => x.variable === "device_name");
  const device_eui = scope.find((x) => x.variable === "device_eui");

  if (!connector_id || !connector_id.value) {
    return console.log('Missing "device_connector" in the data scope.');
  } else if (!network_id || !network_id.value) {
    return console.log('Missing "device_network" in the data scope.');
  } else if (!device_eui || !device_eui.value) {
    return console.log('Missing "device_eui" in the data scope.');
  } else if (!device_name || !device_name.value) {
    return console.log('Missing "device_name" in the data scope.');
  }

  const deviceID = scope[0]?.device;
  if (!deviceID) {
    return console.log("Device ID not found in the data scope");
  }

  const deviceCreateInfo: DeviceCreateInfo = {
    name: device_name.value as string,
    // Serie number is the parameter for device eui, sigfox id, etc..
    serie_number: device_eui.value as string,
    tags: [
      // You can add custom tags here.
      { key: "type", value: "sensor" },
      { key: "device_eui", value: device_eui.value as string },
    ],
    connector: connector_id.value as string,
    network: network_id.value as string,
    active: true,
    type: "immutable",
    chunk_period: "month", //consider change
    chunk_retention: 1, //consider change
  };

  const result = await Resources.devices.create(deviceCreateInfo).catch((error) => {
    // Send the validation to the device.
    // That way we create an error in the dashboard for feedback.
    Resources.devices.sendDeviceData(deviceID, {
      variable: "validation",
      value: `Error when creating the device ${error}`,
      metadata: { color: "red" },
    });
    throw error;
  });

  // To add Configuration Parameters to the device:
  await Resources.devices.paramSet(result.device_id, {
    key: "param_key",
    value: "10",
    sent: false,
  });

  // Send feedback to the dashboard:
  await Resources.devices.sendDeviceData(deviceID, {
    variable: "validation",
    value: "Device succesfully created!",
    metadata: { type: "success" },
  });
  console.log(`Device succesfully created. ID: ${result.device_id}`);
}

Analysis.use(startAnalysis);

Device Data Amount Report

dataamountreportusageanalytics

Get top 20 devices with highest data amount usage

typescriptdata-amount.ts
/**
 * TagoIO - Analysis Example
 * Device Data Amount Analysis
 *
 * This analysis retrieves the amount of data for each device and logs into the console
 * the top 20 devices with the highest data amount.
 *
 * Requirements:
 * - Access Policy must have permission to list devices (Device -> Access)
 * - Access Policy must have permission to get device data (Device -> Get Data)
 *
 * Check out the SDK documentation on: https://js.sdk.tago.io
 * Create Access Policy at https://admin.tago.io/am
 *
 */

import type { Data, DeviceListItem, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Resources } from "npm:@tago-io/sdk";
import { queue } from "npm:async";

interface DeviceResult {
  name: string;
  id: string;
  amount: number;
}

/**
 * This is the main function that will be called when the analysis is executed
 */
async function myAnalysis(_context: TagoContext, _scope: Data[]): Promise<void> {
  const resultList: DeviceResult[] = [];

  const getDeviceAmount = async (deviceObj: DeviceListItem): Promise<void> => {
    const result = await Resources.devices.amount(deviceObj.id).catch(console.log);
    if (!result) {
      // 0 data or error
      return;
    }

    // Any code that you want to run for each device before pushing to the resultList
    // Example to not include devices with less than 40,000 data points
    // if (result < 40000) {
    //   return;
    // }

    resultList.push({ name: deviceObj.name, id: deviceObj.id, amount: result });
    await new Promise((resolve) => setTimeout(resolve, 200)); // sleep
  };

  const filter = {
    // type: "mutable"
    // type: "immutable"
    // tags: [{ key: "my_tag_key", value: "my_tag_value" }]
  };

  // Create a queue to limit the amount of devices being processed at the same time
  const amountQueue = queue(getDeviceAmount, 5);
  amountQueue.error((error: unknown) => console.log(error));

  const deviceList = Resources.devices.listStreaming({ filter });
  for await (const device of deviceList) {
    void amountQueue.push(device);
  }

  // stop if queue is empty
  if (amountQueue.idle() && resultList.length === 0) {
    console.error("No devices found to process");
    return;
  }

  // periodically console the amount of devices still in the queue
  const queueMonitor = setInterval(() => {
    console.log(`Devices in queue: ${amountQueue.length()}`);
  }, 10000);

  // wait for all devices to be processed
  await amountQueue.drain();

  // Clear the monitoring interval
  clearInterval(queueMonitor);

  // Reorder resultList by the highest data amount
  resultList.sort((a, b) => b.amount - a.amount);

  // Log the top 20 devices
  for (const result of resultList) {
    console.log(JSON.stringify(result));
  }
}

Analysis.use(myAnalysis);

Data Retention Management

dataretentioncleanupmanagementstorage

Implement custom data retention policies for device data

typescriptdata-retention.ts
/*
 * Analysis Example
 * Custom Data Retention
 *
 * Get the list of devices, then go to each device removing the variables you chooses.
 *
 ** How to use:
 ** To analysis works, you need to add a new policy in your account. Steps to add a new policy:
 **  1 - Click the button "Add Policy" at this url: https://admin.tago.io/am;
 **  2 - In the Target selector, select the Analysis with the field set as "ID" and choose your Analysis in the list;
 **  3 - Click the "Click to add a new permission" element and select "Device" with the rule "Access" with the field as "Any";
 **  4 - To save your new Policy, click the save button in the bottom right corner;
 */

import { Analysis, type DeviceQuery, Resources, type TagoContext } from "npm:@tago-io/sdk";
import dayjs from "npm:dayjs";

// The function startAnalysis will run when you execute your analysis
async function startAnalysis(context: TagoContext): Promise<void> {
  // Bellow is an empty filter.
  // Examples of filter:
  // { tags: [{ key: 'tag-key', value: 'tag-value' }]}
  // { name: 'name*' }
  // { name: '*name' }
  // { bucket: 'bucket-id' }
  const filter = {};

  const deviceQuery: DeviceQuery = {
    page: 1,
    fields: ["id"],
    filter,
    amount: 100,
  };

  const devices = await Resources.devices.list(deviceQuery);

  for (const deviceObj of devices) {
    const variables = ["variable1", "variable2"];
    const qty = 100; // remove 100 registers of each variable
    const end_date = dayjs().subtract(1, "month").toISOString(); // registers old than 1 month

    const removeOptions = { variables, qty, end_date };

    await Resources.devices
      .deleteDeviceData(deviceObj.id!, removeOptions)
      .then((result) => context.log(result))
      .catch((error) => context.log(error));
  }
}

Analysis.use(startAnalysis);

// To run analysis on your machine (external)
// Analysis.use(myAnalysis, { token: "YOUR-TOKEN" });

Data Transaction Summary

datatransactionuserstatisticsbilling

Get total transaction count and statistics by user

typescriptdata-transaction.ts
/*
 ** Analysis Example
 ** Get users total transactions
 **
 ** This analysis must run by an Scheduled Action.
 ** It gets a total amount of transactions by device, calculating by the total amount of data in the bucket
 ** each time the analysis run. Group the result by a tag.
 **
 ** Environment Variables
 ** In order to use this analysis, you must setup the Environment Variable table.
 **
 ** device_token: Token of a device where the total transactions will be stored. Get this in the Device's page.
 ** account_token: Your account token. Check bellow how to get this.
 **
 ** Steps to generate an account_token:
 ** 1 - Enter the following link: https://admin.tago.io/account/
 ** 2 - Select your Profile.
 ** 3 - Enter Tokens tab.
 ** 4 - Generate a new Token with Expires Never.
 ** 5 - Press the Copy Button and place at the Environment Variables tab of this analysis.
 */

import type { Data, DeviceListItem, TagoContext } from "npm:@tago-io/sdk";
import { Account, Analysis, Device, Utils } from "npm:@tago-io/sdk";
import _ from "npm:lodash";

async function calculateUserTransactions(
  account: Account,
  storage: Device,
  user_value: string,
  device_list: DeviceListItem[]
): Promise<void> {
  // Collect the data amount for each device.
  // Result of bucket_results is:
  // [0, 120, 500, 0, 1000]
  const bucket_results = await Promise.all(device_list.map((device) => account.buckets.amount(device.bucket)));
  const total_transactions = _.sum(bucket_results);

  // Get the total transactions of the last analysis run.
  // Group is used to get only for this user.
  // You can change that to get a specific device for the user, instead of using a global storage device.
  // One way to do that is by just finding the device using a tag, see example:
  //
  // const [user_device] = await account.devices.list({ page: 1, fields: ['id', 'name', 'bucket', 'tags'], filter: { tags: [{ key: 'user_device', value: user_value }] }, amount: 1 });
  // const device_token = await Utils.getTokenByName(account, user_device.id);
  // const storage = new Device({ token: device_token });

  let [last_total_transactions] = await storage.getData({
    variables: ["last_transactions"],
    qty: 1,
    groups: user_value,
  });
  if (!last_total_transactions) {
    last_total_transactions = { value: 0, time: new Date() } as Data;
  }

  const result = total_transactions - (last_total_transactions.value as number);

  // Store the current total of transactions, the result for this analysis run and the key.
  // Now you can just plot these variables in a dynamic table.
  await storage.sendData([
    {
      variable: "last_transactions",
      value: total_transactions,
      group: user_value,
    },
    { variable: "transactions_result", value: result, group: user_value },
    { variable: "user", value: user_value, group: user_value },
  ]);
}

async function myAnalysis(context: TagoContext): Promise<void> {
  // Transform all Environment Variable to JSON.
  const environment = Utils.envToJson(context.environment);
  if (!environment.account_token) {
    return console.log("You must setup an account_token in the Environment Variables.");
  } else if (!environment.device_token) {
    return console.log("You must setup an device_token in the Environment Variables.");
  }
  // Instance the account class
  const account = new Account({ token: environment.account_token });
  const storage = new Device({ token: environment.device_token });

  // Setup the tag we will be searching in the device list
  const tag_to_search = "user_email";

  // Get the device_list and group it by the tag value.
  // Result of grouped_device_list is:
  // [
  //   { value: 'test@tago.io', device_list: [ [Object], [Object] ] },
  //   { value: 'user@tago.io', device_list: [ [Object] ] }
  // ]
  const device_list = await account.devices.list({
    page: 1,
    fields: ["id", "name", "bucket", "tags"],
    filter: { tags: [{ key: tag_to_search }] },
    amount: 10000,
  });

  const grouped_device_list = _.chain(device_list)
    .groupBy(
      (collection: DeviceListItem) =>
        collection.tags?.find((x: { key: string; value: string }) => x.key === tag_to_search)?.value
    )
    .map((value: DeviceListItem[], key: string) => ({ value: key, device_list: value }))
    .value();

  // Call a new function for each group in assynchronous way.
  await Promise.all(
    grouped_device_list.map((group: { value: string; device_list: DeviceListItem[] }) =>
      calculateUserTransactions(account, storage, group.value.replace(/ /g, ""), group.device_list)
    )
  );
}

Analysis.use(myAnalysis);

// To run analysis on your machine (external)
// Analysis.use(myAnalysis, { token: "YOUR-TOKEN" });

Get Device List

devicesapilistfiltering

Retrieve and filter device list from your account

typescriptdevice-list.ts
/*
 ** Analysis Example
 ** Get Device List
 **
 ** This analysis retrieves a list of devices from your account and prints it to the console.
 **
 ** How to use:
 ** To analysis works, you need to add a new policy in your account. Steps to add a new policy:
 **  1 - Click the button "Add Policy" at this url: https://admin.tago.io/am;
 **  2 - In the Target selector, select the Analysis with the field set as "ID" and choose your Analysis in the list;
 **  3 - Click the "Click to add a new permission" element and select "Device" with the rule "Access" with the field as "Any";
 **  4 - To save your new Policy, click the save button in the bottom right corner;
 */

import type { DeviceQuery, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Resources } from "npm:@tago-io/sdk";

async function startAnalysis(_context: TagoContext): Promise<void> {
  // Example of filtering devices by tag.
  // to use this filter, just remove the comment on the line 35
  // const filter = {
  //   tags: [
  //     {
  //       key: "key_name", // change by your key name
  //       value: "key_value", // change by your key value
  //     },
  //   ],
  //   // You also can filter by: name, last_input, last_output, bucket, etc.
  // };

  // Searching all devices with tag we want
  const query: DeviceQuery = {
    page: 1,
    fields: ["id", "tags"],
    // filter,
    amount: 100,
  };

  const devices = await Resources.devices.list(query);

  if (!devices.length) {
    return console.debug("Devices not found");
  }

  console.debug(JSON.stringify(devices));
}

Analysis.use(startAnalysis);

// To run analysis on your machine (external)
// Analysis.use(myAnalysis, { token: "YOUR-TOKEN" });

Device Offline Alert

deviceofflinealertmonitoringstatus

Monitor devices and send alerts when they go offline

typescriptdevice-offline.ts
/*
 ** Analysis Example
 ** Device Offline Alert
 **
 ** This analysis must run by Time Interval. It checks if devices with given Tags
 ** had communication in the past minutes. If not, it sends an email or sms alert.
 **
 ** Environment Variables
 ** In order to use this analysis, you must setup the Environment Variable table.
 **
 ** checkin_time: Minutes between the last input of the device before sending the notification.
 ** tag_key: Device tag Key to filter the devices.
 ** tag_value: Device tag Value to filter the devices.
 ** email_list: Email list comma separated.
 ** sms_list: Phone number list comma separated. The phone number must include the country code
 **
 ** How to use:
 ** To analysis works, you need to add a new policy in your account. Steps to add a new policy:
 **  1 - Click the button "Add Policy" at this url: https://admin.tago.io/am;
 **  2 - In the Target selector, with the field set as "ID", choose your Analysis in the list;
 **  3 - Click the "Click to add a new permission" element and select "Device" with the rule "Access" with the field as "Any";
 **  4 - To save your new Policy, click the save button in the bottom right corner;
 */

import type { DeviceQuery, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Resources, Services, Utils } from "npm:@tago-io/sdk";
import dayjs from "npm:dayjs";

async function startAnalysis(context: TagoContext): Promise<void> {
  // Transform all Environment Variable to JSON.
  const env = Utils.envToJson(context.environment);

  if (!env.checkin_time) {
    return context.log("You must setup a checkin_time in the Environment Variables.");
  } else if (!env.tag_key) {
    return context.log("You must setup a tag_key in the Environment Variables.");
  } else if (!env.tag_value) {
    return context.log("You must setup a tag_value in the Environment Variables.");
  } else if (!env.email_list && !env.sms_list) {
    return context.log("You must setup an email_list or a sms_list in the Environment Variables.");
  }

  const checkin_time = Number(env.checkin_time);
  if (Number.isNaN(checkin_time)) return context.log("The checkin_time must be a number.");

  // You can remove the comments on line 51 and 57 to use the Tag Filter.
  //const filter = { tags: [{ key: env.tag_key, value: env.tag_value }] };

  const deviceQuery: DeviceQuery = {
    page: 1,
    amount: 1000,
    fields: ["id", "name", "last_input"],
    // filter,
  };

  const devices = await Resources.devices.list(deviceQuery);

  if (!devices.length) {
    return context.log(`No device found with given tags. Key: ${env.tag_key}, Value: ${env.tag_value} `);
  }

  context.log("Checking devices: ", devices.map((x) => x.name).join(", "));

  const now = dayjs();
  const alert_devices: string[] = [];

  for (const device of devices) {
    if (!device.last_input) {
      continue;
    }
    const last_input = dayjs(new Date(device.last_input));

    // Check the difference in minutes.
    const diffInMinutes = now.diff(last_input, "minute");

    if (diffInMinutes > checkin_time) {
      alert_devices.push(device.name);
    }
  }

  if (!alert_devices.length) {
    return context.log("All devices are okay.");
  }

  context.log("Sending notifications");
  const emailService = new Services({ token: context.token }).email;
  const smsService = new Services({ token: context.token }).sms;

  let message = `Hi!\nYou're receiving this alert because the following devices didn't send data in the last ${checkin_time} minutes.\n\nDevices:\n`;
  message += alert_devices.join("\n");

  if (env.email_list) {
    // Remove space in the string
    const emails = env.email_list.replace(/ /g, "");

    const emailData = {
      to: emails,
      subject: "Device Offline Alert",
      message,
    };

    await emailService.send(emailData);
  }

  if (env.sms_list) {
    // Remove space in the string and convert to an Array.
    const smsNumbers = env.sms_list.replace(/ /g, "").split(",");

    for (const phone of smsNumbers) {
      const smsData = {
        to: phone,
        message,
      };

      await smsService.send(smsData);
    }
  }
}

Analysis.use(startAnalysis);

// To run analysis on your machine (external)
// Analysis.use(myAnalysis, { token: "YOUR-TOKEN" });

Dynamic Notifications

notificationdynamicemailsmspushconditional

Send dynamic email, SMS and push notifications based on conditions

typescriptdynamic-notification.ts
/*
 ** Notification Analysis Example
 ** Dynamically Sending Notifications
 **
 ** This script demonstrates how to send notifications via Email, SMS, and Push to TagoRUN Users using analysis.
 ** To execute this example, you must first set up an action by variable to trigger this analysis.
 ** Once the action meets your specified conditions, the corresponding data will be dispatched for analysis.
 **
 ** Usage Instructions:
 ** In order for this analysis to function correctly, a new policy must be added to your account. Here are the steps for adding a new policy:
 **  1 - Navigate to https://admin.tago.io/am and click on "Add Policy";
 **  2 - In the Target selector, locate "ID" under Analysis field and choose your desired Analysis from the list;
 **  3 - Click on "Click to add a new permission", select "Device", and set rule as "Access" with "Any" field;
 **  4 - Click on "Click to add a new permission" again, select "Service", and set rules as "Send Email" and "Send SMS";
 **  5 - Once more click on "Click to add a new permission", choose "Run User", set rule as "Create Notification" with field set as "Any";
 **  6 - To finalize your new Policy, hit the save button located in the bottom right corner of the screen.
 */

import type { Data, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Resources, Services } from "npm:@tago-io/sdk";

async function startAnalysis(context: TagoContext, scope: Data[]): Promise<void> {
  if (!scope[0]) {
    return context.log("This analysis must be triggered by an action.");
  }

  console.log("Analysis started");

  // Get the device ID from the scope and retrieve device information.
  const device_id = scope[0].device;
  if (!device_id) {
    return context.log("Device ID not found in scope");
  }

  const device_info = await Resources.devices.info(device_id);

  // Get the device name and tags from the device.
  // [TAG KEY]    [TAG VALUE]
  // email        example@tago.io
  // phone        +1XXxxxxxxx
  // user_id      5f495ae55ff03d0028d39fc5
  //
  // This is just a generic example how to get this information. You can get data from a device, search in tags, or any other way of correlation you have.
  // For example, you can get the email directly from the user_id if it was specified:
  // const { email } = await account.run.userInfo(userID_tag.id);
  const device_name = device_info.name;
  const email_tag = device_info.tags?.find((tag) => tag.key === "email");
  const phone_tag = device_info.tags?.find((tag) => tag.key === "phone");
  const userID_tag = device_info.tags?.find((tag) => tag.key === "user_id");

  // Instance the SMS and Email service using the analysis token from the context.
  const email_service = new Services({ token: context.token }).email;
  const sms_service = new Services({ token: context.token }).sms;

  // Send the notifications and output the results to the analysis console.
  if (email_tag) {
    const emailData = {
      to: email_tag.value,
      subject: "Notification alert",
      message: `You received a notification for the device: ${device_name}. Variable: ${scope[0].variable}, Value: ${scope[0].value}`,
    };

    await email_service
      .send(emailData)
      .then((result) => console.log(result))
      .catch((error) => console.log(error));
  } else {
    console.log("Email not found for this device.");
  }

  if (phone_tag) {
    const smsData = {
      to: phone_tag.value,
      message: `You received a notification for the device: ${device_name}. Variable: ${scope[0].variable}, Value: ${scope[0].value}`,
    };

    await sms_service
      .send(smsData)
      .then((result) => console.log(result))
      .catch((error) => console.log(error));
  } else {
    console.log("Phone number not found for this device.");
  }

  if (userID_tag) {
    const notificationData = {
      title: "Notification Alert",
      message: `You received a notification for the device: ${device_name}. Variable: ${scope[0].variable}, Value: ${scope[0].value}`,
    };

    await Resources.run
      .notificationCreate(userID_tag.value, notificationData)
      .then((result) => console.log(result))
      .catch((error) => console.log(error));
  } else {
    console.log("User ID not found for this device.");
  }

  console.log("Script end.");
}

Analysis.use(startAnalysis);

Email Export

emailexportdataattachmentcsv

Export device data and send via email attachment

typescriptemail-export.ts
/*
 * Analysis Example
 * Email export
 *
 * Learn how to send an email with data in a .csv file attachment.
 *
 * This analysis will read the variable fuel_level from your device,
 * and send the values in a .csv file to an e-mail address
 *
 * Instructions
 * To run this analysis you need to add a device token and the e-mail to the environment variables.
 * To do that, go to your device, then token and copy your token.
 * Go the the analysis, then environment variables,
 * type device_token on key, and paste your token on value
 * click the + button to add a new environment
 * on key, type email and on value, type the e-mail address
 */

import type { DataQuery, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Device, Services, Utils } from "npm:@tago-io/sdk";

// The function myAnalysis will run when you execute your analysis
async function startAnalysis(context: TagoContext): Promise<void> {
  // reads the values from the environment and saves it in the variable env_vars
  const env_vars = Utils.envToJson(context.environment);
  if (!env_vars.device_token) {
    return context.log("device_token environment variable not found");
  }

  if (!env_vars.email) {
    return context.log("email environment variable not found");
  }

  const device = new Device({ token: env_vars.device_token });

  // Get the 5 last records of the variable fuel_level in the device bucket.
  const query: DataQuery = { variables: "fuel_level", qty: 5 };
  const fuel_list = await device.getData(query);

  // Create csv header
  let csv = "Fuel Level";

  // For each record in the fuel_list, add the value in the csv text.
  // Use \n to break the line.
  for (const item of fuel_list) {
    csv = `${csv},\n${item.value}`;
  }

  // Print the csv text to the TagoIO analysis console, as a preview
  context.log(csv);

  // Start the email service
  const email = new Services({ token: context.token }).email;

  // Send the email.
  const emailData = {
    message: "This is an example of a body message",
    subject: "Exported File from TagoIO",
    to: env_vars.email,
    attachment: {
      archive: csv,
      filename: "exported_file.csv",
    },
  };

  const service_response = await email.send(emailData);

  context.log(service_response);
}

Analysis.use(startAnalysis);

// To run analysis on your machine (external)
// Analysis.use(myAnalysis, { token: "YOUR-TOKEN" });

Find and Operate Data

datafindfilteroperationdevice

Find and operate data from devices using filtering and manipulation

typescriptfind.ts
/*
 * Analysis Example
 * Operate data from devices
 *
 * Read information from a variable generated by devices,
 * run a simple calculation in real-time, and create a new variable with the output.
 *
 * Instructions
 * To run this analysis you need to add a device token to the environment variables,
 * To do that, go to your device, then token and copy your token.
 * Go the the analysis, then environment variables,
 * type device_token on key, and paste your token on value
 */

import type { DataQuery, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Device, Utils } from "npm:@tago-io/sdk";

// The function startAnalysis will run when you execute your analysis
async function startAnalysis(context: TagoContext): Promise<void> {
  // reads the values from the environment and saves it in the variable env_vars
  const env_vars = Utils.envToJson(context.environment);

  if (!env_vars.device_token) {
    return context.log("Missing device_token environment variable");
  }

  const device = new Device({ token: env_vars.device_token });

  // create the filter options to get the data from TagoIO
  const filter: DataQuery = {
    variables: "water_level",
    query: "last_item",
  };

  const resultArray = await device.getData(filter).catch(() => null);

  // Check if the array is not empty
  if (!resultArray || !resultArray[0]) {
    return context.log("Empty Array");
  }

  // query:last_item always returns only one value
  const value = resultArray[0].value;
  const time = resultArray[0].time;

  // print to the console at TagoIO
  context.log(`The last record of the water_level is ${value}. It was inserted at ${time}`);

  // Multiplies the water_level value by 2 and inserts it in another variable
  const obj_to_save = {
    variable: "water_level_double",
    value: (value as number) * 2,
  };

  try {
    await device.sendData(obj_to_save);
    context.log("Successfully Inserted");
  } catch (error) {
    context.log("Error when inserting:", error);
  }
}

Analysis.use(startAnalysis);

// To run analysis on your machine (external)
// Analysis.use(myAnalysis, { token: "YOUR-TOKEN" });

Generate PDF Report

pdfreportemailadvanced

Generate PDF report and send via email

typescriptgenerate-pdf-report.ts
/*
 * Analysis Example
 * Generate pdf report and send via email
 *
 *
 * Instructions
 * To run this analysis you need to add a email and device_token to the environment variables,
 * Go the the analysis, then environment variables,
 * type email on key, and insert your email on value
 * type device_token on key and insert your device token on value
 */

import type { AnalysisEnvironment, Data, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Device, Services, Utils } from "npm:@tago-io/sdk";
import dayjs from "npm:dayjs";

const your_variable = "your_variable"; //enter the variable from your device you would like in the report

// The function myAnalysis will run when you execute your analysis
async function startAnalysis(context: TagoContext, _scope: Data[]): Promise<void> {
  // reads the values from the environment and saves it in the variable envVars
  const envVars = Utils.envToJson(context.environment) as AnalysisEnvironment;

  if (!envVars.email) {
    return context.log("email environment variable not found");
  }
  if (!envVars.device_token) {
    return context.log("device_token environment variable not found");
  }

  const device = new Device({ token: envVars.device_token });

  const data = await device.getData({
    variables: [your_variable],
    start_date: "1 month",
    qty: 10,
  });

  let dataParsed = "variable,value,unit,time";

  data.forEach((x) => {
    dataParsed = `${x.variable},${x.value},${x.unit},${x.time}`;
  });

  const dataArray = dataParsed.split(",");
  const dataVar = dataArray[0];
  const dataVal = dataArray[1];

  const html = `<html>
    <head>
        <style>
            body, html {
                margin: 0;
            }
            table {
                width: 100%;
                border-collapse: collapse;
            }
            td {
                border: 1px solid black;
                padding: 5px;
                padding-bottom: 25px;
                font-style: italic;
            }
        </style>
    </head>
    <body>
      <table>
        <tr>
            <td colspan="7">Issue date: ${dayjs().format("YYYY-MM-DD HH:mm:ss")}</td>
        </tr>
        <tr>
            <td colspan="4">Start date: 2020-05-20 10:21:32</td>
            <td colspan="3">Stop date: 2020-10-08 22:56:19</td>
        </tr>
        <tr>
            <td colspan="4"> Report of the ${dataVar}</td>
            <td colspan="3">Device Kitchen Oven 5</td>
        </tr>
        <tr>
            <td>Counter</td>
            <td>${dataVar}</td>
            <td>Time</td>
            <td>Date</td>
            <td>Temperature 2</td>
            <td>Time</td>
            <td>Date</td>
        </tr>
        <tr>
          <td>2</td>
          <td>${dataVal}</td>
          <td>10:53:20</td>
          <td>2020-06-10</td>
          <td>137</td>
          <td>10:53:20</td>
          <td>2020-06-10</td>
        </tr>
      </table>
    </body>
  </html>`;

  const options = {
    displayHeaderFooter: true,
    footerTemplate:
      '<div class="page-footer" style="width:100%; text-align:center; font-size:12px;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
    margin: {
      top: "1.5cm",
      right: "1.5cm",
      left: "1.5cm",
      bottom: "1.5cm",
    },
  };

  // Convert HTML to base64 using built-in functions
  const encoder = new TextEncoder();
  const htmlBytes = encoder.encode(html);
  const base64 = btoa(String.fromCharCode(...htmlBytes));

  // start the PDF service
  const pdfService = new Services({ token: context.token }).PDF;
  const pdf_base64 = await pdfService.generate({
    base64,
    options,
  });

  // Start the email service
  const emailService = new Services({ token: context.token }).email;

  // Send the email.
  await emailService.send({
    to: envVars.email,
    subject: "Exported File from TagoIO",
    message: "This is an example of a body message",
    attachment: {
      archive: pdf_base64.result,
      type: "base64",
      filename: "exportedfile.pdf",
    },
  });
}

Analysis.use(startAnalysis);

Geofence Trigger Alert

geofencelocationalerttrigger

Monitor device location and trigger alerts when entering/leaving geofenced areas

typescriptgeofence.ts
/*
 * Environment Variables
 * In order to use this analysis, you must setup the Environment Variable table.
 * account_token: Your account token. Check the steps below.
 *
 * Steps to generate an account_token:
 * 1 - Enter the following link: https://admin.tago.io/account/
 * 2 - Select your Profile.
 * 3 - Enter Tokens tab.
 * 4 - Generate a new Token with Expires Never.
 * 5 - Press the Copy Button and place at the Environment Variables tab of this analysis with key account_token.
 *
 * Follow this guide https://docs.tago.io/en/articles/151 and create
 * two geofences, one with the event code 'danger' and another named 'safe'.
 */

import type { AnalysisEnvironment, Data, TagoContext } from "npm:@tago-io/sdk";
import { Account, Analysis, Device, Services, Utils } from "npm:@tago-io/sdk";

interface Point {
  latitude: number;
  longitude: number;
}

interface GeofenceLocation {
  type: "Polygon" | "Point";
  coordinates: number[][] | number[];
  radius?: number;
}

interface Geofence {
  event: string;
  geolocation: GeofenceLocation;
  [key: string]: unknown;
}

// This function checks if our device is inside a polygon geofence
function insidePolygon(point: number[], geofence: number[][]): boolean {
  const x = point[1];
  const y = point[0];
  let inside = false;
  for (let i = 0, j = geofence.length - 1; i < geofence.length; j = i++) {
    const xi = geofence[i][0];
    const yi = geofence[i][1];
    const xj = geofence[j][0];
    const yj = geofence[j][1];
    const intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
    if (intersect) inside = !inside;
  }
  return inside;
}

// Simple point-in-circle calculation to replace geolib dependency
function isPointWithinRadius(point: Point, center: Point, radius: number): boolean {
  const R = 6371000; // Earth's radius in meters
  const dLat = (center.latitude - point.latitude) * (Math.PI / 180);
  const dLon = (center.longitude - point.longitude) * (Math.PI / 180);
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(point.latitude * (Math.PI / 180)) *
      Math.cos(center.latitude * (Math.PI / 180)) *
      Math.sin(dLon / 2) *
      Math.sin(dLon / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  const distance = R * c;
  return distance <= radius;
}

// This function checks if our device is inside any geofence
function checkZones(point: number[], geofence_list: Geofence[]): Geofence | undefined {
  // The line below gets all Polygon geofences that we may have.
  const polygons = geofence_list.filter((x) => x.geolocation.type === "Polygon");
  if (polygons.length) {
    // Here we check if our device is inside any Polygon geofence using our function above.
    const pass_check = polygons.map((x) => insidePolygon(point, x.geolocation.coordinates as number[][]));
    const index = pass_check.findIndex((x) => x === true);
    if (index !== -1) return polygons[index];
  }
  // The line below gets all Point (circle) geofences that we may have.
  const circles = geofence_list.filter((x) => x.geolocation.type === "Point");
  if (circles.length) {
    // Here we check if our device is inside any Point geofence using our built-in function.
    const pass_check = circles.map((x) =>
      isPointWithinRadius(
        { latitude: point[1], longitude: point[0] },
        {
          latitude: x.geolocation.coordinates[0] as number,
          longitude: x.geolocation.coordinates[1] as number,
        },
        x.geolocation.radius || 100
      )
    );
    const index = pass_check.findIndex((x) => x);
    if (index !== -1) return circles[index];
  }
  return;
}

// This function help us get the device using just its id.
async function getDevice(account: Account, device_id: string): Promise<Device> {
  const customer_token = await Utils.getTokenByName(account, device_id);
  const customer_dev = new Device({ token: customer_token as string });
  return customer_dev;
}

async function startAnalysis(context: TagoContext, scope: Data[]): Promise<void> {
  context.log("Running");

  if (!scope[0]) {
    throw new Error("Scope is missing"); // doesn't need to run if scope[0] is null
  }

  // The code block below gets all environment variables and checks if we have the needed ones.
  const environment = Utils.envToJson(context.environment) as AnalysisEnvironment;
  if (!environment.account_token) {
    throw new Error("Missing account_token environment var");
  }

  const account = new Account({ token: environment.account_token });
  const device_id = scope[0].device;

  if (!device_id) {
    throw new Error("Device ID not found in scope");
  }

  // Here we get the device information using our account data and the device id.
  const device = await getDevice(account, device_id);
  // This checks if we received a location
  const location = scope.find((data) => data.variable === "location");
  if (!location || !location.location) return context.log("No location found in the scope.");

  // Now we check if we have any geofences to go through.
  const geofences = await device.getData({ variables: "geofence", qty: 10 });
  const zones: Geofence[] = geofences.map((geofence) => geofence.metadata as Geofence);
  const zone = checkZones([location.location.coordinates[1], location.location.coordinates[0]], zones);

  // The line below starts our notification service.
  const notification = new Services({ token: context.token }).Notification;

  if (!zone) {
    // If no geofence is found, we stop our application sending a notification.
    await notification.send({
      title: "No zone alert",
      message: "Your device is not inside any zone.",
    });
    context.log("Your device is not inside any zone.");
    return;
  }

  if (zone.event === "danger") {
    // If our device is inside a danger geofence, we will send a notification with a danger alert.
    await notification.send({
      title: "Danger alert",
      message: "Your device is inside a dangerous zone.",
    });
  }
  if (zone.event === "safe") {
    // If our device is inside a safe geofence, we will send a safe geofence notification.
    await notification.send({
      title: "Safe alert",
      message: "Your device is inside a safe zone.",
    });
  }
  context.log(zone.event);
}

Analysis.use(startAnalysis);

HTTP GET Request

httpgetapirequestexternal

Make HTTP GET requests to external APIs and routes

typescripthttp-get.ts
/*
 ** Analysis Example
 ** HTTP GET Request
 **
 ** This analysis makes a simple GET request to an HTTP route. It's a starting example for you to develop more
 ** complex algorithms.
 ** In this example we get the Account name and print to the console.
 **
 **.
 */

import type { TagoContext } from "npm:@tago-io/sdk";
import { Analysis } from "npm:@tago-io/sdk";

interface ApiResponse {
  result: {
    name: string;
  };
}

async function startAnalysis(context: TagoContext): Promise<void> {
  const url = "https://api.tago.io/info";
  const headers = {
    Authorization: "Your-Account-Token",
  };

  try {
    const response = await fetch(url, {
      method: "GET",
      headers,
      // How to use HTTP QueryString in fetch:
      // new URLSearchParams({ serie: "123" })
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const result: ApiResponse = await response.json();
    context.log(result);

    context.log("Your account name is: ", result.result.name);
  } catch (error) {
    context.log(`${error}\n${error}`);
  }
}

Analysis.use(startAnalysis);

// To run analysis on your machine (external)
// Analysis.use(myAnalysis, { token: "YOUR-TOKEN" });

MQTT Push from Dashboard

mqttpushdashboardmessagingiot

Send MQTT messages triggered from dashboard interactions

typescriptmqtt-push.ts
/*
 ** Analysis Example
 ** MQTT Push
 **
 * Snippet to push data to MQTT. Follow this pattern within your application
 * If you want more details about MQTT, search "MQTT" in TagoIO help center.
 * You can find plenty of documentation about this topic.
 * TagoIO Team.
 **
 ** How to use?
 ** In order to trigger this analysis you must setup a Dashboard.
 ** Create a Widget "Form" and enter the variable 'push_payload' for the device you want to push with the MQTT.
 ** In User Control, select this Analysis in the Analysis Option.
 ** Save and use the form.
 */

import type { Data, TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Services } from "npm:@tago-io/sdk";

interface TemperatureData {
  variable: string;
  value: number;
  unit: string;
}

async function mqttPushExample(context: TagoContext, scope: Data[]): Promise<void> {
  if (!scope.length) {
    return context.log("This analysis must be triggered by a dashboard.");
  }

  const myData = scope.find((x) => x.variable === "push_payload") || scope[0];
  if (!myData) {
    return context.log("Couldn't find any variable in the scope.");
  }

  // Create your data object to push to MQTT
  // In this case we're sending a JSON object.
  // You can send anything you want.
  // Example:
  // const myDataObject = 'This is a string';
  const myDataObject: TemperatureData = {
    variable: "temperature_celsius",
    value: ((myData.value as number) - 32) * (5 / 9),
    unit: "C",
  };

  // Create a object with the options you chooses
  const options = {
    qos: 0,
  };

  // Publishing to MQTT
  const MQTT = new Services({ token: context.token }).MQTT;
  await MQTT.publish({
    // bucket: myData.bucket, // for legacy devices
    bucket: myData.device!, // for immutable/mutable devices
    message: JSON.stringify(myDataObject),
    topic: "tago/my_topic",
    options,
  }).then(
    (result) => context.log(result),
    (error) => context.log(error)
  );
}

Analysis.use(mqttPushExample);

// To run analysis on your machine (external)
// Analysis.use(mqttPushExample, { token: "YOUR-TOKEN" });

Send Notifications

notificationalertdashboardemail

Send notification to account and dashboard with optional dashboard linking

typescriptnotifications.ts
import type { TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Services, Utils } from "npm:@tago-io/sdk";

/**
 * The main function used by Tago to run the script.
 * It sends a notification to the account and another one linked to a dashboard.
 * Optional: You can set a dashboard_id using an environment variable
 * this will show a button on the notification to send the user directly to the dashboard
 */
async function startAnalysis(context: TagoContext): Promise<void> {
  // reads the values from the environment variables and saves it in the variable env_vars
  const env_var = Utils.envToJson(context.environment);

  const notification = new Services({ token: context.token }).Notification;

  // In this variable, you type the title of the notification
  const title = "Your title";

  // In this variable, you type the message that you will send on the notification
  const message = "Your message";

  try {
    const notificationData = {
      message,
      title,
      ref_id: env_var.dashboard_id || undefined,
    };

    const service_response = await notification.send(notificationData);

    context.log(service_response);
  } catch (error) {
    context.log(error);
  }
}

Analysis.use(startAnalysis);

// To run analysis on your machine (external)
// Analysis.use(myAnalysis, { token: "YOUR-TOKEN" });

TagoSQL - Execute a Stored Query

tagosqlsqlstored queryaccess managementquery

Execute a stored TagoSQL query from an analysis using an Access Management grant

typescripttagosql-execute-stored-query.ts
/*
 ** Analysis Example
 ** TagoSQL - Execute a Stored Query
 **
 ** Executes a stored TagoSQL query (POST /sql/{id}/execute) with the analysis' own token,
 ** overriding one of the query's saved parameter defaults. Stored queries are the recommended
 ** way to reuse SQL: they are versioned, can be cached, and can be granted to analyses and
 ** Run users through Access Management.
 **
 ** How to use:
 ** 1 - Create a stored query with POST /sql (or from the admin) that uses a $1 parameter,
 **     for example: SELECT variable, value, time FROM device_tag('type', 'sensor') AS d
 **     WHERE variable = $1 ORDER BY time DESC LIMIT 10
 ** 2 - Grant this analysis access to it at https://admin.tago.io/am:
 **     - Click "Add Policy";
 **     - In the Target selector, select Analysis with the field set as "ID" and choose this analysis;
 **     - Click "Click to add a new permission", select "SQL Query" with the rule "Execute",
 **       matching the query by ID (or by tag to grant a group of queries);
 **     - Save the policy.
 ** 3 - Add the stored query id to the analysis environment variables:
 **     sql_id = your stored query id
 **
 ** Granting execution of a query grants its full result set: the query runs with the
 ** profile's data access, so only grant queries whose results the target may see.
 */

import type { TagoContext } from "npm:@tago-io/sdk";
import { Analysis, Utils } from "npm:@tago-io/sdk";

const TAGOIO_API = "https://api.tago.io";

async function startAnalysis(context: TagoContext): Promise<void> {
  const envVars = Utils.envToJson(context.environment);
  if (!envVars.sql_id) {
    return context.log("Add a sql_id to the analysis environment variables");
  }

  const response = await fetch(`${TAGOIO_API}/sql/${envVars.sql_id}/execute`, {
    method: "POST",
    headers: { token: context.token, "Content-Type": "application/json" },
    // The body is optional: a bare POST runs the query with its saved parameter defaults.
    // Values sent here override the saved defaults per key.
    body: JSON.stringify({
      params: [{ key: "$1", value: "temperature" }],
    }),
  });

  const body = await response.json();
  if (!response.ok || !body.status) {
    // A 403 here means this analysis has no Access Management policy granting
    // the Execute action on this query. See the instructions above.
    return context.log(`Query failed (${response.status}): ${body.message}`);
  }

  const { rows, row_count, served_from_cache } = body.result;
  context.log(`Got ${row_count} rows (served_from_cache: ${served_from_cache})`);
  for (const row of rows) {
    context.log(JSON.stringify(row));
  }
}

Analysis.use(startAnalysis);