@apocaliss92/noderademacher
v0.1.0
Published
Node.js/TypeScript client for Rademacher HomePilot / Start2Smart hubs over the local network (REST + SSDP discovery)
Maintainers
Readme
noderademacher
Node.js / TypeScript client for Rademacher HomePilot / Start2Smart hubs over the local network (no cloud).
Spiritual successor to pyrademacher — same local REST API, TypeScript-native. If you can control a device in the HomePilot app, you can control it with this library. Covers/blinds, switches/plugs, dimmable actuators, Zigbee lights, thermostats/radiator valves, sensors and wall controllers.
Node only, local only. Talks HTTP to the hub on your LAN. No Rademacher cloud account is used or required.
Install
npm i @apocaliss92/noderademacherRequires Node 20 or later.
Quick start
import { Rademacher, CoverDevice } from '@apocaliss92/noderademacher';
// Point at your hub (host or IP). Add `password` if the hub is protected.
const hub = new Rademacher({ host: '192.168.1.50' });
// 1. Connect (auto-resolves the api-version base path, logs in if protected)
await hub.connect();
// 2. List devices registered to the hub
const devices = await hub.listDevices();
// 3. Create a typed handle (fetches its initial state)
const cover = (await hub.createDevice(devices[0])) as CoverDevice;
// 4. React to polled state changes
cover.on('stateChanged', ({ changed }) => console.log('changed:', changed));
// 5. Control it
await cover.open();
await cover.setPosition(60); // 0 = closed, 100 = open
// 6. Re-poll whenever you like (HomePilot has no realtime push)
await cover.refresh();Discovery
HomePilot has no broadcast/mDNS discovery (Rademacher's own integration finds it via DHCP MAC-OUI). So noderademacher discovers hubs by HTTP-probing the LAN — it confirms each host via the hub's identity endpoint:
import { Rademacher } from '@apocaliss92/noderademacher';
// Sweep every local /24 (bounded concurrency), or pass { subnet: '192.168.1' } / { hosts: [...] }
const hubs = await Rademacher.discover();
// → [{ host, model: 'HomePilot' | 'Start2Smart', name, firmware?, uid?, basePath, passwordProtected }]
for (const found of hubs) {
const hub = new Rademacher({ host: found.host, basePath: found.basePath });
await hub.connect();
}Rademacher.discover({ subnet, hosts, concurrency, timeoutMs }) — narrow the sweep for speed. The Rademacher MAC OUIs (B0:1F:81, 38:FD:FE) are exported as RADEMACHER_MAC_PREFIXES if you want to pre-filter an ARP scan.
Devices — the gateway included, with all info
listDevices() / getDevices() return the hub/gateway itself as a separate device (a
HubDevice, did = -1, category hub) first, then every physical device. Pass
{ includeHub: false } to omit it.
const devices = await hub.getDevices(); // typed handles, gateway first, each already polled
for (const dev of devices) {
console.log(dev.name, dev.category, dev.describe());
}describe() returns everything retrievable about a device: identity, battery, every
capability (with value + min/max/step) and the latest statuses / readings. The HubDevice
exposes firmware, hardware/software platform, DuoFern-stick version, node name, MAC, LED status
and the firmware-update status.
Supported devices
| Class | DEVICE_TYPE_LOC | Highlights |
| ---------------------- | ----------------- | ---------------------------------------------------------------- |
| CoverDevice | 2, 8 | open / close / stop, setPosition, setTilt, ventilation |
| SwitchDevice | 1 | turnOn / turnOff / setOn, isOn |
| ActuatorDevice | 4 | dimmable: setBrightness, brightness, isOn |
| LightDevice | 70–76 | actuator + setRgb, setColorTemperature |
| ThermostatDevice | 5 | setTargetTemperature, currentTemperature, boost |
| SensorDevice | 3 | temperature, wind, brightness, rain, contact, motion, smoke |
| WallControllerDevice | 10 | channel key-press timestamps |
| UnknownDevice | — | raw state + passthrough commands |
Feature detection is by capability-name presence (device.hasCapability(...)), exactly like the reference library — never by product code.
How it works
- Connect — probes
http://<host>/…to resolve the api-version base path (''v1 //hpv2) and detect password protection; logs in via a two-stage salted SHA-256 challenge when a password is set (cookie session). - Devices —
listDevices()reads/devices(full capability lists → feature detection + category). - State —
createDevice()/refresh()read/v4/devices/{did}(statusesMap+readings). - Commands — every control is
PUT /devices/{did}with{ name, value? }.
HomePilot's local API has no realtime push, so state is refreshed by polling (device.refresh()).
Two gotchas worth knowing (handled for you)
- Cover position is inverted on the wire (HomePilot 0 = open). This library exposes the HA-style
position(100 = open) and inverts on both read and write. - Thermostat temperatures are deci-°C in state (
/10) but plain °C in the write command — the model converts both ways.
Diagnostics dumper
import { createDumper } from '@apocaliss92/noderademacher';
const snap = createDumper(cover).dump();
// { did, category: 'cover', model, name: 'Ki*****', capabilities: [...], state: { statuses, readings } }redact: true (default) masks the friendly name so dumps can be shared safely.
Error types
| Class | Thrown when |
| -------------------------- | ------------------------------------------------------- |
| RademacherError | Base class |
| RademacherAuthError | Login / re-auth failure |
| RademacherApiError | Non-2xx HTTP or a non-zero hub error_code (.status) |
| RademacherTransportError | Network error reaching the hub |
Credits & scope
Reverse-engineered from pyrademacher (the library behind the peribeir Home Assistant Rademacher integration). See docs/rademacher-homepilot-api-spec.md for the full protocol notes this port is built from. Not affiliated with or endorsed by Rademacher.
License
MIT — see LICENSE.
