Skip to content

ylabonte/procon-ip

procon-ip

NPM

Package info
npm version GitHub GitHub issues Known Vulnerabilities
CI Workflow Docs CodeQL
Buy me a coffee

ProCon.IP TypeScript library

Migrating from 1.x? See CHANGELOG.md — v2.0.0 drops Node ≤ 18, replaces axios with native fetch, removes deep imports, and adds DMX512 support. Migration snippets are at the bottom of the changelog.

Overview


Introduction

procon-ip is a TypeScript client library for the ProCon.IP pool controller. It wraps the controller's small HTTP surface (/GetState.csv, /usrcfg.cgi, /Command.htm, /SetState.pl, /GetDmx.csv) in typed services with a clean error model and a read-mutate-write idiom for DMX512.

Feel free to ask questions on GitHub Issues — discussions there are searchable for everyone with a similar question.

Installation

Requires Node 22 LTS or newer.

pnpm add procon-ip
# or
npm install procon-ip
# or
yarn add procon-ip

The package ships dual ESM + CJS via the exports map — both import { GetStateService } from 'procon-ip' and const { GetStateService } = require('procon-ip') work.

Examples

The runnable forms of the snippets below live under examples/.

Requesting data

import { GetStateService, GetStateCategory, Logger } from 'procon-ip';

const logger = new Logger();
const config = {
  controllerUrl: 'http://192.168.2.3', // <-- your pool controller's address
  basicAuth: true,
  username: 'admin',
  password: 'admin',
  updateInterval: 5000,
  timeout: 5000,
  errorTolerance: 2,
};

const dataSource = new GetStateService(config, logger);

// One-shot fetch:
const data = await dataSource.update();
logger.info(`Uptime: ${data.sysInfo.uptime}`);

// Or poll continuously:
dataSource.start((data) => {
  for (const obj of data.getDataObjectsByCategory(GetStateCategory.ELECTRODES)) {
    logger.info(`${obj.label}: ${obj.displayValue}`);
  }
});

Switching relays

import {
  GetStateService,
  UsrcfgCgiService,
  RelayDataInterpreter,
  GetStateCategory,
  Logger,
} from 'procon-ip';

const logger = new Logger();
const config = {
  controllerUrl: 'http://192.168.2.3',
  basicAuth: true,
  username: 'admin',
  password: 'admin',
  timeout: 5000,
  updateInterval: 5000,
  errorTolerance: 2,
};

const dataSource = new GetStateService(config, logger);
const interpreter = new RelayDataInterpreter(logger);
const relays = new UsrcfgCgiService(config, logger, dataSource, interpreter);

await dataSource.update();

// Switch a known dosage relay off:
await relays.setOff(dataSource.data.getChlorineDosageControl());

// Or hand a relay back to the controller's automatic schedule by label:
for (const relay of dataSource.data.getDataObjectsByCategory(GetStateCategory.RELAYS)) {
  if (relay.label === 'Gartenlicht') await relays.setAuto(relay);
}

setOn / setOff / setAuto return Promise<void> in v2 — failures throw the typed error classes from 'procon-ip' instead of returning a numeric response code (see Error handling).

Manual dosage

import { CommandService, Logger } from 'procon-ip';

const svc = new CommandService(
  { controllerUrl: 'http://192.168.2.3', basicAuth: false, timeout: 5000 },
  new Logger(),
);

const seconds = await svc.setChlorineDosage(60); // dose for 60s
// `seconds` === 60 on success, -1 after three retries.

DMX512

import { GetDmxService, DmxService, Logger } from 'procon-ip';

const logger = new Logger();
const config = { controllerUrl: 'http://192.168.2.3', basicAuth: false, timeout: 5000 };

const reader = new GetDmxService(config, logger);
const writer = new DmxService(config, logger);

const dmx = await reader.update();
for (const ch of dmx) dmx.set(ch.index, (ch.value + 64) % 256); // shift each channel
await writer.set(dmx);

GetDmxData is iterable, indexable via at(index), mutable via set(index, value) (clamps to [0, 255]), and produces the controller's form payload via toPostData(). The controller only accepts full 16-channel writes, so DmxService.set always sends every channel.

Error handling

All HTTP calls throw typed errors from 'procon-ip':

import {
  BadCredentialsError,
  BadStatusCodeError,
  RequestTimeoutError,
  InvalidPayloadError,
  ProconIpError,
} from 'procon-ip';

try {
  await getStateService.update();
} catch (e) {
  if (e instanceof BadCredentialsError) console.error('check basicAuth');
  else if (e instanceof RequestTimeoutError) console.error(`timeout: ${e.timeoutMs}ms`);
  else if (e instanceof BadStatusCodeError) console.error(`HTTP ${e.status}`);
  else throw e;
}

Network-level failures (DNS, connection refused, etc.) propagate as native TypeError from fetch.

Full API docs

Auto-generated TypeDoc reference, deployed on every push to master: https://ylabonte.github.io/procon-ip/.

A brief description of the ProCon.IP pool controller

The ProCon.IP pool controller is a low-budget network-attached control unit for home swimming pools. With its software-switched relays it can drive multiple pumps (filter and dosage) either on a time schedule or based on sensor readings (i/o flow sensors, Dallas 1-Wire thermometers, redox / pH electrodes), and it can also switch arbitrary relays on demand for things like garden lighting. Its DMX512 extension drives 16 channels of lighting or other DMX-compatible gear.

Not all of its functionality is reachable via the documented HTTP API, so this library reverse-engineers the parts the web UI uses (/usrcfg.cgi for relay + DMX writes, /Command.htm for manual dosage, /SetState.pl for relay timers).

For more information (German only):

Changelog

See CHANGELOG.md.

Disclaimer

I have nothing to do with the development, sales, marketing, or support of the pool controller unit itself. This is a community library that originated from my own ioBroker integration, later split out for reuse.