homebridge-weatherflow-tempest
Version:
Exposes WeatherFlow Tempest Station data as Temperature Sensors, Light Sensors, Humidity Sensors and Fan Sensors (for Wind Speed).
232 lines • 9.52 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TempestApi = exports.TempestSocket = void 0;
const axios_1 = __importDefault(require("axios"));
const dgram = __importStar(require("dgram"));
const https_1 = __importDefault(require("https"));
axios_1.default.defaults.timeout = 10000; // same as default interval
axios_1.default.defaults.httpsAgent = new https_1.default.Agent({ keepAlive: true });
class TempestSocket {
constructor(log, reuse_address) {
this.log = log;
this.data = {
air_temperature: 0,
feels_like: 0,
wind_chill: 0,
dew_point: 0,
relative_humidity: 0,
wind_avg: 0,
wind_gust: 0,
barometric_pressure: 0,
precip: 0,
precip_accum_local_day: 0,
wind_direction: 0,
solar_radiation: 0,
uv: 0,
brightness: 0,
lightning_strike_last_epoch: 0,
lightning_strike_last_distance: 0,
};
this.tempest_battery_level = 0;
this.s = dgram.createSocket({ type: 'udp4', reuseAddr: reuse_address });
this.log.info('TempestSocket initialized.');
}
start(address = '0.0.0.0', port = 50222) {
this.setupSocket(address, port);
this.setupSignalHandlers();
}
setupSocket(address, port) {
this.s.bind({ address: address, port: port });
this.s.on('message', (msg) => {
try {
const message_string = msg.toString('utf-8');
const data = JSON.parse(message_string);
this.processReceivedData(data);
}
catch (error) {
this.log.warn('JSON processing of data failed');
this.log.error(error);
}
});
this.s.on('error', (err) => {
this.log.error('Socket error:', err);
});
}
processReceivedData(data) {
if (data.type === 'obs_st') { // Observation event
this.setTempestData(data);
}
else if (data.type === 'evt_strike') { // Lightening strike event
this.appendStrikeEvent(data);
}
}
setTempestData(event) {
const obs = event.obs[0];
// const windLull = (obs[1] !== null) ? obs[1] : 0;
const windSpeed = (obs[2] !== null) ? obs[2] * 2.2369 : 0; // convert to mph for heatindex calculation
const T = (obs[7] * 9 / 5) + 32; // T in F for heatindex, feelsLike and windChill calculations
// eslint-disable-next-line max-len
const heatIndex = -42.379 + 2.04901523 * T + 10.14333127 * obs[8] - 0.22475541 * T * obs[8] - 0.00683783 * (T ** 2) - 0.05481717 * (obs[8] ** 2) + 0.00122874 * (T ** 2) * obs[8] + 0.00085282 * T * (obs[8] ** 2) - 0.00000199 * (T ** 2) * (obs[8] ** 2);
// feels like temperature on defined for temperatures between 80F and 110F
const feelsLike = ((T >= 80) && (T <= 110)) ? heatIndex : T;
// windChill only defined for wind speeds > 3 mph and temperature < 50F
const windChill = ((windSpeed > 3) && (T < 50)) ? (35.74 + 0.6215 * T - 35.75 * (windSpeed ** 0.16) + 0.4275 * T * (windSpeed ** 0.16)) : T;
this.data.air_temperature = obs[7];
this.data.feels_like = 5 / 9 * (feelsLike - 32); // convert back to C
this.data.wind_chill = 5 / 9 * (windChill - 32); // convert back to C
this.data.dew_point = obs[7] - ((100 - obs[8]) / 5.0); // Td = T - ((100 - RH)/5)
this.data.relative_humidity = obs[8];
this.data.wind_avg = obs[2];
this.data.wind_gust = obs[3];
this.data.barometric_pressure = obs[6];
this.data.precip = obs[12];
this.data.precip_accum_local_day = obs[12];
this.data.wind_direction = obs[4];
this.data.solar_radiation = obs[11];
this.data.uv = obs[10];
this.data.brightness = obs[9];
this.tempest_battery_level = Math.round((obs[16] - 1.8) * 100); // 2.80V = 100%, 1.80V = 0%
}
appendStrikeEvent(data) {
if (this.data) {
this.data.lightning_strike_last_epoch = data.evt[0];
this.data.lightning_strike_last_distance = data.evt[1];
}
}
setupSignalHandlers() {
process.on('SIGTERM', () => {
this.log.info('Got SIGTERM, shutting down Tempest Homebridge...');
this.s.close();
});
process.on('SIGINT', () => {
this.log.info('Got SIGINT, shutting down Tempest Homebridge...');
this.s.close();
});
}
hasData() {
return this.data !== undefined;
}
getStationCurrentObservation() {
return this.data;
}
getBatteryLevel() {
return this.tempest_battery_level;
}
}
exports.TempestSocket = TempestSocket;
class TempestApi {
constructor(token, station_id, log) {
this.max_retries = 30;
this.log = log;
this.token = token;
this.station_id = station_id;
this.data = undefined; // last sample of Observation data
this.tempest_device_id = 0;
this.tempest_battery_level = 0;
this.log.info('TempestApi initialized.');
}
async getStationObservation() {
let observation;
const url = `https://swd.weatherflow.com/swd/rest/observations/station/${this.station_id}`;
const options = {
headers: {
'Authorization': `Bearer ${this.token}`,
},
validateStatus: (status) => status >= 200 && status < 300, // Default
};
await axios_1.default.get(url, options)
.then(response => {
observation = response.data['obs'][0];
})
.catch(exception => {
this.log.warn(`[WeatherFlow] ${exception}`);
});
return observation;
}
async delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async getStationCurrentObservation(retry_count) {
if (retry_count === this.max_retries) {
this.log.error(`Reached max API retries: ${this.max_retries}. Stopping.`);
return;
}
const observation = await this.getStationObservation();
if (observation === undefined) {
this.log.warn('Response missing "obs" data.');
if (this.data !== undefined) {
this.log.warn('Returning last cached response.');
return this.data;
}
else {
this.log.warn(`Retrying ${retry_count + 1} of ${this.max_retries}. No cached "obs" data.`);
retry_count += 1;
await this.delay(1000 * retry_count);
return this.getStationCurrentObservation(retry_count);
}
}
else {
this.data = observation;
return this.data;
}
}
async getTempestBatteryLevel(device_id) {
const url = `https://swd.weatherflow.com/swd/rest/observations/device/${device_id}`;
const options = {
headers: {
'Authorization': `Bearer ${this.token}`,
},
validateStatus: (status) => status >= 200 && status < 300, // Default
};
await axios_1.default.get(url, options) // assumes single Tempest station
.then(response => {
const tempest_battery_voltage = response.data.obs[0][16];
this.tempest_battery_level = Math.round((tempest_battery_voltage - 1.8) * 100); // 2.80V = 100%, 1.80V = 0%
})
.catch(exception => {
this.log.warn(`[WeatherFlow] ${exception}`);
});
return this.tempest_battery_level;
}
async getTempestDeviceId() {
const url = `https://swd.weatherflow.com/swd/rest/stations/${this.station_id}`;
const options = {
headers: {
'Authorization': `Bearer ${this.token}`,
},
validateStatus: (status) => status >= 200 && status < 300, // Default
};
await axios_1.default.get(url, options) // assumes single hub with single Tempest station
.then(response => {
this.tempest_device_id = response.data.stations[0].devices[1].device_id;
})
.catch(exception => {
this.log.warn(`[WeatherFlow] ${exception}`);
});
return this.tempest_device_id;
}
}
exports.TempestApi = TempestApi;
//# sourceMappingURL=tempest.js.map