atmosx-tempest-station
Version:
AtmosphericX Tempest Weather Station Polling - Built for standalone and Project AtmosphericX Integration.
532 lines (526 loc) • 18.7 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/index.ts
var index_exports = {};
__export(index_exports, {
TempestStation: () => TempestStation,
default: () => index_default
});
module.exports = __toCommonJS(index_exports);
// src/bootstrap.ts
var fs = __toESM(require("fs"));
var path = __toESM(require("path"));
var events = __toESM(require("events"));
var jobs = __toESM(require("croner"));
var import_axios = __toESM(require("axios"));
var import_ws = __toESM(require("ws"));
var packages = {
fs,
path,
events,
jobs,
axios: import_axios.default,
crypto,
ws: import_ws.default
};
var cache = {
events: new events.EventEmitter(),
lastWarn: null,
isReady: true
};
var settings = {
api: null,
deviceId: null,
stationId: null,
journal: true
};
var definitions = {
messages: {
client_stopped: `Disconnected from Tempest Weather Station.`,
websocket_established: `Successfully connected to Tempest Weather Station.`,
forecast_fetch_error: `Please make sure you have a valid station ID`,
api_failed: `Request failed. Please check your API key and device ID.`
}
};
// src/utils.ts
var Utils = class {
/**
* @function sleep
* @description
* Pauses execution for a specified number of milliseconds.
*
* @static
* @async
* @param {number} ms
* @returns {Promise<void>}
*/
static sleep(ms) {
return __async(this, null, function* () {
return new Promise((resolve) => setTimeout(resolve, ms));
});
}
/**
* @function warn
* @description
* Emits a log event and prints a warning to the console. Throttles repeated
* warnings within a short interval unless `force` is `true`.
*
* @static
* @param {string} message
* @param {boolean} [force=false]
*/
static warn(message, force = false) {
cache.events.emit("log", message);
if (!settings.journal) return;
if (cache.lastWarn != null && Date.now() - cache.lastWarn < 500 && !force) return;
cache.lastWarn = Date.now();
console.warn(`\x1B[33m[ATMOSX-TEMPEST]\x1B[0m [${(/* @__PURE__ */ new Date()).toLocaleString()}] ${message}`);
}
/**
* @function createHttpRequest
* @description
* Performs an HTTP GET request with default headers and timeout, returning
* either the response data or an error message.
*
* @static
* @template T
* @param {string} url
* @param {types.HTTPSettings} [options]
* @returns {Promise<{ error: boolean; message: T | string }>}
*/
static createHttpRequest(url, options) {
return __async(this, null, function* () {
var _a;
const defaultOptions = {
timeout: 1e4,
headers: {
"User-Agent": "AtmosphericX",
"Accept": "application/geo+json, text/plain, */*; q=0.9",
"Accept-Language": "en-US,en;q=0.9"
}
};
const requestOptions = __spreadProps(__spreadValues(__spreadValues({}, defaultOptions), options), {
headers: __spreadValues(__spreadValues({}, defaultOptions.headers), (_a = options == null ? void 0 : options.headers) != null ? _a : {})
});
try {
const resp = yield packages.axios.get(url, {
headers: requestOptions.headers,
timeout: requestOptions.timeout,
maxRedirects: 0,
validateStatus: (status) => status === 200 || status === 500
});
return { error: false, message: resp.data };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { error: true, message: msg };
}
});
}
/**
* @function mergeClientSettings
* @description
* Recursively merges a ClientSettings object into a target object,
* preserving nested structures and overriding existing values.
*
* @static
* @param {Record<string, unknown>} target
* @param {types.ClientSettingsTypes} settings
* @returns {Record<string, unknown>}
*/
static mergeClientSettings(target, settings2) {
for (const key in settings2) {
if (!Object.prototype.hasOwnProperty.call(settings2, key)) continue;
const value = settings2[key];
if (value && typeof value === "object" && !Array.isArray(value)) {
if (!target[key] || typeof target[key] !== "object" || Array.isArray(target[key])) {
target[key] = {};
}
this.mergeClientSettings(target[key], value);
} else {
target[key] = value;
}
}
return target;
}
};
var utils_default = Utils;
// src/handler.ts
var Handler = class {
/**
* @function observationHandler
* @description
* Handles incoming observation data and emits an 'onObservation' event with formatted data.
*
* @public
* @static
* @param {*} data
*/
static observationHandler(data) {
cache.events.emit(`onObservation`, {
features: [{
geometry: { type: "Point", coordinates: [] },
type: "Feature",
properties: {
pressure_trend: data.summary.pressure_trend,
latest: {
epoch_latest_lightning: data.summary.strike_last_epoch,
latest_lightning_distance: data.summary.strike_last_dist,
precipitation_time: data.summary.precip_minutes_local_day_final
},
observation: {
time: data.obs[0][0],
wind_average: parseFloat((data.obs[0][2] * 2.23694).toFixed(2)),
wind_gust: parseFloat((data.obs[0][3] * 2.23694).toFixed(2)),
wind_direction: data.obs[0][4],
temperature: parseFloat((data.obs[0][7] * 9 / 5 + 32).toFixed(2)),
humidity: data.obs[0][8]
}
}
}]
});
}
/**
* @function forecastHandler
* @description
* Handles incoming forecast data and emits an 'onForecast' event with formatted data.
*
* @public
* @static
* @param {*} data
*/
static forecastHandler(data) {
if (data.error || data.message.status.status_code == 3) {
return utils_default.warn(definitions.messages.forecast_fetch_error, true);
}
cache.events.emit(`onForecast`, {
features: [{
geometry: { type: "Point", coordinates: [data.message.latitude, data.message.longitude] },
type: "Feature",
properties: {
feels_like: data.message.current_conditions.feels_like,
temperature: data.message.current_conditions.air_temperature,
densitity: data.message.current_conditions.air_density,
conditions: data.message.current_conditions.conditions,
dew_point: data.message.current_conditions.dew_point,
humidity: data.message.current_conditions.relative_humidity,
pressure_trend: data.message.current_conditions.pressure_trend,
wind_average: data.message.current_conditions.wind_avg,
wind_gust: data.message.current_conditions.wind_gust,
wind_direction: data.message.current_conditions.wind_direction,
station_name: data.message.location_name,
elevation: data.message.elevation
}
}]
});
}
/**
* @function rapidWindHandler
* @description
* Handles incoming rapid wind data and emits an 'onRapidWind' event with formatted data.
*
* @public
* @static
* @param {*} data
*/
static rapidWindHandler(data) {
cache.events.emit(`onRapidWind`, {
features: [{
geometry: { type: "Point", coordinates: [] },
type: "Feature",
properties: {
time: data.ob[0],
speed: data.ob[1],
direction: data.ob[2]
}
}]
});
}
/**
* @function lightningHandler
* @description
* Handles incoming lightning event data and emits an 'onLightning' event with formatted data.
*
* @public
* @static
* @param {*} data
*/
static lightningHandler(data) {
cache.events.emit(`onLightning`, {
features: [{
geometry: { type: "Point", coordinates: [] },
type: "Feature",
properties: {
time: data.evt[0],
distance: parseFloat((data.evt[1] / 0.621371).toFixed(2)),
energy: data.evt[2]
}
}]
});
}
};
var handler_default = Handler;
// src/index.ts
var TempestStation = class {
constructor(metadata) {
this.latitude = 0;
this.longitude = 0;
this.websocket = null;
this.start(metadata);
}
/**
* @function setSettings
* @description
* Merges the provided client settings into the current configuration,
* preserving nested structures.
*
* @async
* @param {types.ClientSettingsTypes} settings
* @returns {Promise<void>}
*/
setSettings(settings2) {
return __async(this, null, function* () {
if (settings2.deviceId === settings.deviceId || settings2.stationId === settings.stationId) return;
this.stop();
utils_default.mergeClientSettings(settings, settings2);
this.start(settings);
});
}
/**
* @function on
* @description
* Registers a callback for a specific event and returns a function
* to unregister the listener.
*
* @param {string} event
* @param {(...args: any[]) => void} callback
* @returns {() => void}
*/
on(event, callback) {
cache.events.on(event, callback);
return () => cache.events.off(event, callback);
}
/**
* @function start
* @description
* Initializes the client with the provided settings
*
* @async
* @param {types.ClientSettingsTypes} metadata
* @returns {Promise<void>}
*/
start(metadata) {
return __async(this, null, function* () {
utils_default.mergeClientSettings(settings, metadata);
const settings2 = settings;
if (!(settings2 == null ? void 0 : settings2.api) || !(settings2 == null ? void 0 : settings2.deviceId)) return;
const wsUrl = `wss://ws.weatherflow.com/swd/data?api_key=${settings2.api}&location_id=${settings2.deviceId}&ver=tempest-20250728`;
this.websocket = new packages.ws(wsUrl);
this.websocket.on("open", () => __async(this, null, function* () {
utils_default.warn(definitions.messages.websocket_established, true);
cache.events.emit(`onConnection`);
if (settings2.stationId) {
const stationsUrl = `https://swd.weatherflow.com/swd/rest/stations/${settings2.stationId}?api_key=${settings2.api}`;
const responseStations = yield utils_default.createHttpRequest(stationsUrl);
if (!responseStations.error) {
const station = responseStations.message;
if (station && typeof station === "object" && Array.isArray(station.stations) && station.stations[0]) {
const s = station.stations[0];
this.latitude = Number(s.latitude);
this.longitude = Number(s.longitude);
}
}
if (this.websocket) {
if (Number.isFinite(this.latitude) && Number.isFinite(this.longitude)) {
this.websocket.send(JSON.stringify({
type: "geo_strike_listen_start",
lat_min: this.latitude - 5,
lat_max: this.latitude + 5,
lon_min: this.longitude - 5,
lon_max: this.longitude + 5
}));
}
this.websocket.send(JSON.stringify({
type: "listen_start",
device_id: settings2.deviceId
}));
this.websocket.send(JSON.stringify({
type: "listen_rapid_start",
device_id: settings2.deviceId
}));
}
}
}));
handler_default.forecastHandler(yield this.getForecast());
this.websocket.on("message", (response) => __async(this, null, function* () {
let data;
try {
data = JSON.parse(response);
} catch (e) {
return;
}
const type = (data == null ? void 0 : data.type) || null;
if (type == `ack`) cache.events.emit(`onAcknowledge`, data);
if (type == `obs_st`) {
handler_default.observationHandler(data);
handler_default.forecastHandler(yield this.getForecast());
}
if (type == `rapid_wind`) handler_default.rapidWindHandler(data);
if (type == `evt_strike`) handler_default.lightningHandler(data);
}));
this.websocket.on("error", (err) => {
utils_default.warn(definitions.messages.api_failed, true);
});
});
}
/**
* @function getForecast
* @description
* Fetches the weather forecast data from the TempestStation API.
*
* @async
* @returns {Promise<{ error: boolean; message: any | string }>}
*/
getForecast() {
return __async(this, null, function* () {
const settings2 = settings;
const forecastUrl = `https://swd.weatherflow.com/swd/rest/better_forecast?api_key=${settings2.api}&station_id=${settings2.stationId}&units_temp=f&units_wind=mph&units_pressure=inhg&units_distance=mi&units_precip=in&units_other=imperial&units_direction=mph`;
return yield utils_default.createHttpRequest(forecastUrl);
});
}
/**
* @function getClosestStation
* @description
* Fetches the closest weather station based on provided coordinates.
*
* @public
* @async
* @param {types.Coordinates} coordinates
* @returns {unknown}
*/
getClosestStation(coordinates) {
return __async(this, null, function* () {
var _a;
if (!coordinates || typeof coordinates.lat !== "number" || typeof coordinates.lon !== "number") return null;
const latMin = coordinates.lat - 5, latMax = coordinates.lat + 5;
const lonMin = coordinates.lon - 5, lonMax = coordinates.lon + 5;
const settings2 = settings;
if (!(settings2 == null ? void 0 : settings2.api)) return null;
const stationsUrl = `https://swd.weatherflow.com/swd/rest/map/stations?api_key=${settings2.api}&build=160&limit=500&lat_min=${latMin}&lon_min=${lonMin}&lat_max=${latMax}&lon_max=${lonMax}&_=${Date.now()}`;
const responseStations = yield utils_default.createHttpRequest(stationsUrl);
if (responseStations.error) return null;
const data = responseStations.message;
const features = Array.isArray(data == null ? void 0 : data.features) ? data.features : [];
if (!features.length) return null;
const refLat = coordinates.lat;
const refLon = coordinates.lon;
const toRad = (deg) => deg * Math.PI / 180;
const earthRadiusKm = 6371;
const haversine = (lat1, lon1, lat2, lon2) => {
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return earthRadiusKm * c;
};
let minDistance = Infinity;
let bestStation = null;
for (const feature of features) {
const coords = (_a = feature == null ? void 0 : feature.geometry) == null ? void 0 : _a.coordinates;
if (!Array.isArray(coords) || coords.length < 2) continue;
const [lon, lat] = coords.map(Number);
if (Number.isNaN(lat) || Number.isNaN(lon)) continue;
const d = haversine(refLat, refLon, lat, lon);
if (d < minDistance) {
minDistance = d;
bestStation = feature;
}
}
if (!bestStation || !isFinite(minDistance)) return null;
return bestStation;
});
}
/**
* @function stop
* @description
* Stops active connections and cleans up resources.
*
* @async
* @returns {Promise<void>}
*/
stop() {
return __async(this, null, function* () {
if (this.websocket) {
this.websocket.close();
this.websocket = null;
}
utils_default.warn(definitions.messages.client_stopped, true);
});
}
};
var index_default = TempestStation;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
TempestStation
});