mirror of
https://github.com/parnic/node-intellicenter.git
synced 2025-06-16 10:10:13 -05:00
This allows require() and import to work for even better compatibility between CJS and ESM consumers. I dislike that this kills our ability for top-level awaits in example.ts, but seeing as how my primary use case for this library is a commonjs module, I think this is a fair trade-off. Also changed "messages" to not encapsulate its export under the name "messages" to remove some repetition in importing "messages" and still needing to do "messages." to get the methods out. Now it's simple to import each message by name or group them under something like "messages" as desired on a per-library-user basis. Refs: * https://www.kravchyk.com/typescript-npm-package-json-exports/ * https://arethetypeswrong.github.io/ * https://evertpot.com/universal-commonjs-esm-typescript-packages/
192 lines
7.1 KiB
JavaScript
192 lines
7.1 KiB
JavaScript
import { createSocket } from "dgram";
|
|
import { EventEmitter } from "events";
|
|
import debug from "debug";
|
|
import { ARecord, GetDNSAnswer, GetDNSQuestion, ipToString, PtrRecord, SrvRecord, TypePtr, } from "./dns.js";
|
|
const debugFind = debug("ic:find");
|
|
/**
|
|
* Contains connection information for an IntelliCenter controller.
|
|
*/
|
|
export class UnitInfo {
|
|
name;
|
|
hostname;
|
|
port;
|
|
address;
|
|
get addressStr() {
|
|
return ipToString(this.address);
|
|
}
|
|
constructor(_name, _hostname, _port, _address) {
|
|
this.name = _name;
|
|
this.hostname = _hostname;
|
|
this.port = _port;
|
|
this.address = _address;
|
|
}
|
|
}
|
|
/**
|
|
* 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
|
|
*/
|
|
export class FindUnits extends 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(TypePtr, offset); // type
|
|
this.message.writeUInt16BE(1, offset); // class: IN
|
|
this.finder = 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.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 = 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 = 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 SrvRecord);
|
|
const a = records.find((r) => r instanceof 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 PtrRecord)
|
|
.map((r) => r.domain)
|
|
.join(", "));
|
|
}
|
|
}
|
|
sendServerBroadcast() {
|
|
this.finder.send(this.message, 0, this.message.length, 5353, "224.0.0.251");
|
|
debugFind("Looking for IntelliCenter hosts...");
|
|
}
|
|
/**
|
|
* Closes the finder socket.
|
|
*/
|
|
close() {
|
|
this.finder.close();
|
|
}
|
|
}
|
|
//# sourceMappingURL=finder.js.map
|