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

spider-farmer-client

v0.0.2

Published

Local-first TypeScript client for Spider Farmer GGS controllers over BLE or MQTT

Readme

spider-farmer-client

A local-first, fully typed Node.js client for Spider Farmer GGS controllers. It reads all currently known sensors and actuator states, emits events for individual value changes, and safely controls lights, fans, blowers, climate modules and power-strip outlets.

The project is independent, reverse-engineered software. Spider Farmer currently documents the GGS as app-only and does not provide a public integration API.

Schnellstart: erstes Ergebnis in zwei Minuten

Du musst weder BLE-UUIDs, Transportklassen, MQTT, pid, uid noch TypeScript verstehen, um den Controller zu testen. Du brauchst nur Node.js ab Version 20 und Bluetooth am Computer.

  1. Schließe die Spider-Farmer-App vollständig. Viele Controller akzeptieren nur eine aktive Bluetooth-Verbindung.
  2. Installiere das Modul in einem leeren Ordner:
npm init -y
npm install spider-farmer-client

Kleinstes eigenes Programm

Lege eine Datei namens app.mjs an:

import { connectBle } from "spider-farmer-client";

const ggs = await connectBle();

ggs.watch((state) => {
  console.log("Temperatur:", state.sensors.airTemperature);
  console.log("Luftfeuchte:", state.sensors.humidity);
  console.log("Umluft:", state.actuators.fans.fan);
  console.log("Abluft:", state.actuators.fans.blower);
});

Starte es:

node app.mjs

connectBle() sucht, verbindet, aktiviert Updates und liest den ersten Zustand. watch() läuft sofort und danach alle zwei Sekunden. Das ist die empfohlene API für normale BLE-Nutzung. Die weiter unten dokumentierten Transportklassen sind nur für fortgeschrittene Setups nötig.

Die häufigsten Befehle sind absichtlich leicht lesbar:

await ggs.setCirculationFan({ on: true, level: 4 }); // Stufe 1..10
await ggs.setCirculationFan({ on: false });
await ggs.setExhaustFan({ on: true, level: 45 });    // 1..100 Prozent
await ggs.setExhaustFan({ on: false });
await ggs.setLight("light", { on: true, level: 70 });

Vor dem ersten Schalten kannst du einfach prüfen:

console.log("Schreiben bereit:", ggs.writeReady);
console.log("Details:", ggs.writeReadiness); // { ready, missing: ["pid", "uid"] }

Ist der Wert false, funktioniert das Lesen trotzdem. Zum Schreiben fehlen dann noch PID oder UID; übergib sie als connectBle({ address, pid, uid }). Viele Controller melden diese Kennungen beim Verbindungsaufbau automatisch.

Aktoren werden aus dem tatsächlich gemeldeten Zustand bzw. der Gerätekonfiguration abgeleitet. Unbekannte Kanäle werden standardmäßig vor dem Schreiben abgelehnt:

for (const actuator of ggs.availableActuators) {
  console.log(actuator.id, actuator.fields);
}

fields enthält die schreibbaren Eigenschaften und, wo sinnvoll, min, max und step. Widersprüchliche Patches wie { on: false, level: 50 } werden ebenfalls ohne Transport-Write abgelehnt.

Beginne mit reinem Lesen. Teste jeden AUS-Befehl direkt am Gerät, bevor eine Automation unbeaufsichtigt laufen darf.

Highlights

  • No controller hotspot required.
  • Direct, cloud-free BLE connection (known GGS service variants, FF01/FF02).
  • Transparent TLS/MQTT proxy for router DNS/NAT setups; the official cloud and app continue to work.
  • Experimental direct MQTT client for brokers for which you already have legitimate credentials.
  • Air temperature, humidity, VPD, CO₂, PPFD, aggregate soil values and every individual soil probe.
  • Lights 1/2, circulation fan, exhaust blower, heater, humidifier, dehumidifier and outlets O1–O10.
  • Manual, schedule, cycle, environment and PPFD configuration fields.
  • Typed state, change, sensorChange and actuatorChange events.
  • Full config preservation: writes merge into the controller's existing block instead of erasing schedules or future firmware fields.
  • BLE AA AA chunk framing with CRC16/MODBUS for large configFile payloads.
  • Raw command and configuration APIs for unknown/new firmware fields.
  • ESM and CommonJS builds with TypeScript declarations.

Requirements

  • Node.js 20 or newer.
  • A Spider Farmer GGS controller, power strip or light controller supported by the observed protocol.
  • For BLE: Bluetooth on the Node.js host and the OS prerequisites of @stoprocent/noble.
  • Reading over the beginner API needs no device identifiers. Configuration writes need a uid; it is learned from status messages when the firmware includes it, otherwise pass it to connectBle({ pid, uid }). The common GGS pcode is 1004 and is already the default.

Install:

npm install spider-farmer-client

The BLE binding is an optional dependency so MQTT-only/server installations do not fail on machines without native Bluetooth support. If your package manager skipped it:

npm install @stoprocent/noble

Raspberry Pi setup

Yes: the module can run continuously on a Raspberry Pi without turning the Pi into a Wi-Fi hotspot. The recommended setup is Raspberry Pi OS 64-bit on a Pi with working Bluetooth, for example a Pi 3/4/5 or Zero 2 W. The Pi only needs to remain within BLE range of the GGS controller. The controller can keep its normal Wi-Fi and Spider Farmer cloud connection in parallel.

1. Install operating-system dependencies

sudo apt update
sudo apt install -y \
  bluetooth \
  bluez \
  libbluetooth-dev \
  libudev-dev \
  build-essential \
  python3

sudo systemctl enable --now bluetooth

Verify that Node.js 20 or newer is installed:

node --version
npm --version

2. Install the client

npm init -y
npm install spider-farmer-client

3. Permit Bluetooth access without running Node as root

Add the service user to the Bluetooth group, then log out and back in:

sudo usermod -aG bluetooth "$USER"

For an interactive/manual process, grant the Node executable raw Bluetooth access:

sudo setcap cap_net_raw+eip "$(readlink -f "$(command -v node)")"

Reapply setcap after replacing or upgrading the Node executable. Close the nearby Spider Farmer mobile app during the first BLE connection because some controllers allow only one BLE central at a time.

Recommended API: local BLE, no hotspot, no cloud

import { connectBle } from "spider-farmer-client";

const ggs = await connectBle({
  // Every option is optional. Omit address to scan for SF-GGS-CB.
  address: "AA:BB:CC:DD:EE:FF",
});

ggs.on("sensorChange", ({ path, previous, value }) => {
  console.log(`${path}: ${previous} -> ${value}`);
});

ggs.on("actuatorChange", ({ path, value }) => {
  console.log(`${path} is now`, value);
});

ggs.on("error", console.error);

console.log(ggs.sensors);
console.log(ggs.fans);

await ggs.setLight("light", { on: true, level: 70, mode: "manual" });
await ggs.setExhaustFan({ on: true, level: 45 });
await ggs.setCirculationFan({ on: true, level: 5, oscillation: 3 });
await ggs.setClimate("humidifier", { on: true });
await ggs.setOutlet(3, true);

BLE notes:

  • The controller may permit only one active BLE central. Close the nearby mobile app while the Node process is connecting.
  • BLE does not disable the controller's independent Wi-Fi/cloud connection.
  • The package reads getConfigFile, preserves the complete section, writes setConfigField, verifies the result, and falls back to full setConfigFile on BLE/proxy firmware that rejects the smaller write.

Hotspot-free network interception with router DNS/NAT

Use this mode when the Node host is not within BLE range. The controller stays on its normal Wi-Fi. Your router must redirect its connection for sf.mqtt.spider-farmer.com:8883 to the Node host.

import { readFileSync } from "node:fs";
import {
  SpiderFarmerClient,
  SpiderFarmerProxyTransport,
} from "spider-farmer-client";

const transport = new SpiderFarmerProxyTransport({
  mac: "AABBCCDDEEFF",
  listenHost: "0.0.0.0",
  listenPort: 8883,
  upstreamHost: "sf.mqtt.spider-farmer.com",
  tls: {
    // Supply certificates you are legally entitled to use. Nothing is bundled.
    cert: readFileSync("./private/controller-client.pem"),
    key: readFileSync("./private/controller-client-key.pem"),
    ca: readFileSync("./private/spider-farmer-ca.pem"),
  },
});

const ggs = new SpiderFarmerClient({
  transport,
  device: { pid: "AABBCCDDEEFF" },
});

await ggs.connect(); // waits for this controller's MQTT CONNECT packet

Router outline:

  1. Give the proxy host a fixed LAN address.
  2. Override the broker DNS record for the controller or DNAT the controller's TCP/8883 traffic to that host.
  3. Allow the proxy host to reach the real upstream broker. If its own DNS uses the same override, set upstreamHost to a separately resolved public address and keep upstreamServername as the real hostname.
  4. Restrict inbound port 8883 to the controller VLAN/IP. This is an interception endpoint, not a public service.

The TLS proxy forwards every MQTT byte unchanged, observes UP and DOWN messages, learns CB/PS/LC topic prefixes from the controller's subscription, and injects commands into the already authenticated device session.

Direct MQTT transport

This transport speaks the raw SF/GGS/+/API/{UP,DOWN}/{MAC} protocol. It is useful for a compatible raw broker or an experimental independent connection to the vendor broker. It does not consume the normalized spiderfarmer/# or ggs/# topics created by other bridges.

import { readFileSync } from "node:fs";
import {
  SpiderFarmerClient,
  SpiderFarmerMqttTransport,
} from "spider-farmer-client";

const transport = new SpiderFarmerMqttTransport({
  url: "mqtts://sf.mqtt.spider-farmer.com:8883",
  mac: "AABBCCDDEEFF",
  mqtt: {
    ca: readFileSync("./private/ca.pem"),
    cert: readFileSync("./private/client.pem"),
    key: readFileSync("./private/client-key.pem"),
  },
});

const ggs = new SpiderFarmerClient({
  transport,
  device: { pid: "AABBCCDDEEFF", uid: process.env.GGS_UID },
  // Broker/device firmwares differ in whether command acknowledgements are visible.
  verifyWrites: false,
});

await ggs.connect();

The vendor broker is private and undocumented. ACLs, mTLS certificates and duplicate-session behavior may change; no credentials or extracted private keys are distributed by this project.

State model

client.state returns a defensive copy:

interface SpiderFarmerState {
  device: { pid: string; uid?: string; pcode?: number; online: boolean; lastSeen?: Date };
  sensors: {
    airTemperature?: number;
    humidity?: number;
    vpd?: number;
    co2?: number;
    ppfd?: number;
    soilTemperature?: number;
    soilMoisture?: number;
    soilEc?: number;
    raw: JsonObject;
  };
  soilSensors: Record<string, SoilSensorState>;
  actuators: {
    lights: { light?: LightState; light2?: LightState };
    fans: { fan?: FanState; blower?: FanState };
    climate: {
      heater?: ClimateState;
      humidifier?: ClimateState;
      dehumidifier?: ClimateState;
    };
    outlets: Record<`O${number}`, OutletState>;
  };
  system?: SystemState;
  configuration?: JsonObject;
  lastMessage?: SpiderFarmerMessage;
}

Known values are normalized, while every block also retains its original raw object. Absent hardware stays absent rather than being represented by ghost devices.

Events

ggs.on("connected", () => {});
ggs.on("disconnected", (reason) => {});
ggs.on("message", ({ message, direction, topic }) => {});
ggs.on("state", (completeState) => {});
ggs.on("change", ({ path, previous, value, state, message }) => {});
ggs.on("sensorChange", (change) => {});
ggs.on("actuatorChange", (change) => {});
ggs.on("error", (error) => {});

Examples of change paths:

  • sensors.airTemperature
  • soilSensors.3.humidity
  • actuators.lights.light.level
  • actuators.fans.blower.on
  • actuators.outlets.O4.on

Events fire only when normalized values actually differ. Raw protocol blocks are available on the full message event without creating noisy duplicate change events.

Control API

await ggs.refresh();
await ggs.getConfiguration();

await ggs.setLight("light", {
  mode: "schedule",
  schedule: {
    enabled: 1,
    weekmask: 127,
    startTime: 6 * 3600,
    endTime: 18 * 3600,
    brightness: 80,
    fadeTime: 15 * 60,
  },
});

await ggs.setLight("light", {
  mode: "ppfd",
  ppfdSchedule: { startTime: 6 * 3600, endTime: 18 * 3600, brightness: 850 },
  ppfdMinBrightness: 20,
  ppfdMaxBrightness: 100,
});

await ggs.setFan("blower", {
  mode: "temperature-and-humidity",
  minSpeed: 25,
  maxSpeed: 80,
});

await ggs.setFan("fan", {
  mode: "cycle",
  cycle: { startTime: 0, openDur: 10 * 60, closeDur: 5 * 60, times: 96 },
});

const effectiveLight = await ggs.setLight("light", { on: true, level: 70 });
console.log(effectiveLight);

// Stabiles JSON-DTO: ISO-Zeitstempel, ohne raw/configuration als Standard.
const dto = ggs.snapshot();
const diagnosticDto = ggs.snapshot({ raw: true, configuration: true });

Fan mode mappings observed in the official app:

| API mode | modeType | |---|---:| | manual | 0 | | schedule | 1 | | cycle | 2 | | temperature | 3 | | humidity | 4 | | prioritize-temperature | 7 | | prioritize-humidity | 8 | | temperature-and-humidity | 13 |

Light modes are manual (0), schedule (1) and ppfd (12). Numeric mode values remain accepted for forward compatibility.

Raw/future firmware access

import { buildCommand } from "spider-farmer-client";

// Read/write an unknown full configuration block.
await ggs.setConfigField(
  ["device", "futureModule"],
  { modeType: 0, mOnOff: 1, vendorField: 42 },
  { verify: false },
);

// Send any future method and wait for the matching method/msgId response.
const response = await ggs.request(
  buildCommand("futureMethod", { value: 42 }, { pid: "AABBCCDDEEFF", uid: "..." }),
);

// Fire-and-forget is explicit.
await ggs.request(buildCommand("futureMethod"), { awaitResponse: false });

setConfiguration() rejects suspiciously short/partial files by default because setConfigFile replaces controller configuration. unsafeAllowPartial: true is available only for deliberate protocol research or recovery.

Protocol provenance and limits

The implementation was independently written from observed wire behavior and cross-checked against:

This cannot guarantee compatibility with untested hardware or future firmware. Keep physical safety controls in place, test OFF commands before unattended automation, and never make grow-room safety depend on a single wireless integration.

License

MIT. Not affiliated with, endorsed by, or sponsored by Spider Farmer.