bigassfans
Version:
A library for discovering and controlling BigAssFans
414 lines (413 loc) • 14.4 kB
JavaScript
"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;