npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sixb/connector-ace-iot

v0.1.0

Published

ACE IoT Deploy API connector for Sixb

Downloads

115

Readme

@sixb/connector-ace-iot

Typed ACE IoT Deploy API connector for Sixb. It covers the whole published surface — clients, sites, points, gateways, timeseries, and weather — one method per endpoint, with wire-shape types taken from ACE's own schema and corrected against the live API.

Register

Create an API key in ACE, then define the connector in your project's connectors/ directory:

import { aceIot } from "@sixb/connector-ace-iot"
import { defineConnector } from "@sixb/core"

export const aceIotConnector = defineConnector(
  "ace-iot",
  aceIot({
    apiKey: process.env.ACE_IOT_API_KEY!,
  })
)

createSixb() discovers the definition automatically. Resolve the typed client with:

const ace = await sixb.connector(aceIotConnector)

The key can also be an async resolver, called before every attempt, which supports rotation without recreating the connector.

Options

| Option | Description | | --- | --- | | apiKey | Required API key or async resolver. Sent as Authorization: Bearer. | | baseUrl | Defaults to https://flightdeck.aceiot.cloud/api/. | | timeoutMs | Optional per-attempt timeout. | | minDelayMs | Delay between request starts. Defaults to 0 — ACE publishes no rate limit. | | retry | Method-aware transient-failure policy. Defaults to two retries for reads. |

Default retries cover network errors, 429, and 5xx responses on reads, and honor Retry-After. Writes are never replayed: POST /points/ and PUT /points/{name} merge tag state, and POST /gateways/{name}/token mints a credential, so a replay would issue a second one. The one exception is points.getTimeseriesForPoints, a read that ACE exposes as a POST; it is marked idempotent and retried like any other read.

Timeseries

The reason to reach for this connector rather than calling the endpoint directly.

for await (const sample of ace.sites.iterateTimeseries("my_site", {
  startTime: new Date(Date.now() - 15 * 60_000),
  endTime: new Date(),
  pageSize: 1000,
})) {
  // sample.value is a string; sample.time is naive UTC.
}

iterateTimeseries walks the window to the end and hands back every reading exactly once. iterateTimeseriesPages yields whole pages instead, and getTimeseriesPage returns a single page exactly as ACE sends it, for callers persisting cursors themselves.

ACE's cursor does not always advance

GET /sites/{site_name}/timeseries/paginated returns a base64 cursor that decodes to {"offset": N, "timestamp": "..."}. ACE sets offset to the number of rows it drew from the page's final timestamp bucket, without adding the offset the request carried in. When a page begins and ends inside one bucket, the cursor it returns is the cursor it was given, and a plain while (has_more) loop re-requests the same page forever.

Measured against a live site over a 15-minute window holding 3,222 readings:

| page_size | Following ACE's cursor | iterateTimeseries | | --- | --- | --- | | 50 | stalls after 2 pages — 100 rows | 65 pages, 3,222 rows, no duplicates | | 100 | stalls after 2 pages — 200 rows | 33 pages, 3,222 rows, no duplicates | | 500 | stalls after 2 pages — 1,000 rows | 7 pages, 3,222 rows, no duplicates | | 1000 | 4 pages, 3,222 rows | 4 pages, identical result |

The connector recomputes the cursor from the page it just read, which the server honors correctly. Large page sizes appear to work only because every page happens to cross a bucket boundary — the failure is silent, so lowering page_size loses data rather than raising an error. Where ACE's cursor is already right, the recomputed one is identical.

A walk that returns the same page twice raises rather than looping, so an upstream change can never turn into an unbounded read.

Timestamps are naive but mean UTC

Every ACE timestamp arrives without a zone designator — 2026-08-07T16:25:00 — so new Date(value) reads it as local time and shifts it silently on any host that is not on UTC. Use the exported parser:

import { parseAceIotTimestamp } from "@sixb/connector-ace-iot"

parseAceIotTimestamp("2026-08-07T16:25:00") // 2026-08-07T16:25:00.000Z

It also handles the two other shapes ACE emits: microsecond precision (…:00.627593, truncated to milliseconds) and the space-separated form used by a gateway's device_token_expires (2027-03-04 19:34:54.114795). normalizeAceIotTimestamp applies the same rule and stays a string.

Query bounds accept a Date or a string, and a naive timestamp read off a response can be passed straight back as a bound without shifting.

Values stay strings

point_samples[].value is a string on the wire, including numerics like "1.6100000143051147". The connector leaves it alone rather than introducing rounding the API never had.

Pagination

List endpoints use ACE's page/per_page envelope, returned verbatim as {items, page, pages, per_page, total}. Every list* has a listAll* companion that walks it:

for await (const point of ace.sites.listAllConfiguredPoints("my_site")) {
  // …
}

listAll* defaults to perPage: 1000 because ACE's own default is 10. Pass maxPages to bound the walk. pages comes back null on the gateway PCAP listing, so the walk also stops on an empty page, a short page, and on reaching total.

per_page is a closed enum, not a range — 2, 10, 20, 30, 40, 50, 100, 500, 1000, 5000, 10000, 100000. The timeseries endpoint takes a different one for page_size: 3, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 300000, 500000 (it allows 3, but not 2). Both are exported as ACE_IOT_PER_PAGE_VALUES and ACE_IOT_PAGE_SIZE_VALUES, and both are checked locally so an unsupported size is a clear error instead of a 400 that names the value but not the allowed set.

Clients API

| Client method | ACE endpoint | | --- | --- | | ace.clients.list / listAll(options?) | GET /clients/ | | ace.clients.get(name) | GET /clients/{client_name} | | ace.clients.create(input) | POST /clients/ | | ace.clients.listSites / listAllSites(name, options?) | GET /clients/{client_name}/sites | | ace.clients.listDerEvents / listAllDerEvents(name, options?) | GET /clients/{client_name}/der_events | | ace.clients.createDerEvents(name, events) | POST /clients/{client_name}/der_events | | ace.clients.updateDerEvents(name, events) | PUT /clients/{client_name}/der_events | | ace.clients.listVolttronAgentPackages / listAll…(name, options?) | GET /clients/{client_name}/volttron_agent_package/list | | ace.clients.downloadVolttronAgentPackage(name, packageId) | GET /clients/{client_name}/volttron_agent_package | | ace.clients.uploadVolttronAgentPackage(name, input) | POST /clients/{client_name}/volttron_agent_package |

The record type is AceIotClientAccount, because AceIotClient is the connected SDK client.

Sites API

| Client method | ACE endpoint | | --- | --- | | ace.sites.list / listAll(options?) | GET /sites/ | | ace.sites.get(name) | GET /sites/{site_name} | | ace.sites.create(input) | POST /sites/ | | ace.sites.listPoints / listAllPoints(name, options?) | GET /sites/{site_name}/points | | ace.sites.listConfiguredPoints / listAllConfiguredPoints(name, options?) | GET /sites/{site_name}/configured_points | | ace.sites.getTimeseries(name, range) | GET /sites/{site_name}/timeseries | | ace.sites.appendTimeseries(name, samples) | POST /sites/{site_name}/timeseries | | ace.sites.getTimeseriesPage(name, options) | GET /sites/{site_name}/timeseries/paginated | | ace.sites.iterateTimeseries / iterateTimeseriesPages(name, options) | Repaired cursor walk | | ace.sites.getWeather(name) | GET /sites/{site_name}/weather |

list takes collectEnabled and showArchived; both default to false upstream and are omitted unless set. getWeather returns null for a site with no weather feed, which ACE reports as a 404 carrying an all-null body.

geo_location is a PostGIS point as a WKB hex string, not a coordinate pair — latitude and longitude are the readable fields.

Points API

| Client method | ACE endpoint | | --- | --- | | ace.points.list / listAll(options?) | GET /points/ | | ace.points.get(name) | GET /points/{point_name} | | ace.points.create(points, options?) | POST /points/ | | ace.points.update(name, input, options?) | PUT /points/{point_name} | | ace.points.getTimeseries(name, range) | GET /points/{point_name}/timeseries | | ace.points.getTimeseriesForPoints(names, range) | POST /points/get_timeseries |

Point names are slash-separated paths (client/site/10.0.0.1-100/analogInput/1); the connector percent-encodes them, which is required or the request routes elsewhere.

Writes merge tags into what a point already has. Pass overwriteMarkerTags or overwriteKvTags to replace them instead.

bacnet_data is typed with the 33 properties the live API returns — ACE's schema documents eleven and types the object as free-form — plus an index signature for vendor properties outside that set. A property the device did not answer for comes back as the literal string "property: unknown-property", not null.

Gateways API

| Client method | ACE endpoint | | --- | --- | | ace.gateways.list / listAll(options?) | GET /gateways/ | | ace.gateways.get(name) | GET /gateways/{gateway_name} | | ace.gateways.create(input) | POST /gateways/ | | ace.gateways.update(name, input) | PATCH /gateways/{gateway_name} | | ace.gateways.createToken(name) | POST /gateways/{gateway_name}/token | | ace.gateways.listAgentConfigs / listAll…(name, options?) | GET /gateways/{gateway_name}/agent_configs | | ace.gateways.createAgentConfigs(name, configs, options?) | POST /gateways/{gateway_name}/agent_configs | | ace.gateways.listVolttronAgents / listAll…(name, options?) | GET /gateways/{gateway_name}/volttron_agents | | ace.gateways.createVolttronAgents(name, agents) | POST /gateways/{gateway_name}/volttron_agents | | ace.gateways.getVolttronAgentConfigPackage(name, identity, options?) | GET /gateways/{gateway_name}/volttron_agent_config_package | | ace.gateways.createVolttronAgentConfigPackage(name, input, options?) | POST /gateways/{gateway_name}/volttron_agent_config_package | | ace.gateways.listHawkeConfigurations / listAll…(name, options?) | GET /gateways/{gateway_name}/hawke_configuration | | ace.gateways.createHawkeConfigurations(name, configs, options?) | POST /gateways/{gateway_name}/hawke_configuration | | ace.gateways.getHawkeAgentConfiguration(name, agentId, options?) | GET …/hawke_configuration/{hawke_agent_id} | | ace.gateways.setHawkeAgentConfiguration(name, agentId, config, options?) | POST …/hawke_configuration/{hawke_agent_id} | | ace.gateways.listHawkeAgentConfigurations / listAll…(name, agentId, options?) | GET …/hawke_configuration/{hawke_agent_id}/list | | ace.gateways.listDerEvents / listAllDerEvents(name, options?) | GET /gateways/{gateway_name}/der_events | | ace.gateways.listPcapFiles / listAllPcapFiles(name, options) | GET /gateways/{gateway_name}/pcap/list | | ace.gateways.downloadPcap(name, fileName) | GET /gateways/{gateway_name}/pcap | | ace.gateways.uploadPcap(name, file, filename?) | POST /gateways/{gateway_name}/pcap |

interfaces and deploy_config are free-form JSON blobs ACE does not describe, so they stay Record<string, unknown>. Downloads return the raw Response with its body unread, so the caller decides whether to buffer or stream.

Errors

Non-successful responses throw AceIotApiError with the status, parsed body, headers, and retryAfterMs. Flask-RESTX field validation is surfaced per parameter through validationErrors.

import { AceIotApiError } from "@sixb/connector-ace-iot"

try {
  await ace.sites.get("unknown_site")
} catch (error) {
  if (error instanceof AceIotApiError) {
    console.error(error.status, error.validationErrors, error.responseBody)
  }
}

AceIotConfigurationError covers problems with the connector's own setup — an empty API key, an empty base URL. It is never retried, because it fails the same way on every attempt.

Two response behaviors worth knowing, both verified against the live API and neither in ACE's schema:

  • An invalid API key returns 500, carrying Flask's generic "An unhandled exception occurred.", not a 401. The connector appends a note saying so, because that message is otherwise impossible to act on. A missing key does return 401.
  • An unknown site returns 403 with a fully-null site body, while an unknown point — or an unknown site's points — returns 404.

Not covered

ACE publishes no webhooks, no delete endpoints, and no incremental change log, so the connector exposes none. Every path in ACE's Swagger document is implemented.

Official documentation