UNPKG

bigassfans

Version:

A library for discovering and controlling BigAssFans

414 lines (413 loc) 14.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BigAssFan = void 0; const net = __importStar(require("net")); const SLIP = __importStar(require("./SLIP")); const events_1 = require("events"); const schema_1 = require("./schema"); const fan_1 = require("./proto/fan"); const logging_1 = require("./logging"); const discovery_1 = require("./discovery"); class PropertyStore extends events_1.EventEmitter { store = {}; get(key) { return this.store[key]; } set(key, value) { this.store[key] = value; this.emit(key, value); } waitForChange(key) { return new Promise(resolve => this.once(key, resolve)); } } class BigAssDevice { // Event Emitter emitter = new events_1.EventEmitter(); on = this.emitter.on; once = this.emitter.once; emit = this.emitter.emit; // Online status ip; port; failCount = 0; ready = false; fatal = false; // For TCP Handling connection; tcp_leftovers = null; slip_open = false; slip_start = null; // Device State properties = new PropertyStore(); constructor(ip, port) { this.connection = this.createConnection(ip, port); this.connection.on("connect", this.onConnected.bind(this)); this.connection.on("error", this.onError.bind(this)); this.connection.on("data", this.onTCPChunk.bind(this)); this.connection.on("ready", this.onReady.bind(this)); this.ip = ip; this.port = port; } onReady() { this.emit("online"); } onConnected() { // Send initial query for all data this.sendQuery(fan_1.Query_Type.All); } onError(err) { this.failCount++; (0, logging_1.DEBUG)(err); if (err.name == "ECONNREFUSED") { this.fatalError(err.name); } else if (err.name == "EHOSTUNREACH") { this.fatalError(err.name); } else { if (this.failCount >= 2) { (0, logging_1.DEBUG)(`BigAssDevice (${this.ip}): Has failed too may times and will now quit`); this.fatalError("Too Many connection erros"); } else { (0, logging_1.DEBUG)(`BigAssDevice (${this.ip}): Has failed and will retry`); this.connection = this.createConnection(this.ip, this.port); } } } createConnection(ip, port) { return net.createConnection({ host: ip, port: port, family: 4, // As far as I know, BaF is only IPv4 :( keepAlive: true }); } onTCPChunk(b) { const data = Uint8Array.from(b); for (let i = 0; i < data.length; i++) { if (data[i] == 0xC0) { if (!this.slip_open) { this.slip_open = true; this.slip_start = i; } else { if (this.slip_open === null) throw "theoretically impossible behavior"; if (this.slip_start === null) throw "theoretically impossible behavior"; let packet; if (this.tcp_leftovers === null) { packet = data.subarray(this.slip_start, i + 1); } else { const totalLength = this.tcp_leftovers.length + data.length; const combined = new Uint8Array(totalLength); combined.set(this.tcp_leftovers); combined.set(data, this.tcp_leftovers.length); packet = combined.subarray(this.slip_start, this.tcp_leftovers.length + i + 1); } this.handlePacket(packet); this.slip_start = null; this.slip_open = false; this.tcp_leftovers = null; } } } if (this.slip_open) { this.tcp_leftovers = data; } } handlePacket(packet) { const protodata = SLIP.unwrap(packet); if (protodata === null) return (0, logging_1.DEBUG)(`failed to decode packet: ${packet}`); const decodedProto = fan_1.ApiMessage.decode(protodata); this.receive(decodedProto); } fatalError(msg) { console.log(`WARN: Fatal Error: ${msg}`); this.fatal = true; this.emit("error", new Error(msg)); this.destroy(); } destroy() { (0, logging_1.DEBUG)(`Destroying connection to ${this.connection.remoteAddress}`); this.ready = false; this.connection.destroy(); } // API Message Handling syncAPIUpdate(properties) { for (let key of Object.keys(properties)) { let newValue = properties[key]; if (newValue !== undefined) { this.properties.set(key, newValue); } } } receive(msg) { (0, logging_1.DEBUG_APIMessage)(msg); msg.inner?.update?.properties.forEach(async (property) => { let safe = await schema_1.PropertiesSchema.partial().parseAsync(property); this.syncAPIUpdate(safe); }); } send(msg) { (0, logging_1.DEBUG)("Sending:"); (0, logging_1.DEBUG)(msg); (0, logging_1.DEBUG_APIMessage)(msg); const protodata = fan_1.ApiMessage.encode(msg); const buffer = protodata.finish(); const encapsulated = SLIP.wrap(buffer); this.connection.write(encapsulated); } // Helper functions sendQuery(type) { this.send({ inner: { query: { type } } }); } sendCommands(commands, scheduleJob) { if (commands === undefined) commands = []; if (scheduleJob === undefined) scheduleJob = []; if (commands.length === 0 && scheduleJob.length === 0) { return (0, logging_1.DEBUG)("No commands or jobs passed to sendCommands. Doing nothing."); } this.send({ inner: { job: { systemAction: undefined, commands, scheduleJob } } }); } // High Level Property Access async get(property) { let current = this.properties.get(property); if (current === undefined) return this.properties.waitForChange(property); else return current; } async set(property, value) { if (this.properties.get(property) == value) { (0, logging_1.DEBUG)(`BigAssDevice.set: ${property} value is same as current (${value}). Will not send packet.`); return value; } this.sendCommands([{ [property]: value }]); return this.properties.waitForChange(property); } } function noCapabilities() { return { "hasTempSensor": false, "hasHumiditySensor": false, "hasOccupancySensor": false, "hasLight": false, "hasLightSensor": false, "hasColorTempControl": false, "hasFan": false, "hasSpeaker": false, "hasPiezo": false, "hasLedIndicators": false, "hasUplight": false, "hasUvcLight": false, "hasStandbyLed": false, "hasEcoMode": false }; } class BigAssFan extends BigAssDevice { // Static Methods static discover = discovery_1.discover; // You MUST include the Generic in a type somewhere, otherwise type inference does not work _features = noCapabilities(); constructor(ip, port) { super(ip, port); this.on("online", this.onOnline.bind(this)); } // Setup capabilities and property accessors async onOnline() { const features = await this.capabilities.get(); this._features = features; if (features.hasFan) this.fan = this._fan; if (features.hasLight) this.light = this._light; if (this.hasFan() && features.hasOccupancySensor) this._fan.occupancy = this._fanOccupancy; if (this.hasLight() && features.hasOccupancySensor) this._light.occupancy = this._lightOccupancy; if (this.hasLight() && features.hasColorTempControl) this._light.temperature = this._colorTemperature; if (features.hasTempSensor) this.sensors.temperature = this._temperature; if (features.hasHumiditySensor) this.sensors.humidity = this._humidity; if (features.hasEcoMode) this.eco = this._eco; // Setup finished. We are ready. this.ready = true; this.emit("ready", this); } createProp(key) { const parent = this; let schema = schema_1.PropertiesSchema.shape[key]; return { async get() { return parent.get(key); }, async set(value) { let validated = await schema.safeParseAsync(value); if (validated.success) { let s = validated.data; return parent.set(key, s); } else { throw new Error("Invalid Property Value"); } }, onChange(func) { parent.properties.on(key, func); } }; } createReadonlyProp(key) { const parent = this; return { async get() { return parent.get(key); }, onChange(func) { parent.properties.on(key, func); } }; } // Type Guards hasFan() { return this._features.hasFan; } hasLight() { return this._features.hasLight; } hasColorTemperature() { return this._features.hasColorTempControl && this._features.hasLight; } hasOccupancy() { return this._features.hasOccupancySensor; } hasSensors() { return this._features.hasTempSensor && this._features.hasHumiditySensor; } hasEco() { return this._features.hasEcoMode; } hasUvc() { return this._features.hasUvcLight; } // Fan Settings name = this.createReadonlyProp("name"); model = this.createReadonlyProp("model"); timeUtc = this.createReadonlyProp("utcTime"); version = this.createReadonlyProp("fwVersion"); macAddress = this.createReadonlyProp("macAddress"); // privated so that implementors use type guards capabilities = this.createReadonlyProp("deviceCapabilities"); deviceId = this.createReadonlyProp("deviceId"); cloudId = this.createReadonlyProp("cloudId"); cloudServerUrl = this.createReadonlyProp("cloudServerUrl"); network = this.createReadonlyProp("network"); indicators = { visual: this.createProp("indicatorsEnabled"), audible: this.createProp("audibleIndicatorEnabled") }; legacyIr = this.createProp("legacyIrEnabled"); group = this.createReadonlyProp("groupContainer"); _fanOccupancy = { enabled: this.createProp("fanOccupancyEnabled"), timeout: this.createProp("fanOccupancyTimeout"), isOccupied: this.createProp("fanOccupied") }; _fan = { mode: this.createProp("fanMode"), direction: this.createProp("fanDirection"), speedPercent: this.createProp("fanPercent"), speed: this.createProp("fanSpeed"), whoosh: this.createProp("whooshEnabled"), occupancy: null, comfortSense: { enabled: this.createProp("comfortSenseEnabled"), idealTemperature: this.createProp("comfortSenseIdealTemp"), minSpeed: this.createProp("comfortSenseMinSpeed"), maxSpeed: this.createProp("comfortSenseMaxSpeed"), heatSense: { enabled: this.createProp("comfortSenseHeatAssistEnabled"), speed: this.createProp("comfortSenseHeatAssistSpeed"), direction: this.createProp("comfortSenseHeatAssistDirection") } }, commandedRpm: this.createProp("commandedRpm"), actualRpm: this.createProp("actualRpm") }; fan = null; _lightOccupancy = { enabled: this.createProp("lightOccupancyEnabled"), timeout: this.createProp("lightOccupancyTimeout"), isOccupied: this.createProp("lightOccupied") }; _colorTemperature = this.createProp("lightColorTemperature"); _light = { mode: this.createProp("lightMode"), percent: this.createProp("lightPercent"), level: this.createProp("lightPercent"), temperature: null, occupancy: null, dimToWarm: this.createProp("lightDimToWarmEnabled") }; light = null; _uvc = { enabled: this.createReadonlyProp("uvcEnabled"), life: this.createReadonlyProp("uvcLife") }; uvc = null; _eco = this.createProp("ecoModeEnabled"); eco = null; _temperature = this.createReadonlyProp("temperature"); _humidity = this.createReadonlyProp("humidity"); sensors = { temperature: null, humidity: null }; } exports.BigAssFan = BigAssFan;