spider-farmer-client
v0.0.2
Published
Local-first TypeScript client for Spider Farmer GGS controllers over BLE or MQTT
Maintainers
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.
- Schließe die Spider-Farmer-App vollständig. Viele Controller akzeptieren nur eine aktive Bluetooth-Verbindung.
- Installiere das Modul in einem leeren Ordner:
npm init -y
npm install spider-farmer-clientKleinstes 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.mjsconnectBle() 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,sensorChangeandactuatorChangeevents. - Full config preservation: writes merge into the controller's existing block instead of erasing schedules or future firmware fields.
- BLE
AA AAchunk framing with CRC16/MODBUS for largeconfigFilepayloads. - 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 toconnectBle({ pid, uid }). The common GGSpcodeis1004and is already the default.
Install:
npm install spider-farmer-clientThe 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/nobleRaspberry 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 bluetoothVerify that Node.js 20 or newer is installed:
node --version
npm --version2. Install the client
npm init -y
npm install spider-farmer-client3. 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, writessetConfigField, verifies the result, and falls back to fullsetConfigFileon 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 packetRouter outline:
- Give the proxy host a fixed LAN address.
- Override the broker DNS record for the controller or DNAT the controller's TCP/8883 traffic to that host.
- Allow the proxy host to reach the real upstream broker. If its own DNS uses the same override, set
upstreamHostto a separately resolved public address and keepupstreamServernameas the real hostname. - 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.airTemperaturesoilSensors.3.humidityactuators.lights.light.levelactuators.fans.blower.onactuators.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:
- SpiderBridge — MQTT/TLS interception, topic and configuration fields.
- Schedule 4 Real — multi-module state and control behavior.
- Spider Farmer GGS Controller MQTT — BLE UUIDs and raw JSON telemetry.
- ggs-esp32-mqtt-bridge — AA AA framing, CRC and config-preserving BLE writes.
- Spider Farmer Bridge for Home Assistant — additional hardware/state shapes and configuration preservation.
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.
