homebridge-weather-noaa
Version:
Homebridge plugin providing temperature and humidity sensors using NOAA API.
206 lines (205 loc) • 9.71 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NOAAWeatherPlatform = void 0;
const axios_1 = __importDefault(require("axios"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const weatherAccessory_1 = require("./weatherAccessory");
class NOAAWeatherPlatform {
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.accessories = [];
this.log.info(this.formatLog('NOAAWeatherPlatform initialized'));
this.axiosInstance = axios_1.default.create({
headers: {
'User-Agent': 'homebridge-weather-noaa (https://github.com/Phirtue/homebridge-weather-noaa/)',
'Accept': 'application/geo+json',
'Referer': 'https://github.com/Phirtue/homebridge-weather-noaa'
},
timeout: 10000
});
this.axiosInstance.interceptors.response.use(response => response, error => {
NOAAWeatherPlatform.metrics.apiFailures++;
this.log.error(this.formatLog(`NOAA API request failed: ${error.message}`));
return Promise.reject(error);
});
api.on('didFinishLaunching', () => {
this.safeDiscoverDevices().catch(err => {
this.log.error(this.formatLog('Unhandled error in discoverDevices:'), err);
});
});
setInterval(() => this.logMetrics(), 60 * 60 * 1000);
process.on('exit', () => this.logMetrics());
}
configureAccessory(accessory) {
this.log.info(this.formatLog('Re-using cached accessory:'), accessory.displayName);
this.accessories.push(accessory);
}
async fetchWithRetry(url, maxRetries = 4) {
let attempt = 0;
let delay = 1000;
while (attempt < maxRetries) {
try {
const response = await this.axiosInstance.get(url);
return response;
}
catch (error) {
const status = error.response?.status || 'NO_RESPONSE';
if ([500, 502, 503, 504].includes(status) || status === 'NO_RESPONSE') {
NOAAWeatherPlatform.metrics.retryCount++;
this.log.warn(this.formatLog(`Request failed (status: ${status}). Retrying in ${delay / 1000}s...`));
await new Promise(res => setTimeout(res, delay));
delay *= 2;
attempt++;
}
else {
throw error;
}
}
}
throw new Error(`Failed after ${maxRetries} attempts for URL: ${url}`);
}
async safeDiscoverDevices() {
try {
await this.discoverDevices();
}
catch (err) {
this.log.error(this.formatLog('Critical error during device discovery:'), err);
}
}
async discoverDevices() {
const latitude = this.config.latitude;
const longitude = this.config.longitude;
const refresh = (this.config.refreshInterval || 5) * 60 * 1000;
const cacheFile = path_1.default.join(this.api.user.persistPath(), 'noaa-points-cache.json');
if (!latitude || !longitude) {
this.log.error(this.formatLog('Latitude and Longitude must be configured.'));
return;
}
let stationId = this.config.stationId || null;
if (!stationId && fs_1.default.existsSync(cacheFile)) {
try {
const cache = JSON.parse(fs_1.default.readFileSync(cacheFile, 'utf8'));
if (cache.latitude === latitude &&
cache.longitude === longitude &&
(Date.now() - cache.timestamp) < 30 * 24 * 60 * 60 * 1000) {
this.log.info(this.formatLog(`📦 Using cached NOAA station: ${cache.stationId}`));
stationId = cache.stationId;
}
}
catch (e) {
NOAAWeatherPlatform.metrics.stationCacheResets++;
this.log.warn(this.formatLog('Corrupted NOAA station cache detected. Rebuilding cache.'));
try {
fs_1.default.unlinkSync(cacheFile);
}
catch { }
}
}
if (!stationId && !this.config.stationId) {
try {
this.log.info(this.formatLog(`🔎 Fetching NOAA stations for coordinates: ${latitude},${longitude}`));
const stations = await this.fetchWithRetry(`https://api.weather.gov/points/${latitude},${longitude}/stations`);
const stationList = stations.data.features
.filter((f) => /^[A-Z0-9]{3,4}$/.test(f.properties.stationIdentifier))
.map((f) => ({
id: f.properties.stationIdentifier,
distance: f.properties.distance?.value ?? Number.MAX_SAFE_INTEGER
}));
if (stationList.length === 0) {
this.log.error(this.formatLog('No valid NOAA stations found.'));
return;
}
stationList.sort((a, b) => a.distance - b.distance);
this.log.info(this.formatLog('📡 NOAA stations sorted by distance:', stationList.map((s) => `${s.id} (${s.distance}m)`).join(', ')));
stationId = stationList[0].id;
this.log.info(this.formatLog(`✅ Selected closest NOAA station: ${stationId}`));
fs_1.default.writeFileSync(cacheFile, JSON.stringify({
latitude,
longitude,
stationId,
timestamp: Date.now()
}, null, 2));
}
catch (error) {
this.log.error(this.formatLog('Failed to fetch NOAA stations'), error);
return;
}
}
else if (this.config.stationId) {
this.log.info(this.formatLog(`📍 Using manually configured NOAA station: ${this.config.stationId}`));
stationId = this.config.stationId;
}
const uuid = this.api.hap.uuid.generate('noaa-weather-unique');
let accessory = this.accessories.find(acc => acc.UUID === uuid);
if (accessory) {
this.log.info(this.formatLog('Reusing existing accessory for NOAA Weather.'));
}
else {
accessory = new this.api.platformAccessory('NOAA Weather', uuid);
this.api.registerPlatformAccessories('homebridge-weather-noaa', 'NOAAWeather', [accessory]);
this.log.info(this.formatLog('Created new accessory for NOAA Weather.'));
}
const weatherAccessory = new weatherAccessory_1.NOAAWeatherAccessory(this, accessory);
const fetchWeather = async () => {
try {
this.log.info(this.formatLog('🔄 Starting NOAA weather update...'));
const data = await this.fetchWithRetry(`https://api.weather.gov/stations/${stationId}/observations/latest`);
const properties = data.data.properties;
const timestamp = properties.timestamp;
const temperature = properties.temperature?.value;
const humidity = properties.relativeHumidity?.value;
const tempQC = properties.temperature?.qualityControl;
const humidityQC = properties.relativeHumidity?.qualityControl;
const elevation = properties.elevation?.value;
const weatherConditions = properties.presentWeather?.map((w) => w.weather).join(', ') || 'None';
this.log.info(this.formatLog(`🌡️ NOAA Data — Timestamp: ${timestamp}, Temp: ${temperature}°C (QC: ${tempQC}), Humidity: ${humidity}% (QC: ${humidityQC}), Elevation: ${elevation}m, Conditions: ${weatherConditions}`));
accessory.context.weather = { temperature, humidity };
weatherAccessory.updateValues();
}
catch (e) {
NOAAWeatherPlatform.metrics.apiFailures++;
this.log.error(this.formatLog('❌ Failed to fetch NOAA data'), e);
}
};
try {
await fetchWeather();
}
catch (e) {
this.log.error(this.formatLog('Initial NOAA fetch failed:'), e);
}
setInterval(() => {
fetchWeather().catch(err => {
NOAAWeatherPlatform.metrics.apiFailures++;
this.log.error(this.formatLog('Recurring NOAA fetch failed:'), err);
});
}, refresh);
}
logMetrics() {
this.log.info(this.formatLog(`📊 NOAA Platform Metrics → API Failures: ${NOAAWeatherPlatform.metrics.apiFailures}, ` +
`Retry Count: ${NOAAWeatherPlatform.metrics.retryCount}, ` +
`Station Cache Resets: ${NOAAWeatherPlatform.metrics.stationCacheResets}`));
}
formatLog(message, data) {
const now = new Date();
const formattedTime = new Intl.DateTimeFormat('en-US', {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
hour12: true,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).format(now);
return `[${formattedTime}] ${message}${data ? ' ' + data : ''}`;
}
}
exports.NOAAWeatherPlatform = NOAAWeatherPlatform;
NOAAWeatherPlatform.metrics = {
apiFailures: 0,
retryCount: 0,
stationCacheResets: 0,
};