node-intellicenter
Version:
NodeJS library for communicating with a Pentair IntelliCenter controller
208 lines • 7.94 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FindUnits = exports.UnitInfo = void 0;
const dgram_1 = require("dgram");
const events_1 = require("events");
const debug_1 = __importDefault(require("debug"));
const dns_js_1 = require("./dns.js");
const debugFind = (0, debug_1.default)("ic:find");
/**
* Contains connection information for an IntelliCenter controller.
*/
class UnitInfo {
name;
hostname;
port;
address;
get addressStr() {
return (0, dns_js_1.ipToString)(this.address);
}
constructor(_name, _hostname, _port, _address) {
this.name = _name;
this.hostname = _hostname;
this.port = _port;
this.address = _address;
}
}
exports.UnitInfo = UnitInfo;
/**
* Broadcasts mDNS packets to the local network to identify any Pentair IntelliCenter controllers connected to it.
*
* Available events:
*
* * `"close"` - fired when the search socket has closed
* * `"error"` - fired when an unrecoverable error has occurred in the search socket
* * `"serverFound"` - fired immediately when an IntelliCenter unit has been located; receives a {@linkcode UnitInfo} argument
*/
class FindUnits extends events_1.EventEmitter {
broadcastInterface;
/**
* Creates a new finder.
*
* @param broadcastInterface the address of the interface to send the broadcast to. If not specified, will use system selection. Only necessary if you have more than one network adapter/interface and want to search on a specific one.
*/
constructor(broadcastInterface) {
super();
this.broadcastInterface = broadcastInterface;
// construct mDNS packet to ping for intellicenter controllers
this.message = Buffer.alloc(34);
let offset = 0;
offset = this.message.writeUInt16BE(0, offset); // transaction id
offset = this.message.writeUInt16BE(0, offset); // flags: 0x0 (standard query)
offset = this.message.writeUInt16BE(1, offset); // asking 1 question
offset = this.message.writeUInt16BE(0, offset); // answer rr
offset = this.message.writeUInt16BE(0, offset); // authority rr
offset = this.message.writeUInt16BE(0, offset); // additional rr
offset = this.message.writeUInt8("_http".length, offset);
offset += this.message.write("_http", offset);
offset = this.message.writeUInt8("_tcp".length, offset);
offset += this.message.write("_tcp", offset);
offset = this.message.writeUInt8("local".length, offset);
offset += this.message.write("local", offset);
offset = this.message.writeUInt8(0, offset); // no more strings
offset = this.message.writeUInt16BE(dns_js_1.TypePtr, offset); // type
this.message.writeUInt16BE(1, offset); // class: IN
this.finder = (0, dgram_1.createSocket)("udp4");
this.finder
.on("listening", () => {
if (this.broadcastInterface) {
this.finder?.setMulticastInterface(this.broadcastInterface);
}
this.finder?.setBroadcast(true);
this.finder?.setMulticastTTL(128);
if (!this.bound) {
this.bound = true;
this.sendServerBroadcast();
}
})
.on("message", (msg) => {
this.foundServer(msg);
})
.on("close", () => {
debugFind("Finder socket closed.");
this.emit("close");
})
.on("error", (e) => {
debugFind("Finder socket error: %O", e);
this.emit("error", e);
});
}
finder;
bound = false;
message;
units = [];
/**
* Begins a search and returns immediately. Must close the finder with close() when done with all searches.
* Subscribe to the `"serverFound"` event to receive connected unit information.
*/
search() {
if (!this.finder) {
debugFind("Cannot search with a closed finder.");
return;
}
if (!this.bound) {
this.finder.bind();
}
else {
this.sendServerBroadcast();
}
}
/**
* Searches for the given amount of time. Must close the finder with close() when done with all searches.
*
* @param searchTimeMs the number of milliseconds to search before giving up and returning found results (default: 5000)
* @returns Promise resolving to a list of discovered {@linkcode UnitInfo}, if any.
*/
async searchAsync(searchTimeMs) {
const p = new Promise((resolve) => {
setTimeout(() => {
if (this.units.length === 0) {
debugFind("No units found searching locally.");
}
this.removeAllListeners();
resolve(this.units);
}, searchTimeMs ?? 5000);
this.on("serverFound", (unit) => {
debugFind(" found: %o", unit);
this.units.push(unit);
});
this.search();
});
return p;
}
foundServer(msg) {
let flags = 0;
if (msg.length > 4) {
flags = msg.readUInt16BE(2);
const answerBit = 1 << 15;
if ((flags & answerBit) === 0) {
// received query, don't process as answer
return;
}
}
let nextAnswerOffset = 12;
let questions = 0;
if (msg.length >= 6) {
questions = msg.readUInt16BE(4);
let nextQuestionOffset = 12;
for (let i = 0; i < questions; i++) {
const parsed = (0, dns_js_1.GetDNSQuestion)(msg, nextQuestionOffset);
nextQuestionOffset = parsed.endOffset;
}
nextAnswerOffset = nextQuestionOffset;
}
let answers = 0;
if (msg.length >= 8) {
answers = msg.readUInt16BE(6);
}
const records = [];
if (answers > 0) {
for (let i = 0; i < answers; i++) {
if (msg.length <= nextAnswerOffset) {
console.error(`while inspecting dns answers, expected message length > ${nextAnswerOffset.toString()} but it was ${msg.length.toString()}`);
break;
}
const answer = (0, dns_js_1.GetDNSAnswer)(msg, nextAnswerOffset);
if (!answer) {
break;
}
records.push(answer);
nextAnswerOffset = answer.endOffset;
}
}
if (records.find((r) => r.name.startsWith("Pentair -i"))) {
const srv = records.find((r) => r instanceof dns_js_1.SrvRecord);
const a = records.find((r) => r instanceof dns_js_1.ARecord);
if (!srv || !a) {
return;
}
const unit = new UnitInfo(srv.name, a.name, srv.port, a.address);
this.emit("serverFound", unit);
}
else {
debugFind(" found something that wasn't an IntelliCenter unit: %s", records
.filter((r) => r instanceof dns_js_1.PtrRecord)
.map((r) => r.domain)
.join(", "));
}
}
sendServerBroadcast() {
if (!this.finder) {
debugFind("Cannot send server broadcast with a closed finder.");
return;
}
this.finder.send(this.message, 0, this.message.length, 5353, "224.0.0.251");
debugFind("Looking for IntelliCenter hosts...");
}
/**
* Closes the finder socket. Finder is no longer usable after this.
*/
close() {
this.finder?.close();
}
}
exports.FindUnits = FindUnits;
//# sourceMappingURL=finder.js.map