mirror of
https://github.com/parnic/node-intellicenter.git
synced 2025-06-17 02:21:53 -05:00
Finish Finder implementation
This can correctly find and return IntelliCenter units on the local network now. Cleaned up a bunch of Finder code and added back in "debug" module logging. Fixed up types in Dns module so we can use instanceof
This commit is contained in:
23
dist/dns.d.ts
vendored
23
dist/dns.d.ts
vendored
@ -1,40 +1,31 @@
|
|||||||
export declare const TypeTxt = 16;
|
|
||||||
export declare const TypePtr = 12;
|
|
||||||
export declare const TypeSrv = 33;
|
|
||||||
export declare const TypeA = 1;
|
|
||||||
export declare class Question {
|
export declare class Question {
|
||||||
name: string;
|
name: string;
|
||||||
type: number;
|
type: number;
|
||||||
class: number;
|
class: number;
|
||||||
endOffset: number;
|
endOffset: number;
|
||||||
constructor(_name: string, _type: number, _cls: number, _endOffset: number);
|
|
||||||
}
|
}
|
||||||
export interface Record {
|
export declare abstract class Record {
|
||||||
interface: string | undefined;
|
|
||||||
type: number;
|
type: number;
|
||||||
ttlSeconds: number;
|
ttlSeconds: number;
|
||||||
name: string;
|
name: string;
|
||||||
endOffset: number;
|
endOffset: number;
|
||||||
}
|
}
|
||||||
export interface PtrRecord extends Record {
|
export declare class PtrRecord extends Record {
|
||||||
interface: "ptr";
|
|
||||||
domain: string;
|
domain: string;
|
||||||
}
|
}
|
||||||
export interface TxtRecord extends Record {
|
export declare class TxtRecord extends Record {
|
||||||
interface: "txt";
|
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
export interface SrvRecord extends Record {
|
export declare class SrvRecord extends Record {
|
||||||
interface: "srv";
|
|
||||||
priority: number;
|
priority: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
port: number;
|
port: number;
|
||||||
target: string;
|
target: string;
|
||||||
}
|
}
|
||||||
export interface ARecord extends Record {
|
export declare class ARecord extends Record {
|
||||||
interface: "a";
|
|
||||||
address: number;
|
address: number;
|
||||||
addressStr: string;
|
get addressStr(): string;
|
||||||
}
|
}
|
||||||
|
export declare function ipToString(ip: number): string;
|
||||||
export declare function GetDNSQuestion(msg: Buffer, startOffset: number): Question;
|
export declare function GetDNSQuestion(msg: Buffer, startOffset: number): Question;
|
||||||
export declare function GetDNSAnswer(msg: Buffer, startOffset: number): Record | undefined;
|
export declare function GetDNSAnswer(msg: Buffer, startOffset: number): Record | undefined;
|
||||||
|
121
dist/dns.js
vendored
121
dist/dns.js
vendored
@ -1,19 +1,45 @@
|
|||||||
export const TypeTxt = 16;
|
const TypeTxt = 16;
|
||||||
export const TypePtr = 12;
|
const TypePtr = 12;
|
||||||
export const TypeSrv = 33;
|
const TypeSrv = 33;
|
||||||
export const TypeA = 1;
|
const TypeA = 1;
|
||||||
export class Question {
|
export class Question {
|
||||||
name = "";
|
name = "";
|
||||||
type = 0;
|
type = 0;
|
||||||
class = 0;
|
class = 0;
|
||||||
endOffset = 0;
|
endOffset = 0;
|
||||||
constructor(_name, _type, _cls, _endOffset) {
|
}
|
||||||
this.name = _name;
|
export class Record {
|
||||||
this.type = _type;
|
type = 0;
|
||||||
this.class = _cls;
|
ttlSeconds = 0;
|
||||||
this.endOffset = _endOffset;
|
name = "";
|
||||||
|
endOffset = -1;
|
||||||
|
}
|
||||||
|
export class PtrRecord extends Record {
|
||||||
|
domain = "";
|
||||||
|
}
|
||||||
|
export class TxtRecord extends Record {
|
||||||
|
text = "";
|
||||||
|
}
|
||||||
|
export class SrvRecord extends Record {
|
||||||
|
priority = 0;
|
||||||
|
weight = 0;
|
||||||
|
port = 0;
|
||||||
|
target = "";
|
||||||
|
}
|
||||||
|
export class ARecord extends Record {
|
||||||
|
address = 0;
|
||||||
|
get addressStr() {
|
||||||
|
return ipToString(this.address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export function ipToString(ip) {
|
||||||
|
const o1 = (ip >> 24) & 0xff;
|
||||||
|
const o2 = (ip >> 16) & 0xff;
|
||||||
|
const o3 = (ip >> 8) & 0xff;
|
||||||
|
const o4 = (ip >> 0) & 0xff;
|
||||||
|
const addressStr = `${o1.toString()}.${o2.toString()}.${o3.toString()}.${o4.toString()}`;
|
||||||
|
return addressStr;
|
||||||
|
}
|
||||||
class dnsAnswerParseResult {
|
class dnsAnswerParseResult {
|
||||||
name = "";
|
name = "";
|
||||||
endOffset = 0;
|
endOffset = 0;
|
||||||
@ -58,7 +84,12 @@ export function GetDNSQuestion(msg, startOffset) {
|
|||||||
offset += 2;
|
offset += 2;
|
||||||
const cls = msg.readUInt16BE(offset);
|
const cls = msg.readUInt16BE(offset);
|
||||||
offset += 2;
|
offset += 2;
|
||||||
return new Question(parsedResult.name, type, cls, offset);
|
const ret = new Question();
|
||||||
|
ret.name = parsedResult.name;
|
||||||
|
ret.type = type;
|
||||||
|
ret.class = cls;
|
||||||
|
ret.endOffset = offset;
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
export function GetDNSAnswer(msg, startOffset) {
|
export function GetDNSAnswer(msg, startOffset) {
|
||||||
let offset = startOffset;
|
let offset = startOffset;
|
||||||
@ -74,26 +105,22 @@ export function GetDNSAnswer(msg, startOffset) {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case TypePtr: {
|
case TypePtr: {
|
||||||
const domainResult = parseDnsName(msg, offset);
|
const domainResult = parseDnsName(msg, offset);
|
||||||
const ret = {
|
const ret = new PtrRecord();
|
||||||
interface: "ptr",
|
ret.type = type;
|
||||||
type: type,
|
ret.ttlSeconds = ttlSeconds;
|
||||||
ttlSeconds: ttlSeconds,
|
ret.name = parsedResult.name;
|
||||||
name: parsedResult.name,
|
ret.endOffset = offset + rDataLength;
|
||||||
endOffset: offset + rDataLength,
|
ret.domain = domainResult.name;
|
||||||
domain: domainResult.name,
|
|
||||||
};
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
case TypeTxt: {
|
case TypeTxt: {
|
||||||
const textResult = parseDnsName(msg, offset);
|
const textResult = parseDnsName(msg, offset);
|
||||||
const ret = {
|
const ret = new TxtRecord();
|
||||||
interface: "txt",
|
ret.type = type;
|
||||||
type: type,
|
ret.ttlSeconds = ttlSeconds;
|
||||||
ttlSeconds: ttlSeconds,
|
ret.name = parsedResult.name;
|
||||||
name: parsedResult.name,
|
ret.endOffset = offset + rDataLength;
|
||||||
endOffset: offset + rDataLength,
|
ret.text = textResult.name;
|
||||||
text: textResult.name,
|
|
||||||
};
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
case TypeSrv: {
|
case TypeSrv: {
|
||||||
@ -101,35 +128,25 @@ export function GetDNSAnswer(msg, startOffset) {
|
|||||||
const weight = msg.readUInt16BE(offset + 2);
|
const weight = msg.readUInt16BE(offset + 2);
|
||||||
const port = msg.readUInt16BE(offset + 4);
|
const port = msg.readUInt16BE(offset + 4);
|
||||||
const targetResult = parseDnsName(msg, offset + 6);
|
const targetResult = parseDnsName(msg, offset + 6);
|
||||||
const ret = {
|
const ret = new SrvRecord();
|
||||||
interface: "srv",
|
ret.type = type;
|
||||||
type: type,
|
ret.ttlSeconds = ttlSeconds;
|
||||||
ttlSeconds: ttlSeconds,
|
ret.name = parsedResult.name;
|
||||||
name: parsedResult.name,
|
ret.endOffset = offset + rDataLength;
|
||||||
endOffset: offset + rDataLength,
|
ret.priority = priority;
|
||||||
priority: priority,
|
ret.weight = weight;
|
||||||
weight: weight,
|
ret.port = port;
|
||||||
port: port,
|
ret.target = targetResult.name;
|
||||||
target: targetResult.name,
|
|
||||||
};
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
case TypeA: {
|
case TypeA: {
|
||||||
const o1 = msg.readUInt8(offset);
|
const address = msg.readUInt32BE(offset);
|
||||||
const o2 = msg.readUInt8(offset + 1);
|
const ret = new ARecord();
|
||||||
const o3 = msg.readUInt8(offset + 2);
|
ret.type = type;
|
||||||
const o4 = msg.readUInt8(offset + 3);
|
ret.ttlSeconds = ttlSeconds;
|
||||||
const address = (o1 << 24) | (o2 << 16) | (o3 << 8) | (o4 << 0);
|
ret.name = parsedResult.name;
|
||||||
const addressStr = `${o1.toString()}.${o2.toString()}.${o3.toString()}.${o4.toString()}`;
|
ret.endOffset = offset + rDataLength;
|
||||||
const ret = {
|
ret.address = address;
|
||||||
interface: "a",
|
|
||||||
type: type,
|
|
||||||
ttlSeconds: ttlSeconds,
|
|
||||||
name: parsedResult.name,
|
|
||||||
endOffset: offset + rDataLength,
|
|
||||||
address: address,
|
|
||||||
addressStr: addressStr,
|
|
||||||
};
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
2
dist/dns.js.map
vendored
2
dist/dns.js.map
vendored
File diff suppressed because one or more lines are too long
27
dist/finder.d.ts
vendored
27
dist/finder.d.ts
vendored
@ -1,13 +1,32 @@
|
|||||||
import * as dgram from "dgram";
|
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
export declare class UnitInfo {
|
||||||
|
name: string;
|
||||||
|
hostname: string;
|
||||||
|
port: number;
|
||||||
|
address: number;
|
||||||
|
get addressStr(): string;
|
||||||
|
constructor(_name: string, _hostname: string, _port: number, _address: number);
|
||||||
|
}
|
||||||
export declare class FindUnits extends EventEmitter {
|
export declare class FindUnits extends EventEmitter {
|
||||||
constructor();
|
constructor();
|
||||||
private finder;
|
private finder;
|
||||||
private bound;
|
private bound;
|
||||||
private message;
|
private message;
|
||||||
|
private units;
|
||||||
|
/**
|
||||||
|
* Begins a search and returns immediately. Must close the finder with close() when done with all searches.
|
||||||
|
*/
|
||||||
search(): void;
|
search(): void;
|
||||||
searchAsync(searchTimeMs?: number): Promise<void>;
|
/**
|
||||||
foundServer(msg: Buffer, remote: dgram.RemoteInfo): void;
|
* Searches for the given amount of time. Must close the finder with close() when done with all searches.
|
||||||
sendServerBroadcast(): void;
|
* @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 units, if any.
|
||||||
|
*/
|
||||||
|
searchAsync(searchTimeMs?: number): Promise<UnitInfo[]>;
|
||||||
|
private foundServer;
|
||||||
|
private sendServerBroadcast;
|
||||||
|
/**
|
||||||
|
* Closes the finder socket.
|
||||||
|
*/
|
||||||
close(): void;
|
close(): void;
|
||||||
}
|
}
|
||||||
|
138
dist/finder.js
vendored
138
dist/finder.js
vendored
@ -1,35 +1,28 @@
|
|||||||
import * as dgram from "dgram";
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
import { createSocket } from "dgram";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { setTimeout as setTimeoutSync } from "timers";
|
import debug from "debug";
|
||||||
import os from "os";
|
import { ARecord, GetDNSAnswer, GetDNSQuestion, ipToString, PtrRecord, SrvRecord, } from "./dns.js";
|
||||||
import { GetDNSAnswer, GetDNSQuestion } from "./dns.js";
|
const debugFind = debug("ic:find");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
export class FindUnits extends EventEmitter {
|
export class FindUnits extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const infs = os.networkInterfaces();
|
|
||||||
const localIps = [];
|
|
||||||
let localIp = "127.0.0.1";
|
|
||||||
Object.keys(infs).forEach((key) => {
|
|
||||||
infs[key]?.forEach((iface) => {
|
|
||||||
if (iface.internal) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (iface.family !== "IPv4") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
localIps.push(iface.address);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (localIps.length === 0) {
|
|
||||||
console.error(`no local interfaces found, can't search for controllers.`);
|
|
||||||
// todo: emit error
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
localIp = localIps[0];
|
|
||||||
}
|
|
||||||
if (localIps.length > 1) {
|
|
||||||
console.log(`found ${localIps.length.toString()} local IPs, using the first one for SSDP search (${localIp})`);
|
|
||||||
}
|
|
||||||
// construct mDNS packet to ping for intellicenter controllers
|
// construct mDNS packet to ping for intellicenter controllers
|
||||||
this.message = Buffer.alloc(34);
|
this.message = Buffer.alloc(34);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
@ -48,7 +41,7 @@ export class FindUnits extends EventEmitter {
|
|||||||
offset = this.message.writeUInt8(0, offset); // no more strings
|
offset = this.message.writeUInt8(0, offset); // no more strings
|
||||||
offset = this.message.writeUInt16BE(0x000c, offset); // type: ptr
|
offset = this.message.writeUInt16BE(0x000c, offset); // type: ptr
|
||||||
this.message.writeUInt16BE(1, offset); // class: IN
|
this.message.writeUInt16BE(1, offset); // class: IN
|
||||||
this.finder = dgram.createSocket("udp4");
|
this.finder = createSocket("udp4");
|
||||||
this.finder
|
this.finder
|
||||||
.on("listening", () => {
|
.on("listening", () => {
|
||||||
this.finder.setBroadcast(true);
|
this.finder.setBroadcast(true);
|
||||||
@ -58,23 +51,25 @@ export class FindUnits extends EventEmitter {
|
|||||||
this.sendServerBroadcast();
|
this.sendServerBroadcast();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("message", (msg, remote) => {
|
.on("message", (msg) => {
|
||||||
this.foundServer(msg, remote);
|
this.foundServer(msg);
|
||||||
})
|
})
|
||||||
.on("close", () => {
|
.on("close", () => {
|
||||||
// debugFind("closed");
|
debugFind("Finder socket closed.");
|
||||||
console.log("closed");
|
|
||||||
this.emit("close");
|
this.emit("close");
|
||||||
})
|
})
|
||||||
.on("error", (e) => {
|
.on("error", (e) => {
|
||||||
// debugFind("error: %O", e);
|
debugFind("Finder socket error: %O", e);
|
||||||
console.log("errored");
|
|
||||||
this.emit("error", e);
|
this.emit("error", e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finder;
|
finder;
|
||||||
bound = false;
|
bound = false;
|
||||||
message;
|
message;
|
||||||
|
units = [];
|
||||||
|
/**
|
||||||
|
* Begins a search and returns immediately. Must close the finder with close() when done with all searches.
|
||||||
|
*/
|
||||||
search() {
|
search() {
|
||||||
if (!this.bound) {
|
if (!this.bound) {
|
||||||
this.finder.bind();
|
this.finder.bind();
|
||||||
@ -83,27 +78,29 @@ export class FindUnits extends EventEmitter {
|
|||||||
this.sendServerBroadcast();
|
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 units, if any.
|
||||||
|
*/
|
||||||
async searchAsync(searchTimeMs) {
|
async searchAsync(searchTimeMs) {
|
||||||
const p = new Promise((resolve) => {
|
const p = new Promise((resolve) => {
|
||||||
// debugFind("IntelliCenter finder searching for local units...");
|
setTimeout(() => {
|
||||||
setTimeoutSync(() => {
|
if (this.units.length === 0) {
|
||||||
// if (units.length === 0) {
|
debugFind("No units found searching locally.");
|
||||||
// debugFind("No units found searching locally.");
|
}
|
||||||
// }
|
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
resolve(0);
|
resolve(this.units);
|
||||||
}, searchTimeMs ?? 5000);
|
}, searchTimeMs ?? 5000);
|
||||||
this.on("serverFound", () => {
|
this.on("serverFound", (unit) => {
|
||||||
// debugFind(`IntelliCenter found unit ${JSON.stringify(unit)}`);
|
debugFind(" found: %o", unit);
|
||||||
console.log("found");
|
this.units.push(unit);
|
||||||
// units.push(unit);
|
|
||||||
});
|
});
|
||||||
this.search();
|
this.search();
|
||||||
});
|
});
|
||||||
return Promise.resolve(p);
|
return p;
|
||||||
}
|
}
|
||||||
foundServer(msg, remote) {
|
foundServer(msg) {
|
||||||
// debugFind("found something");
|
|
||||||
let flags = 0;
|
let flags = 0;
|
||||||
if (msg.length > 4) {
|
if (msg.length > 4) {
|
||||||
flags = msg.readUInt16BE(2);
|
flags = msg.readUInt16BE(2);
|
||||||
@ -128,6 +125,7 @@ export class FindUnits extends EventEmitter {
|
|||||||
if (msg.length >= 8) {
|
if (msg.length >= 8) {
|
||||||
answers = msg.readUInt16BE(6);
|
answers = msg.readUInt16BE(6);
|
||||||
}
|
}
|
||||||
|
const records = [];
|
||||||
if (answers > 0) {
|
if (answers > 0) {
|
||||||
for (let i = 0; i < answers; i++) {
|
for (let i = 0; i < answers; i++) {
|
||||||
if (msg.length <= nextAnswerOffset) {
|
if (msg.length <= nextAnswerOffset) {
|
||||||
@ -138,45 +136,33 @@ export class FindUnits extends EventEmitter {
|
|||||||
if (!answer) {
|
if (!answer) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
records.push(answer);
|
||||||
nextAnswerOffset = answer.endOffset;
|
nextAnswerOffset = answer.endOffset;
|
||||||
if (answer.interface === "a") {
|
|
||||||
console.log("a record:", answer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const str = msg.toString();
|
if (records.find((r) => r.name.startsWith("Pentair -i"))) {
|
||||||
console.log(str);
|
const srv = records.find((r) => r instanceof SrvRecord);
|
||||||
if (msg.length >= 40) {
|
const a = records.find((r) => r instanceof ARecord);
|
||||||
const server = {
|
if (!srv || !a) {
|
||||||
address: remote.address,
|
return;
|
||||||
type: msg.readInt32LE(0),
|
|
||||||
port: msg.readInt16LE(8),
|
|
||||||
gatewayType: msg.readUInt8(10),
|
|
||||||
gatewaySubtype: msg.readUInt8(11),
|
|
||||||
gatewayName: msg.toString("utf8", 12, 29),
|
|
||||||
};
|
|
||||||
// debugFind(
|
|
||||||
// " type: " +
|
|
||||||
// server.type +
|
|
||||||
// ", host: " +
|
|
||||||
// server.address +
|
|
||||||
// ":" +
|
|
||||||
// server.port +
|
|
||||||
// ", identified as " +
|
|
||||||
// server.gatewayName,
|
|
||||||
// );
|
|
||||||
if (server.type === 2) {
|
|
||||||
this.emit("serverFound", server);
|
|
||||||
}
|
}
|
||||||
|
const unit = new UnitInfo(srv.name, a.name, srv.port, a.address);
|
||||||
|
this.emit("serverFound", unit);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// debugFind(" unexpected message");
|
debugFind(" found something that wasn't an IntelliCenter unit: %s", records
|
||||||
|
.filter((r) => r instanceof PtrRecord)
|
||||||
|
.map((r) => r.domain)
|
||||||
|
.join(", "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendServerBroadcast() {
|
sendServerBroadcast() {
|
||||||
this.finder.send(this.message, 0, this.message.length, 5353, "224.0.0.251");
|
this.finder.send(this.message, 0, this.message.length, 5353, "224.0.0.251");
|
||||||
// debugFind("Looking for IntelliCenter hosts...");
|
debugFind("Looking for IntelliCenter hosts...");
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Closes the finder socket.
|
||||||
|
*/
|
||||||
close() {
|
close() {
|
||||||
this.finder.close();
|
this.finder.close();
|
||||||
}
|
}
|
||||||
|
2
dist/finder.js.map
vendored
2
dist/finder.js.map
vendored
File diff suppressed because one or more lines are too long
18
dist/index.js
vendored
18
dist/index.js
vendored
@ -4,12 +4,20 @@ import { v4 as uuidv4 } from "uuid";
|
|||||||
import { FindUnits } from "./finder.js";
|
import { FindUnits } from "./finder.js";
|
||||||
console.log("searching...");
|
console.log("searching...");
|
||||||
const f = new FindUnits();
|
const f = new FindUnits();
|
||||||
await f.searchAsync(5000);
|
const units = await f.searchAsync(1000);
|
||||||
// temp. replace with the IP of your device
|
f.close();
|
||||||
const endpoint = "10.0.0.41";
|
console.log("Discovered units:", units);
|
||||||
|
if (units.length === 0) {
|
||||||
|
throw new Error("no IntelliCenter units found, exiting.");
|
||||||
|
}
|
||||||
|
if (units.length > 1) {
|
||||||
|
throw new Error(`found more than one IntelliCenter unit, unsure which one to use. ${JSON.stringify(units)}`);
|
||||||
|
}
|
||||||
|
const endpoint = units[0].addressStr;
|
||||||
|
const port = units[0].port;
|
||||||
let pingTimeout;
|
let pingTimeout;
|
||||||
console.log("connecting to intellicenter device at", endpoint);
|
console.log("connecting to intellicenter device at", endpoint, "port", port);
|
||||||
const client = new WebSocket(`ws://${endpoint}:6680`);
|
const client = new WebSocket(`ws://${endpoint}:${port.toString()}`);
|
||||||
const heartbeat = () => {
|
const heartbeat = () => {
|
||||||
clearTimeout(pingTimeout);
|
clearTimeout(pingTimeout);
|
||||||
pingTimeout = setTimeout(() => {
|
pingTimeout = setTimeout(() => {
|
||||||
|
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AAC5B,MAAM,CAAC,GAAG,IAAI,SAAS,EAAE,CAAC;AAC1B,MAAM,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AAE1B,2CAA2C;AAC3C,MAAM,QAAQ,GAAG,WAAW,CAAC;AAE7B,IAAI,WAA0C,CAAC;AAE/C,OAAO,CAAC,GAAG,CAAC,uCAAuC,EAAE,QAAQ,CAAC,CAAC;AAC/D,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,QAAQ,QAAQ,OAAO,CAAC,CAAC;AAEtD,MAAM,SAAS,GAAG,GAAG,EAAE;IACrB,YAAY,CAAC,WAAW,CAAC,CAAC;IAE1B,WAAW,GAAG,UAAU,CAAC,GAAG,EAAE;QAC5B,MAAM,CAAC,SAAS,EAAE,CAAC;IACrB,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,CAAC;AACnB,CAAC,CAAC;AAEF,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AAClC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAC7B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAC7B,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;IACtB,YAAY,CAAC,WAAW,CAAC,CAAC;AAC5B,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAW,EAAE,EAAE;IACnC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,8DAA8D;IAC1G,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC,CAAC,CAAC;AACH,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACpC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC7B,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC/B,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACX,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACzB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAClC,MAAM,GAAG,GAAG;QACV,SAAS,EAAE,EAAE;QACb,UAAU,EAAE;YACV;gBACE,MAAM,EAAE,OAAO;gBACf,IAAI,EAAE;oBACJ,KAAK;oBACL,MAAM;oBACN,KAAK;oBACL,QAAQ;oBACR,UAAU;oBACV,MAAM;oBACN,SAAS;oBACT,MAAM;oBACN,OAAO;oBACP,OAAO;oBACP,QAAQ;oBACR,OAAO;oBACP,QAAQ;oBACR,SAAS;oBACT,OAAO;oBACP,MAAM;oBACN,MAAM;oBACN,OAAO;oBACP,SAAS;oBACT,QAAQ;oBACR,UAAU;oBACV,IAAI;oBACJ,OAAO;oBACP,SAAS;iBACV;aACF;SACF;QACD,OAAO,EAAE,cAAc;QACvB,SAAS,EAAE,MAAM,EAAE;KACpB,CAAC;IACF,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,MAAM,CAAC,KAAK,EAAE,CAAC;AACjB,CAAC,CAAC,CAAC"}
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
158
dns.ts
158
dns.ts
@ -1,52 +1,52 @@
|
|||||||
export const TypeTxt = 16;
|
const TypeTxt = 16;
|
||||||
export const TypePtr = 12;
|
const TypePtr = 12;
|
||||||
export const TypeSrv = 33;
|
const TypeSrv = 33;
|
||||||
export const TypeA = 1;
|
const TypeA = 1;
|
||||||
|
|
||||||
export class Question {
|
export class Question {
|
||||||
public name = "";
|
public name = "";
|
||||||
public type = 0;
|
public type = 0;
|
||||||
public class = 0;
|
public class = 0;
|
||||||
public endOffset = 0;
|
public endOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(_name: string, _type: number, _cls: number, _endOffset: number) {
|
export abstract class Record {
|
||||||
this.name = _name;
|
public type = 0;
|
||||||
this.type = _type;
|
public ttlSeconds = 0;
|
||||||
this.class = _cls;
|
public name = "";
|
||||||
this.endOffset = _endOffset;
|
public endOffset = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PtrRecord extends Record {
|
||||||
|
public domain = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TxtRecord extends Record {
|
||||||
|
public text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SrvRecord extends Record {
|
||||||
|
public priority = 0;
|
||||||
|
public weight = 0;
|
||||||
|
public port = 0;
|
||||||
|
public target = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ARecord extends Record {
|
||||||
|
public address = 0;
|
||||||
|
|
||||||
|
public get addressStr(): string {
|
||||||
|
return ipToString(this.address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Record {
|
export function ipToString(ip: number): string {
|
||||||
interface: string | undefined;
|
const o1 = (ip >> 24) & 0xff;
|
||||||
type: number;
|
const o2 = (ip >> 16) & 0xff;
|
||||||
ttlSeconds: number;
|
const o3 = (ip >> 8) & 0xff;
|
||||||
name: string;
|
const o4 = (ip >> 0) & 0xff;
|
||||||
endOffset: number;
|
const addressStr = `${o1.toString()}.${o2.toString()}.${o3.toString()}.${o4.toString()}`;
|
||||||
}
|
return addressStr;
|
||||||
|
|
||||||
export interface PtrRecord extends Record {
|
|
||||||
interface: "ptr";
|
|
||||||
domain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TxtRecord extends Record {
|
|
||||||
interface: "txt";
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SrvRecord extends Record {
|
|
||||||
interface: "srv";
|
|
||||||
priority: number;
|
|
||||||
weight: number;
|
|
||||||
port: number;
|
|
||||||
target: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ARecord extends Record {
|
|
||||||
interface: "a";
|
|
||||||
address: number;
|
|
||||||
addressStr: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class dnsAnswerParseResult {
|
class dnsAnswerParseResult {
|
||||||
@ -99,7 +99,13 @@ export function GetDNSQuestion(msg: Buffer, startOffset: number): Question {
|
|||||||
offset += 2;
|
offset += 2;
|
||||||
const cls = msg.readUInt16BE(offset);
|
const cls = msg.readUInt16BE(offset);
|
||||||
offset += 2;
|
offset += 2;
|
||||||
return new Question(parsedResult.name, type, cls, offset);
|
|
||||||
|
const ret = new Question();
|
||||||
|
ret.name = parsedResult.name;
|
||||||
|
ret.type = type;
|
||||||
|
ret.class = cls;
|
||||||
|
ret.endOffset = offset;
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetDNSAnswer(
|
export function GetDNSAnswer(
|
||||||
@ -121,27 +127,25 @@ export function GetDNSAnswer(
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case TypePtr: {
|
case TypePtr: {
|
||||||
const domainResult = parseDnsName(msg, offset);
|
const domainResult = parseDnsName(msg, offset);
|
||||||
const ret: PtrRecord = {
|
|
||||||
interface: "ptr",
|
const ret = new PtrRecord();
|
||||||
type: type,
|
ret.type = type;
|
||||||
ttlSeconds: ttlSeconds,
|
ret.ttlSeconds = ttlSeconds;
|
||||||
name: parsedResult.name,
|
ret.name = parsedResult.name;
|
||||||
endOffset: offset + rDataLength,
|
ret.endOffset = offset + rDataLength;
|
||||||
domain: domainResult.name,
|
ret.domain = domainResult.name;
|
||||||
};
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
case TypeTxt: {
|
case TypeTxt: {
|
||||||
const textResult = parseDnsName(msg, offset);
|
const textResult = parseDnsName(msg, offset);
|
||||||
const ret: TxtRecord = {
|
|
||||||
interface: "txt",
|
const ret = new TxtRecord();
|
||||||
type: type,
|
ret.type = type;
|
||||||
ttlSeconds: ttlSeconds,
|
ret.ttlSeconds = ttlSeconds;
|
||||||
name: parsedResult.name,
|
ret.name = parsedResult.name;
|
||||||
endOffset: offset + rDataLength,
|
ret.endOffset = offset + rDataLength;
|
||||||
text: textResult.name,
|
ret.text = textResult.name;
|
||||||
};
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,37 +155,27 @@ export function GetDNSAnswer(
|
|||||||
const port = msg.readUInt16BE(offset + 4);
|
const port = msg.readUInt16BE(offset + 4);
|
||||||
const targetResult = parseDnsName(msg, offset + 6);
|
const targetResult = parseDnsName(msg, offset + 6);
|
||||||
|
|
||||||
const ret: SrvRecord = {
|
const ret = new SrvRecord();
|
||||||
interface: "srv",
|
ret.type = type;
|
||||||
type: type,
|
ret.ttlSeconds = ttlSeconds;
|
||||||
ttlSeconds: ttlSeconds,
|
ret.name = parsedResult.name;
|
||||||
name: parsedResult.name,
|
ret.endOffset = offset + rDataLength;
|
||||||
endOffset: offset + rDataLength,
|
ret.priority = priority;
|
||||||
priority: priority,
|
ret.weight = weight;
|
||||||
weight: weight,
|
ret.port = port;
|
||||||
port: port,
|
ret.target = targetResult.name;
|
||||||
target: targetResult.name,
|
|
||||||
};
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
case TypeA: {
|
case TypeA: {
|
||||||
const o1 = msg.readUInt8(offset);
|
const address = msg.readUInt32BE(offset);
|
||||||
const o2 = msg.readUInt8(offset + 1);
|
|
||||||
const o3 = msg.readUInt8(offset + 2);
|
|
||||||
const o4 = msg.readUInt8(offset + 3);
|
|
||||||
const address = (o1 << 24) | (o2 << 16) | (o3 << 8) | (o4 << 0);
|
|
||||||
const addressStr = `${o1.toString()}.${o2.toString()}.${o3.toString()}.${o4.toString()}`;
|
|
||||||
|
|
||||||
const ret: ARecord = {
|
const ret = new ARecord();
|
||||||
interface: "a",
|
ret.type = type;
|
||||||
type: type,
|
ret.ttlSeconds = ttlSeconds;
|
||||||
ttlSeconds: ttlSeconds,
|
ret.name = parsedResult.name;
|
||||||
name: parsedResult.name,
|
ret.endOffset = offset + rDataLength;
|
||||||
endOffset: offset + rDataLength,
|
ret.address = address;
|
||||||
address: address,
|
|
||||||
addressStr: addressStr,
|
|
||||||
};
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
176
finder.ts
176
finder.ts
@ -1,41 +1,48 @@
|
|||||||
import * as dgram from "dgram";
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
import { Socket } from "dgram";
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
import { createSocket, Socket } from "dgram";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { setTimeout as setTimeoutSync } from "timers";
|
import debug from "debug";
|
||||||
import os from "os";
|
|
||||||
import { GetDNSAnswer, GetDNSQuestion } from "./dns.js";
|
import {
|
||||||
|
ARecord,
|
||||||
|
GetDNSAnswer,
|
||||||
|
GetDNSQuestion,
|
||||||
|
ipToString,
|
||||||
|
PtrRecord,
|
||||||
|
Record,
|
||||||
|
SrvRecord,
|
||||||
|
} from "./dns.js";
|
||||||
|
|
||||||
|
const debugFind = debug("ic:find");
|
||||||
|
|
||||||
|
export class UnitInfo {
|
||||||
|
public name: string;
|
||||||
|
public hostname: string;
|
||||||
|
public port: number;
|
||||||
|
public address: number;
|
||||||
|
|
||||||
|
public get addressStr(): string {
|
||||||
|
return ipToString(this.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
_name: string,
|
||||||
|
_hostname: string,
|
||||||
|
_port: number,
|
||||||
|
_address: number,
|
||||||
|
) {
|
||||||
|
this.name = _name;
|
||||||
|
this.hostname = _hostname;
|
||||||
|
this.port = _port;
|
||||||
|
this.address = _address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FindUnits extends EventEmitter {
|
export class FindUnits extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
const infs = os.networkInterfaces();
|
|
||||||
const localIps: string[] = [];
|
|
||||||
let localIp = "127.0.0.1";
|
|
||||||
Object.keys(infs).forEach((key) => {
|
|
||||||
infs[key]?.forEach((iface) => {
|
|
||||||
if (iface.internal) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (iface.family !== "IPv4") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
localIps.push(iface.address);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (localIps.length === 0) {
|
|
||||||
console.error(`no local interfaces found, can't search for controllers.`);
|
|
||||||
// todo: emit error
|
|
||||||
} else {
|
|
||||||
localIp = localIps[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localIps.length > 1) {
|
|
||||||
console.log(
|
|
||||||
`found ${localIps.length.toString()} local IPs, using the first one for SSDP search (${localIp})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// construct mDNS packet to ping for intellicenter controllers
|
// construct mDNS packet to ping for intellicenter controllers
|
||||||
this.message = Buffer.alloc(34);
|
this.message = Buffer.alloc(34);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
@ -55,7 +62,7 @@ export class FindUnits extends EventEmitter {
|
|||||||
offset = this.message.writeUInt16BE(0x000c, offset); // type: ptr
|
offset = this.message.writeUInt16BE(0x000c, offset); // type: ptr
|
||||||
this.message.writeUInt16BE(1, offset); // class: IN
|
this.message.writeUInt16BE(1, offset); // class: IN
|
||||||
|
|
||||||
this.finder = dgram.createSocket("udp4");
|
this.finder = createSocket("udp4");
|
||||||
this.finder
|
this.finder
|
||||||
.on("listening", () => {
|
.on("listening", () => {
|
||||||
this.finder.setBroadcast(true);
|
this.finder.setBroadcast(true);
|
||||||
@ -66,17 +73,15 @@ export class FindUnits extends EventEmitter {
|
|||||||
this.sendServerBroadcast();
|
this.sendServerBroadcast();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("message", (msg, remote) => {
|
.on("message", (msg) => {
|
||||||
this.foundServer(msg, remote);
|
this.foundServer(msg);
|
||||||
})
|
})
|
||||||
.on("close", () => {
|
.on("close", () => {
|
||||||
// debugFind("closed");
|
debugFind("Finder socket closed.");
|
||||||
console.log("closed");
|
|
||||||
this.emit("close");
|
this.emit("close");
|
||||||
})
|
})
|
||||||
.on("error", (e) => {
|
.on("error", (e) => {
|
||||||
// debugFind("error: %O", e);
|
debugFind("Finder socket error: %O", e);
|
||||||
console.log("errored");
|
|
||||||
this.emit("error", e);
|
this.emit("error", e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -84,8 +89,12 @@ export class FindUnits extends EventEmitter {
|
|||||||
private finder: Socket;
|
private finder: Socket;
|
||||||
private bound = false;
|
private bound = false;
|
||||||
private message: Buffer;
|
private message: Buffer;
|
||||||
|
private units: UnitInfo[] = [];
|
||||||
|
|
||||||
search() {
|
/**
|
||||||
|
* Begins a search and returns immediately. Must close the finder with close() when done with all searches.
|
||||||
|
*/
|
||||||
|
public search() {
|
||||||
if (!this.bound) {
|
if (!this.bound) {
|
||||||
this.finder.bind();
|
this.finder.bind();
|
||||||
} else {
|
} else {
|
||||||
@ -93,33 +102,34 @@ export class FindUnits extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async searchAsync(searchTimeMs?: number): Promise<void> {
|
/**
|
||||||
const p = new Promise((resolve) => {
|
* Searches for the given amount of time. Must close the finder with close() when done with all searches.
|
||||||
// debugFind("IntelliCenter finder searching for local units...");
|
* @param searchTimeMs the number of milliseconds to search before giving up and returning found results (default: 5000)
|
||||||
setTimeoutSync(() => {
|
* @returns Promise resolving to a list of discovered units, if any.
|
||||||
// if (units.length === 0) {
|
*/
|
||||||
// debugFind("No units found searching locally.");
|
public async searchAsync(searchTimeMs?: number): Promise<UnitInfo[]> {
|
||||||
// }
|
const p = new Promise<UnitInfo[]>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.units.length === 0) {
|
||||||
|
debugFind("No units found searching locally.");
|
||||||
|
}
|
||||||
|
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
resolve(0);
|
resolve(this.units);
|
||||||
}, searchTimeMs ?? 5000);
|
}, searchTimeMs ?? 5000);
|
||||||
|
|
||||||
this.on("serverFound", () => {
|
this.on("serverFound", (unit: UnitInfo) => {
|
||||||
// debugFind(`IntelliCenter found unit ${JSON.stringify(unit)}`);
|
debugFind(" found: %o", unit);
|
||||||
console.log("found");
|
this.units.push(unit);
|
||||||
// units.push(unit);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.search();
|
this.search();
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve(p) as Promise<void>;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
foundServer(msg: Buffer, remote: dgram.RemoteInfo) {
|
private foundServer(msg: Buffer) {
|
||||||
// debugFind("found something");
|
|
||||||
|
|
||||||
let flags = 0;
|
let flags = 0;
|
||||||
if (msg.length > 4) {
|
if (msg.length > 4) {
|
||||||
flags = msg.readUInt16BE(2);
|
flags = msg.readUInt16BE(2);
|
||||||
@ -149,6 +159,7 @@ export class FindUnits extends EventEmitter {
|
|||||||
answers = msg.readUInt16BE(6);
|
answers = msg.readUInt16BE(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const records: Record[] = [];
|
||||||
if (answers > 0) {
|
if (answers > 0) {
|
||||||
for (let i = 0; i < answers; i++) {
|
for (let i = 0; i < answers; i++) {
|
||||||
if (msg.length <= nextAnswerOffset) {
|
if (msg.length <= nextAnswerOffset) {
|
||||||
@ -163,50 +174,39 @@ export class FindUnits extends EventEmitter {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
records.push(answer);
|
||||||
nextAnswerOffset = answer.endOffset;
|
nextAnswerOffset = answer.endOffset;
|
||||||
if (answer.interface === "a") {
|
|
||||||
console.log("a record:", answer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const str = msg.toString();
|
if (records.find((r) => r.name.startsWith("Pentair -i"))) {
|
||||||
console.log(str);
|
const srv = records.find((r) => r instanceof SrvRecord);
|
||||||
|
const a = records.find((r) => r instanceof ARecord);
|
||||||
if (msg.length >= 40) {
|
if (!srv || !a) {
|
||||||
const server = {
|
return;
|
||||||
address: remote.address,
|
|
||||||
type: msg.readInt32LE(0),
|
|
||||||
port: msg.readInt16LE(8),
|
|
||||||
gatewayType: msg.readUInt8(10),
|
|
||||||
gatewaySubtype: msg.readUInt8(11),
|
|
||||||
gatewayName: msg.toString("utf8", 12, 29),
|
|
||||||
};
|
|
||||||
|
|
||||||
// debugFind(
|
|
||||||
// " type: " +
|
|
||||||
// server.type +
|
|
||||||
// ", host: " +
|
|
||||||
// server.address +
|
|
||||||
// ":" +
|
|
||||||
// server.port +
|
|
||||||
// ", identified as " +
|
|
||||||
// server.gatewayName,
|
|
||||||
// );
|
|
||||||
|
|
||||||
if (server.type === 2) {
|
|
||||||
this.emit("serverFound", server);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unit = new UnitInfo(srv.name, a.name, srv.port, a.address);
|
||||||
|
this.emit("serverFound", unit);
|
||||||
} else {
|
} else {
|
||||||
// debugFind(" unexpected message");
|
debugFind(
|
||||||
|
" found something that wasn't an IntelliCenter unit: %s",
|
||||||
|
records
|
||||||
|
.filter((r) => r instanceof PtrRecord)
|
||||||
|
.map((r) => r.domain)
|
||||||
|
.join(", "),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendServerBroadcast() {
|
private sendServerBroadcast() {
|
||||||
this.finder.send(this.message, 0, this.message.length, 5353, "224.0.0.251");
|
this.finder.send(this.message, 0, this.message.length, 5353, "224.0.0.251");
|
||||||
// debugFind("Looking for IntelliCenter hosts...");
|
debugFind("Looking for IntelliCenter hosts...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the finder socket.
|
||||||
|
*/
|
||||||
public close() {
|
public close() {
|
||||||
this.finder.close();
|
this.finder.close();
|
||||||
}
|
}
|
||||||
|
22
index.ts
22
index.ts
@ -6,15 +6,27 @@ import { FindUnits } from "./finder.js";
|
|||||||
|
|
||||||
console.log("searching...");
|
console.log("searching...");
|
||||||
const f = new FindUnits();
|
const f = new FindUnits();
|
||||||
await f.searchAsync(5000);
|
const units = await f.searchAsync(1000);
|
||||||
|
f.close();
|
||||||
|
console.log("Discovered units:", units);
|
||||||
|
|
||||||
// temp. replace with the IP of your device
|
if (units.length === 0) {
|
||||||
const endpoint = "10.0.0.41";
|
throw new Error("no IntelliCenter units found, exiting.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (units.length > 1) {
|
||||||
|
throw new Error(
|
||||||
|
`found more than one IntelliCenter unit, unsure which one to use. ${JSON.stringify(units)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = units[0].addressStr;
|
||||||
|
const port = units[0].port;
|
||||||
|
|
||||||
let pingTimeout: ReturnType<typeof setTimeout>;
|
let pingTimeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
console.log("connecting to intellicenter device at", endpoint);
|
console.log("connecting to intellicenter device at", endpoint, "port", port);
|
||||||
const client = new WebSocket(`ws://${endpoint}:6680`);
|
const client = new WebSocket(`ws://${endpoint}:${port.toString()}`);
|
||||||
|
|
||||||
const heartbeat = () => {
|
const heartbeat = () => {
|
||||||
clearTimeout(pingTimeout);
|
clearTimeout(pingTimeout);
|
||||||
|
49
package-lock.json
generated
49
package-lock.json
generated
@ -9,14 +9,17 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
"@types/ws": "^8.5.13",
|
"@types/ws": "^8.5.13",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"prettier": "3.4.2",
|
"prettier": "3.4.2",
|
||||||
|
"supports-color": "^10.0.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"typescript-eslint": "^8.19.0"
|
"typescript-eslint": "^8.19.0"
|
||||||
},
|
},
|
||||||
@ -255,6 +258,16 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/debug": {
|
||||||
|
"version": "4.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
|
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||||
@ -269,6 +282,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ms": {
|
||||||
|
"version": "0.7.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
||||||
|
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.10.2",
|
"version": "22.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
|
||||||
@ -630,6 +650,19 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chalk/node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@ -676,7 +709,6 @@
|
|||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@ -1231,7 +1263,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
@ -1501,16 +1532,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.0.0.tgz",
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
"integrity": "sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"has-flag": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
|
@ -30,14 +30,17 @@
|
|||||||
"lint": "eslint . && prettier . --check"
|
"lint": "eslint . && prettier . --check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
"@types/ws": "^8.5.13",
|
"@types/ws": "^8.5.13",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"prettier": "3.4.2",
|
"prettier": "3.4.2",
|
||||||
|
"supports-color": "^10.0.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"typescript-eslint": "^8.19.0"
|
"typescript-eslint": "^8.19.0"
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user