afEasyConnExt icon

afEasyConnExt

EasyConn - Write connectors in Axon
afEasyConnExtAdvanced example

Registered StackHub users may elect to receive email notifications whenever a new package version is released or a comment is posted on the forum.

There are 0 watchers.

v1.0.0

Following is a more complex example of Axon functions that power an EasyConn connector.

Similar to the more basic example in the documentation these functions connect to the HTTP REST API provided by the UK's NationalGrid to obtain carbon intensity values for the electrical power generation in all regions of the UK.

This specific example, connecting to the UK's National Grid, was chosen because the data is publicly available and the REST API does NOT require any form of authentication.

Most REST APIs will require some form of authentication, for which Fantom Factory's HTTP Client SkySpark extension offers a secure means to send authentication credentials.

Most of the code in these Axon functions deal with the specifics of transforming the data structures returned from the National Grid API - in many cases the actual data returned does not match the documentation!

On the EasyConn connector record, you would add the following tags:

afEasyConnExtLearnFn   : "ciLearn"
afEasyConnExtSyncCurFn : "ciSyncCur"
afEasyConnExtSyncHisFn : "ciSyncHis"

Learn funcs

Learning is often the most complex function of a connector. These 3 functions provide a complete heirarical Learn tree for Carbon Intensity data.

func: ciLearn

// func
// name: ciLearn
(conn, arg : null) => do

  if (arg == null)
    return [
      { dis:"National", learn:"/national" },
      { dis:"Regional", learn:"/regional" },
    ].toGrid()

  uri  : arg.parseUri
  path : uri.uriPath

  if (path.size == 1 and path[0] == "national")
    return [
      { dis:"Intensity",      learn:"/national/intensity"  },
      { dis:"Statistics",     learn:"/national/statistics" },
      { dis:"Generation Mix", learn:"/national/generation" },
    ].toGrid()

  if (path.size == 1 and path[0] == "regional")
    return [
      { dis:"Kingdoms", learn:"/regional/kingdoms" },
      { dis:"Regions",  learn:"/regional/regions"  },
    ].toGrid()

  if (path.size == 2)
    return ciLearnPath2(uri)

  if (path.size == 3)
    return ciLearnPath3(uri)

  return null
end

func: ciLearnPath2

// func
// name: ciLearnPath2
(arg) => do
  path : arg.uriPath
  if (path[1] == "intensity") do
    json : ioReadJson(`https://api.carbonintensity.org.uk/intensity`, {safeNames})
    dataDict : json["data"].getSafe(0)
    if (dataDict == null) return null
    intDict : dataDict["intensity"]
    if (intDict == null) return null
    learnItems : []
    intDict.each() (val, key) => do
      kind : "Number"
      if (key == "index") kind = "Str"
      learnItems = learnItems.add({
        "dis"      : key.capitalize,
        "kind"     : kind,
        "ciKey"    : key,
        "ciType"   : "intensity",
        his, cur,
        ciNational,
      })
    end
    return learnItems.toGrid
  end

  if (path[1] == "statistics") do
    from : (now() - 30min).format("YYYY-MM-DD'T'hh:mmz")
      to : now().format("YYYY-MM-DD'T'hh:mmz")
    json : ioReadJson(parseUri("https://api.carbonintensity.org.uk/intensity/stats/" + from + "/" + to))
    dataDict : json["data"].getSafe(0)
    if (dataDict == null) return null
    statDict : dataDict["intensity"]
    if (statDict == null) return null
    learnItems : []
    statDict.each() (val, key) => do
      kind : "Number"
      if (key == "index") kind = "Str"
      learnItems = learnItems.add({
        "dis"      : key.capitalize,
        "kind"     : kind,
        "ciKey"    : key,
        "ciType"   : "statistics",
        his, cur,
        ciNational,
      })
    end
    return learnItems.toGrid
  end

  if (path[1] == "generation") do
    json : ioReadJson(`https://api.carbonintensity.org.uk/generation`, {safeNames})
    // For some reason this is coming back as a single dict even though api specifies a list
    dataDict : json["data"]
    if (dataDict == null) return null
    genDict : dataDict["generationmix"]
    if (genDict == null) return null
    learnItems : []
    genDict.each() (dict) => do
      learnItems = learnItems.add({
        "dis"      : dict->fuel.capitalize,
        "kind"     : "Number",
        "ciKey"    : dict->fuel,
        "ciType"   : "generation",
        "unit"     : "%",
        his, cur,
        ciNational,
      })
    end
    return learnItems.toGrid
  end

  if (path[1] == "kingdoms")
    return [
      {dis:"England", learn:"/regional/kingdoms/england"},
      {dis:"Wales", learn:"/regional/kingdoms/wales"},
      {dis:"Scotland", learn:"/regional/kingdoms/scotland"}
    ].toGrid()

  if (path[1] == "regions") do
    json : ioReadJson(`https://api.carbonintensity.org.uk/regional`, {safeNames})
    dataDict : json["data"].getSafe(0)
    if (dataDict == null) return null
    regionDict : dataDict["regions"]
    if (regionDict == null) return null
    learnItems : []
    regionDict.each() (dict) => do
      learn  : arg.uriPathStr + "/" + dict->regionid.format("0")
      learnItems = learnItems.add({
        "dis"   : dict->shortname.capitalize,
        "learn" : learn,
        "shortname" : dict->shortname,
        "dnoregion" : dict->dnoregion,
      })
    end
    return learnItems.toGrid
  end

  return null
end

func: ciLearnPath3

// func
// name: ciLearnPath3
(arg) => do
  path : arg.uriPath
  if (path[1] == "kingdoms") do
    url : parseUri(`https://api.carbonintensity.org.uk/regional/`.toStr() + path[2])
    json : ioReadJson(url, {safeNames})
    dataDict : json["data"].getSafe(0)
    if (dataDict == null) return null
    // another odd layout compared to documented api
    regionDict : dataDict.get("data").getSafe(0)
    if (regionDict == null) return null
    learnItems : []
    regionDict->intensity.each() (val, key) => do
      kind : "Number"
      if (key == "index") kind = "Str"
      learnItems = learnItems.add({
        "dis"        : key.capitalize + " (Intensity)",
        "ciRegionId" : dataDict->regionid,
        "ciKey"      : key,
        "ciType"     : "intensity",
        "kind"       : kind,
        his, cur,
      })
    end
    regionDict->generationmix.each() (dict) => do
      learnItems = learnItems.add({
        "dis"        : dict->fuel.capitalize + " (Generation Mix)",
        "ciRegionId" : dataDict->regionid,
        "ciKey"      : dict->fuel,
        "ciType"     : "generation",
        "kind"       : "Number",
        "unit"       : "%",
        his, cur,
      })
    end
    return learnItems.toGrid
  end

  if (path[1] == "regions") do
    url : parseUri(`https://api.carbonintensity.org.uk/regional/regionid/`.toStr() + path[2])
    json : ioReadJson(url, {safeNames})
    dataDict : json["data"].getSafe(0)
    if (dataDict == null) return null
    regionDict : dataDict.get("data").getSafe(0) // another odd layout compared to documented api
    if (regionDict == null) return null
    learnItems : []
    regionDict->intensity.each() (val, key) => do
      addr  : parseUri(arg.uriPathStr + "?key=" + key)
      kind : "Number"
      if (key == "index") kind = "Str"
      learnItems = learnItems.add({
        "dis"        : key.capitalize + " (Intensity)",
        "ciRegionId" : dataDict->regionid,
        "ciKey"      : key,
        "ciType"     : "intensity",
        "kind"       : kind,
        his, cur,
      })
    end
    regionDict->generationmix.each() (dict) => do
      learnItems = learnItems.add({
        "dis"        : dict->fuel.capitalize + " (Generation Mix)",
        "ciRegionId" : dataDict->regionid,
        "ciKey"      : dict->fuel,
        "ciType"     : "generation",
        "kind"       : "Number",
        "unit"       : "%",
        his, cur,
      })
    end
    return learnItems.toGrid
  end

  return null
end

Sync Cur func

The ciSyncCur function clamps the current time to a 30 minute interval and calls out to the REST API, using identifying data saved in the point record.

func: ciSyncCur

// func
// name: ciSyncCur
(conn, points) => do
  hour : now().time.hour
  mins : now().time.minute
  if (mins < 30) mins = 0
  else mins = 30

  from : dateTime(today(), time(hour, mins   )).format("YYYY-MM-DD'T'hh:mmz")
  to   : dateTime(today(), time(hour, mins+29)).format("YYYY-MM-DD'T'hh:mmz")

  return points.map() (point) => do
    if (point["ciNational"] != null) do
      if (point["ciType"] == "generation") do
        json : ioReadJson(`https://api.carbonintensity.org.uk/generation`, {safeNames})
        genDict : json["data"]["generationmix"].find(dict => dict["fuel"] == point["ciKey"])
        return {pointRef:point->id, val:genDict["perc"]}
      end
      if (point["ciType"] == "statistics") do
        json : ioReadJson(parseUri("https://api.carbonintensity.org.uk/intensity/stats/" + from + "/" + to), {safeNames})
        statsDict : json["data"][0]["intensity"]
        return {pointRef:point->id, val:statsDict[point["ciKey"]]}
      end
      if (point["ciType"] == "intensity") do
        json : ioReadJson(`https://api.carbonintensity.org.uk/intensity`, {safeNames})
        intDict : json["data"][0]["intensity"]
        return {pointRef:point->id, val:intDict[point["ciKey"]]}
      end
    end
    else do
      json : ioReadJson(parseUri("https://api.carbonintensity.org.uk/regional/regionid/" + point["ciRegionId"].toStr), {safeNames})
      if (point["ciType"] == "intensity") do
        intDict : json["data"][0]["data"][0]["intensity"]
        return {pointRef:point->id, val:intDict[point["ciKey"]]}
      end
      if (point["ciType"] == "generation") do
        genDict : json["data"][0]["data"][0]["generationmix"].find(dict => dict["fuel"] == point["ciKey"])
        return {pointRef:point->id, val:genDict["perc"]}
      end
    end

    throw "Something went wrong - ensure tags in bound points are correctly configured."
  end.toGrid()
end

His Cur func

The ciSyncHis function returns time series data from the REST API for the given point and date span.

func: ciSyncHis

// func
// name: ciSyncHis
(conn, point, span) => do
  point = point.toRec
  hour : span.start.time.hour
  mins : span.start.time.minute
  if (mins < 30) mins = 0
  else mins = 30

  from : dateTime(span.start.date(), time(hour, mins))
  spanDur : span.end - span.start
  minsDur : spanDur.to(1min).round
  numSegs : minsDur / 30

  hisItems : []

  numSegs.times() (i) => do
    tempFrom : from          .format("YYYY-MM-DD'T'hh:mmz")
    tempTo   : (from + 29min).format("YYYY-MM-DD'T'hh:mmz")
    if ((from + 29min) >= now()) return null

    if (point["ciNational"] != null) do
      if (point["ciType"] == "generation") do
        json : ioReadJson(parseUri("https://api.carbonintensity.org.uk/generation/" + tempFrom + "/" + tempTo), {safeNames})
        genDict : json["data"][0]["generationmix"].find(dict => dict["fuel"] == point["ciKey"])
        hisItems = hisItems.add({ts:from, val:genDict["perc"]})
      end
      else if (point["ciType"] == "statistics") do
        json : ioReadJson(parseUri("https://api.carbonintensity.org.uk/intensity/stats/" + tempFrom + "/" + tempTo), {safeNames})
        statsDict : json["data"][0]["intensity"]
        hisItems = hisItems.add({ts:from, val:statsDict[point["ciKey"]]})
      end
      else if (point["ciType"] == "intensity") do
        json : ioReadJson(parseUri("https://api.carbonintensity.org.uk/intensity/" + tempFrom + "/" + tempTo), {safeNames})
        intDict : json["data"][0]["intensity"]
        hisItems = hisItems.add({ts:from, val:intDict[point["ciKey"]]})
      end
    end
    else do
      json : ioReadJson(parseUri("https://api.carbonintensity.org.uk/regional/intensity/" + tempFrom + "/" + tempTo + "/regionid/" + point["ciRegionId"].toStr), {safeNames})
      if (point["ciType"] == "intensity") do
        intDict : json["data"]["data"][0]["intensity"]
        hisItems = hisItems.add({ts:from, val:intDict[point["ciKey"]]})
      end
      else if (point["ciType"] == "generation") do
        genDict : json["data"]["data"][0]["generationmix"].find(dict => dict["fuel"] == point["ciKey"])
        hisItems = hisItems.add({ts:from, val:genDict["perc"]})
      end
    end

    from = from + 30min
  end

  return hisItems.toGrid
end
Published by Fantom Factory

Products & Services by Fantom Factory

Packages by Fantom Factory

Commercial packages

Free packages

Licensing options
Developer Bundle - Senior
Our main tools under one licence: Axon Encryptor, Easy Conn, Folio File Sync, HTTP Client, and Pod Builder
1 year licence
$995.00USD
EasyConn
EasyConn - Write connectors in Axon
1 year licence
$495.00USD
Package details
Version1.0.0
LicenseCommercial
Build date11 months ago
on 26th May 2023
Requirements SkySpark v3.1.6
Depends on
File nameafEasyConnExt.pod
File size157.51 kB
MD536f87e2ed50b45dc730d85941b3a8168
SHA1 5e5cd9c2041abca27e4bf84e3b67a393e424eff9
Published by
Fantom FactoryDownload now
Also available via SkyArc Install Manager
Tags
Axon
Connector
Fantom
Sky Spark