diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9d658..f4ef704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -278,3 +278,17 @@ ## 3.3.3 * Fix Tempest weather station breakage introduced in 3.3.2 * Fix Rain accumulation + +## 3.3.4 +* Add support to detect failing sensors on Tempest weather station and report failure HomeKit +* Fix tile naming for Home app so it contains name and value +* Add support for Sunrise and Sunset for current conditions and forecast with OpenWeather v2.5 (Legacy/Free) API +* Add support for Sunrise and Sunset for current conditions with OpenWeather v3.0 OneCall API +* Fix configuration UI to save the threshold values +* Add support for Forecasts with Tempest weather station. +* Add extra error logging when OpenWeatherMap URL load fails +* Add support for light level history +* Add error handling to WeatherUnderground to indicate if PWS is offline +* Increase max UV Index value as UV Index values in Australia's outback goes higher than 12 +* Add Rain sensor support to WeatherUnderground +* Fix units in WeatherUnderground so that values are not converted multiple times diff --git a/README.md b/README.md index 8098ee7..418000c 100644 --- a/README.md +++ b/README.md @@ -32,19 +32,18 @@ This plugin supports multiple weather services. Each has its own advantages. The | | OpenWeatherMap (recommended) | Weather Underground [2](#a2) | Tempest weather station [7](#a7) | |----------------------------|:-----------------------------------------------------:|:----------------------------------------------------:|:----------------------------------------------------:| | Current observation values | 15 | 12 | 20 | -| Forecast values | 18[6](#a6) | 0 | 0 | -| Forecast days | today + 7[6](#a6) | 0 | 0[8](#a8) | +| Forecast values | 18[6](#a6) | 0 | 10 | +| Forecast days | today + 7[6](#a6) | 0 | today + 9 | | Location | city name, geo-coordinates | station id | local | | Personal weather stations | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Free | :heavy_check_mark: | :heavy_check_mark: (only if you own a station) | :heavy_check_mark: (you need the station) | -| Register | [here](https://home.openweathermap.org/users/sign_up) | [here](https://www.wunderground.com/member/api-keys) | - | +| Register | [here](https://home.openweathermap.org/users/sign_up) | [here](https://www.wunderground.com/member/api-keys) | [here](https://tempestwx.com/settings/tokens) | *You can add more services easily by forking the project and submitting a pull request for a new api file.* > 2 You can use the weather underground service only if you can provide weather data from your own station in exchange. > 6 uv-index, dew point, sunrise, sunset are only available after registering for the new OpenWeatherMap One Call API 3.0, which is free as well. The old API also has 4 instead of 7 forecast days. -> 7 [Weatherflow's Tempest](https://weatherflow.com) is a physical weather station that can be installed in your home. Current weather conditions are published on home network. -> 8 Weatherflow does provide an API to retrieve forecast. That capability has not been added to this plugin. +> 7 [Weatherflow's Tempest](https://weatherflow.com) is a physical weather station that can be installed in your home. Current weather conditions are published on home network. ## Installation @@ -171,13 +170,21 @@ Used to indicate Weewx version. The homekit plugin requires an location field t ### Tempest Weatherflow -The [Tempest Weatherflow](https://weatherflow.com/tempest-home-weather-system/) is a local weather reporting device that publishes the current weather on the local network via [UDP packets](https://weatherflow.github.io/Tempest/api/). Data is broadcast once per minute, so the Interval setting is ignored. The physical station can only provide the current weather. Future forecasts are available with this weather source, though has not been implemented yet. This uses data published on your local network for the current weather, and therefore runs fine without an internet connection. +The [Tempest Weatherflow](https://weatherflow.com/tempest-home-weather-system/) is a local weather reporting device that publishes the current weather on the local network via [UDP packets](https://weatherflow.github.io/Tempest/api/). Data is broadcast once per minute, so the Interval setting is ignored. The physical station can only provide the current weather. This uses data published on your local network for the current weather, and therefore runs fine without an internet connection. Future forecasts are available from Tempest for your weather station using a Personal Use Token and your station id. + +**key** +(Optional - only needed for Forecasts) The API key that you get by logging into your [Account](https://tempestwx.com/settings/tokens) and creating a personal use token. + +**stationId** +(Optional - only needed for Forecasts) Your personal StationID. Viewing your [station settings](https://tempestwx.com/settings/station/), it is the number on the end of the URL. ```json "platforms": [ { "platform": "WeatherPlus", - "service": "tempest" + "service": "tempest", + "key": "PERSONAL_USE_TOKEN", + "stationId": "STATION_ID" } ] ``` @@ -338,3 +345,4 @@ This plugin is a fork of [homebridge-weather-station](https://github.com/kcharwo ## Attribution - [Powered by Weather Underground](https://www.wunderground.com/) - [Powered by OpenWeatherMap](https://openweathermap.org/) +- [Powered by WeatherFlow - Tempest](https://tempest.earth/tempest-home-weather-system/) diff --git a/accessories/currentConditions.js b/accessories/currentConditions.js index 7e624ba..620ab81 100644 --- a/accessories/currentConditions.js +++ b/accessories/currentConditions.js @@ -26,6 +26,7 @@ function CurrentConditionsWeatherAccessory(platform, stationIndex) this.name = this.config.nameNow; this.displayName = this.config.nameNow; this.stationIndex = stationIndex; + this.services = []; // Use homekit temperature service or eve weather service depending on compatibility setting this.log.debug("Using compatibility mode '%s'", this.config.compatibility); @@ -60,6 +61,7 @@ function CurrentConditionsWeatherAccessory(platform, stationIndex) }); } } + this.services.push(this.CurrentConditionsService); // Add all current condition characteristics that are supported by the selected api @@ -128,6 +130,10 @@ function CurrentConditionsWeatherAccessory(platform, stationIndex) { this.CurrentConditionsService.addCharacteristic(Characteristic.ChargingState); } + else if (name === "StatusFault") + { + this.CurrentConditionsService.addCharacteristic(Characteristic.StatusFault); + } // Add everything else as a custom characteristic to the temperature service else { @@ -135,6 +141,7 @@ function CurrentConditionsWeatherAccessory(platform, stationIndex) } } }); + this.services.concat(compatibility.getServices(this)); // Create information service this.informationService = new Service.AccessoryInformation(); @@ -143,9 +150,11 @@ function CurrentConditionsWeatherAccessory(platform, stationIndex) .setCharacteristic(Characteristic.Model, this.platform.stations[stationIndex].attribution) .setCharacteristic(Characteristic.SerialNumber, this.config.serial) .setCharacteristic(Characteristic.FirmwareRevision, version); + this.services.push(this.informationService); // Create history service - this.historyService = new FakeGatoHistoryService("weather", this, this.config.fakegatoParameters); + this.historyService = new FakeGatoHistoryService("custom", this, this.config.fakegatoParameters); + this.services.push(this.historyService); } CurrentConditionsWeatherAccessory.prototype = { @@ -170,6 +179,6 @@ CurrentConditionsWeatherAccessory.prototype = { getServices: function () { - return [this.informationService, this.CurrentConditionsService, this.historyService].concat(compatibility.getServices(this)); + return this.services; } }; diff --git a/apis/openweathermap.js b/apis/openweathermap.js index 9341d04..f2e8006 100644 --- a/apis/openweathermap.js +++ b/apis/openweathermap.js @@ -36,6 +36,8 @@ class OpenWeatherMapAPI 'Rain1h', 'RainBool', 'SnowBool', + 'SunriseTime', + 'SunsetTime', 'Temperature', 'TemperatureApparent', 'UVIndex', @@ -106,10 +108,12 @@ class OpenWeatherMapAPI this.getWeatherData(this.apiBaseURL + "/data/2.5/forecast", (error, result) => { if (!error) { - this.generateForecasts(weather, result["list"], result["city"]["timezone"], callback); + // Pass the entire "city" JSON array as it has both the timezone and sunrise & sunset values + this.generateForecasts(weather, result["list"], result["city"], callback); } else { - that.log.error("Error retrieving weather Forecast from API 2.5"); - that.log.error(result); + that.log.error("Error retrieving OpenWeatherMap Forecast from API 2.5"); + that.log.error("Error result: " + result); + that.log.error("Error message: " + error); callback(); } }); @@ -131,22 +135,22 @@ class OpenWeatherMapAPI this.removeCharacteristic(this.reportCharacteristics, "DewPoint"); this.removeCharacteristic(this.forecastCharacteristics, "UVIndex"); this.removeCharacteristic(this.forecastCharacteristics, "DewPoint"); - this.removeCharacteristic(this.forecastCharacteristics, "SunriseTime"); - this.removeCharacteristic(this.forecastCharacteristics, "SunsetTime"); this.forecastDays = 5; this.update(forecastDays, callback); } else { that.log.error("Could not retreive weather report with neither API 3.0 or API 2.5. You may need to wait up to 30 minutes after creating your api key. If the error persist, check if you copied the api key correctly."); - that.log.error(result); + that.log.error("Error result: " + result); + that.log.error("Error message: " + error); callback(); } } else { - that.log.error("Error retrieving weather report"); - that.log.error(result); + that.log.error("Error retrieving OpenWeatherMap report"); + that.log.error("Error result: " + result); + that.log.error("Error message: " + error); callback(); } } @@ -165,7 +169,7 @@ class OpenWeatherMapAPI } else { - this.parseReportOneCall(weather.report, values["current"]); + this.parseReportOneCall(weather.report, values["current"], timezone); weather.report.ObservationTime = moment.unix(values["current"].dt).tz(timezone).format('HH:mm:ss'); } @@ -181,6 +185,7 @@ class OpenWeatherMapAPI // API 2.5 does not send a summary for the forecast day, instead it sends a report for every 3 hours. // We need to combine 8 x 3hrs reports to get the forecast for 1 day. + // Also for API 2.5, timezone parameter is actually the result "city" JSON array let legacyDays = []; if (this.api === "2.5") { @@ -199,7 +204,7 @@ class OpenWeatherMapAPI { if (this.api === "2.5") { - forecasts[forecasts.length] = this.parseForecastLegacy(values[i], timezone / 60); + forecasts[forecasts.length] = this.parseForecastLegacy(values[i], timezone); } else { @@ -224,6 +229,8 @@ class OpenWeatherMapAPI let detailedCondition = this.getConditionCategory(values.weather[0].id, true); report.RainBool = [5, 6, 9].includes(detailedCondition); report.SnowBool = [7, 8].includes(detailedCondition); + report.SunriseTime = moment.unix(values.sys.sunrise).utcOffset(values.timezone / 60).format('HH:mm:ss'); + report.SunsetTime = moment.unix(values.sys.sunset).utcOffset(values.timezone / 60).format('HH:mm:ss'); report.TemperatureApparent = typeof values.main.feels_like === 'object' ? parseInt(values.main.feels_like.day) : parseInt(values.main.feels_like); report.TemperatureMax = parseInt(values.main.temp_max); report.TemperatureMin = parseInt(values.main.temp_min); @@ -249,7 +256,7 @@ class OpenWeatherMapAPI * @param timezoneShift shift in seconds from utc of location timezone * @returns {{}} forecast day */ - parseForecastLegacy(values, timezoneShift) + parseForecastLegacy(values, city) { let forecast = {}; let combinedHourlyValues = { @@ -265,6 +272,10 @@ class OpenWeatherMapAPI "all": values.map(v => v.clouds.all).reduce((acc, v, i, a) => (acc + v / a.length), 0) }, "weather": values[4].weather, + "sys": { + "sunrise": city["sunrise"], + "sunset": city["sunset"] + }, "wind": { "speed": Math.max(...values.map(v => v.wind.speed)), "deg": values[4].wind.deg, @@ -273,16 +284,17 @@ class OpenWeatherMapAPI "rain": { "24h": values.map(v => v.rain === undefined || isNaN(parseFloat(v.rain['3h'])) ? 0 : parseFloat(v.rain['3h'])).reduce((a, b) => a + b) }, - "pop": Math.max(...values.map(v => v.pop)) + "pop": Math.max(...values.map(v => v.pop)), + "timezone" : city["timezone"] } this.parseReportLegacy(forecast, combinedHourlyValues, true); - forecast.ForecastDay = moment.unix(combinedHourlyValues.dt).utcOffset(timezoneShift).format('dddd'); + forecast.ForecastDay = moment.unix(combinedHourlyValues.dt).utcOffset(city["timezone"] / 60).format('dddd'); return forecast; } - parseReportOneCall(report, values, isForecast = false) + parseReportOneCall(report, values, timezone, isForecast = false) { report.AirPressure = parseInt(values.pressure); report.CloudCover = parseInt(values.clouds); @@ -293,6 +305,8 @@ class OpenWeatherMapAPI let detailedCondition = this.getConditionCategory(values.weather[0].id, true); report.RainBool = [5, 6, 9].includes(detailedCondition); report.SnowBool = [7, 8].includes(detailedCondition); + report.SunriseTime = moment.unix(values.sunrise).tz(timezone).format('HH:mm:ss'); + report.SunsetTime = moment.unix(values.sunset).tz(timezone).format('HH:mm:ss'); report.TemperatureApparent = typeof values.feels_like === 'object' ? parseInt(values.feels_like.day) : parseInt(values.feels_like); report.UVIndex = parseInt(values.uvi); report.WindDirection = converter.getWindDirection(values.wind_deg); @@ -318,11 +332,9 @@ class OpenWeatherMapAPI parseForecastOneCall(values, timezone) { let forecast = {}; - this.parseReportOneCall(forecast, values, true); + this.parseReportOneCall(forecast, values, timezone, true); forecast.ForecastDay = moment.unix(values.dt).tz(timezone).format('dddd'); - forecast.SunriseTime = moment.unix(values.sunrise).tz(timezone).format('HH:mm:ss'); - forecast.SunsetTime = moment.unix(values.sunset).tz(timezone).format('HH:mm:ss'); return forecast; } diff --git a/apis/weatherflow.js b/apis/weatherflow.js index 219e2a1..2a2d7ca 100644 --- a/apis/weatherflow.js +++ b/apis/weatherflow.js @@ -11,11 +11,12 @@ const converter = require('../util/converter'), moment = require('moment-timezone'), dgram = require("dgram"), - wformula = require('weather-formulas'); + wformula = require('weather-formulas'), + request = require('request'); class TempestAPI { - constructor (conditionDetail, log, cacheDirectory) + constructor (apiKey, locationId, conditionDetail, log, cacheDirectory) { this.attribution = 'Weatherflow Tempest'; this.reportCharacteristics = [ @@ -40,6 +41,7 @@ class TempestAPI 'LightLevel', // Illuminance 'BatteryLevel', // Device Battery level percent 'BatteryIsCharging', // Device Battery charging state + 'StatusFault', // Report if there is a fault // Derived values // @see https://weatherflow.github.io/Tempest/api/derived-metric-formulas.html @@ -47,9 +49,32 @@ class TempestAPI 'TemperatureApparent', // Calculated from Humidity, Temperature & Windspeed 'TemperatureWetBulb' // Calculated from Humidity & Temperature ]; + + this.forecastCharacteristics = [ + 'ObservationTime', // Forecast update time + 'Condition', //Conditions + 'ConditionCategory', + 'ForecastDay', // day_num, month_num, day_start_local + 'RainBool', // precip_icon contains === 'rain' || 'sleet' || 'storm' + 'SnowBool', // precip_icon contains === 'snow' + 'SunriseTime', //sunrise + 'SunsetTime', // sunset + 'TemperatureMax', // air_temp_high + 'TemperatureMin', // air_temp_low + 'RainChance' // precip_probability + ]; + this.forecastDays = 10; + + // Only define the update variable if we have an apiKey and locationId + if (apiKey && apiKey.length > 0 && locationId && locationId.length > 0) { + this.lastForecastUpdate = -1; + } this.conditionDetail = conditionDetail; this.log = log; + this.apiKey = apiKey; + this.locationId = locationId; + this.storage = require('node-persist'); // The saved data is only valid for up to 24hrs (TTL) this.storage.initSync({dir:cacheDirectory, forgiveParseErrors: true, ttl: true}); @@ -89,20 +114,27 @@ class TempestAPI this.currentReport.LightningAvgDistance = 0; this.currentReport.LightLevel = 0; this.currentReport.TemperatureWetBulb = 0; + this.currentReport.StatusFault = 0; // Non-exposed Weather report characteristics + // Sky or Tempest station (unlikely to have both) this.currentReport.SkySensorBatteryLevel = 100; this.currentReport.SkySerialNumber = "SK-"; this.currentReport.SkyFirmware = "1.0"; + this.currentReport.SkySensorFailureLog = -1; + // Air station this.currentReport.AirSensorBatteryLevel = 100; this.currentReport.AirSerialNumber = "AR-"; this.currentReport.AirFirmware = "1.0"; - this.currentReport.LightLevelSensorFail = 0; - this.currentReport.HumiditySensorFail = 0; - this.currentReport.TemperatureSensorFail = 0; + this.currentReport.AirSensorFailureLog = -1; + this.currentReport.SensorString = "Ok"; // Attempt to restore previous values this.load(); + + // Keep track of previous message so that + // we can remove duplicates + this.prevMsg = ""; // Create UDP listener and start listening this.server = dgram.createSocket({type: 'udp4', reuseAddr: true}); @@ -115,7 +147,12 @@ class TempestAPI try { var message = JSON.parse(msg); this.log.debug(`Server got: ${message.type}`); - this.parseMessage(message); + if (msg.toString() === this.prevMsg) { + this.log.debug(`Duplicate msg ${msg}`); + } else { + this.parseMessage(message); + } + this.prevMsg = msg.toString(); } catch(ex) { this.log(`JSON Parse Exception: ${msg} ${ex}`); @@ -175,13 +212,40 @@ class TempestAPI let weather = {}; weather.forecasts = []; - let that = this; - weather.report = that.currentReport; - callback(null, weather); - // Save the state after updating plugin state so we don't - // delay the update - this.save(that.currentReport); + // Limit forecast updates to once every hour. Forecast won't change that quickly + if ((typeof this.lastForecastUpdate !== 'undefined') && moment().hour() != this.lastForecastUpdate) { + this.lastForecastUpdate = moment().hour(); + this.getForecastData((error, result) => + { + if (!error) { + try { + weather.forecasts = this.parseForecasts(result["current_conditions"]["time"], result["forecast"]["daily"], result["timezone"]); + } catch (e) { + this.log.error("Error parsing weather Forecast"); + this.log.error(result); + this.log.error(e); + } + } else { + this.log.error("Error retrieving weather Forecast"); + this.log.error(result); + } + + let that = this; + weather.report = that.currentReport; + callback(null, weather); + + // Save the state after updating plugin state so we don't + // delay the update + this.save(that.currentReport); + }); + } + else { + // Don't update the forecast, just update current conditions + let that = this; + weather.report = that.currentReport; + callback(null, weather); + } } // Map Tempest precipitation values to Eve Condition Categories @@ -230,10 +294,11 @@ class TempestAPI return accumulation; } - // Assume that zero battery level is 2.6v + // For AIR/SKY sensor units, assume to be the same as + // the Tempest documentation. getBatteryPercent(batteryVoltage) { - return (batteryVoltage * 100 - 260); + return this.getTempestBatteryPercent(batteryVoltage); } // Tempest battery ranges from 2.355 (low) to 2.8 (full) @@ -254,23 +319,83 @@ class TempestAPI if (message.type == 'device_status') { that.currentReport.ObservationStation = message.serial_number; that.currentReport.ObservationTime = moment.unix(message.timestamp).format('HH:mm:ss'); - that.currentReport.TemperatureSensorFail = (message.sensor_status & 0x00000010) ? 1 : 0; - that.currentReport.HumiditySensorFail = (message.sensor_status & 0x00000020) ? 1 : 0; - that.currentReport.LightLevelSensorFail = (message.sensor_status & 0x00000100) ? 1 : 0; - // TODO: Check if a sensor has failed, and log it! - this.log.debug("Temperature Sensor Fail: %d, Humidity Sensor Fail: %d, Light Level Sensor Fail: %d", - that.currentReport.TemperatureSensorFail, - that.currentReport.HumiditySensorFail, - that.currentReport.LightLevelSensorFail); - var previousLevel = that.currentReport.BatteryLevel; - that.currentReport.BatteryIsCharging = false; - if (message.serial_number.charAt(1) == 'T') { // Tempest - that.currentReport.BatteryLevel = this.getTempestBatteryPercent(message.voltage); + + // Handle sensor failures + // Per API v171, only intepret values defined, ignore all others + message.sensor_status = message.sensor_status & 0x1FFFF; + + // Any value other than zero for sensor_status means we have a failure + that.currentReport.StatusFault = message.sensor_status == 0 ? false : true; + if (message.sensor_status == 0) { + this.currentReport.SensorString = "Ok"; + + // Reset logging interval for only the unit that is ok + // Unit prefixes: AR Air, SK Sky, ST Tempest + if (message.serial_number.charAt(1) == 'R') { + this.currentReport.AirSensorFailureLog = -1; + } else { + this.currentReport.SkySensorFailureLog = -1; + } } else { - that.currentReport.BatteryLevel = this.getBatteryPercent(message.voltage); - } - if (that.currentReport.BatteryLevel > previousLevel) { - that.currentReport.BatteryIsCharging = true; + this.currentReport.SensorString = ""; + if (message.sensor_status & 0x00000001) { + this.currentReport.SensorString += "Lightning failed" + } + if (message.sensor_status & 0x00000002) { + if (this.currentReport.SensorString.length > 0) this.currentReport.SensorString += ", "; + this.currentReport.SensorString += "Lightning noise" + } + if (message.sensor_status & 0x00000004) { + if (this.currentReport.SensorString.length > 0) this.currentReport.SensorString += ", "; + this.currentReport.SensorString += "Lightning disturber" + } + if (message.sensor_status & 0x00000008) { + if (this.currentReport.SensorString.length > 0) this.currentReport.SensorString += ", "; + this.currentReport.SensorString += "Pressure failed" + } + if (message.sensor_status & 0x00000010) { + if (this.currentReport.SensorString.length > 0) this.currentReport.SensorString += ", "; + this.currentReport.SensorString += "Temperature failed" + } + if (message.sensor_status & 0x00000020) { + if (this.currentReport.SensorString.length > 0) this.currentReport.SensorString += ", "; + this.currentReport.SensorString += "Relative Humidity failed" + } + if (message.sensor_status & 0x00000040) { + if (this.currentReport.SensorString.length > 0) this.currentReport.SensorString += ", "; + this.currentReport.SensorString += "Wind failed" + } + if (message.sensor_status & 0x00000080) { + if (this.currentReport.SensorString.length > 0) this.currentReport.SensorString += ", "; + this.currentReport.SensorString += "Precipitation failed" + } + if (message.sensor_status & 0x00000100) { + if (this.currentReport.SensorString.length > 0) this.currentReport.SensorString += ", "; + this.currentReport.SensorString += "Light/UV failed" + } + if (message.sensor_status & 0x00008000) { + if (this.currentReport.SensorString.length > 0) this.currentReport.SensorString += ", "; + this.currentReport.SensorString += "Power booster depleted" + } + if (message.sensor_status & 0x00010000) { + if (this.currentReport.SensorString.length > 0) this.currentReport.SensorString += ", "; + this.currentReport.SensorString += "Power booster shore power" + } + this.log.debug("Sensor on unit %s failed error code: %d", message.serial_number, message.sensor_status); + + // Track error logging per failed device + // Unit prefixes: AR Air, SK Sky, ST Tempest + if (message.serial_number.charAt(1) == 'R') { + if (this.currentReport.AirSensorFailureLog != moment.unix(message.timestamp).hour()) { + this.log.error("Sensor on unit %s failed: ", message.serial_number, this.currentReport.SensorString); + } + this.currentReport.AirSensorFailureLog = moment.unix(message.timestamp).hour(); + } else { + if (this.currentReport.SkySensorFailureLog != moment.unix(message.timestamp).hour()) { + this.log.error("Sensor on unit %s failed: ", message.serial_number, this.currentReport.SensorString); + } + this.currentReport.SkySensorFailureLog = moment.unix(message.timestamp).hour(); + } } } @@ -294,31 +419,40 @@ class TempestAPI that.currentReport.AirFirmware = message.firmware_revision; that.currentReport.ObservationTime = moment.unix(message.obs[0][0]).format('HH:mm:ss'); that.currentReport.AirPressure = message.obs[0][1]; - if (that.currentReport.TemperatureSensorFail == 1) - that.currentReport.Temperature = 0; - else - that.currentReport.Temperature = message.obs[0][2]; - if (that.currentReport.HumiditySensorFail == 1) { - that.currentReport.Humidity = 0; - that.currentReport.DewPoint = 0; - } - else { - that.currentReport.Humidity = message.obs[0][3]; + that.currentReport.Temperature = message.obs[0][2]; + that.currentReport.Humidity = message.obs[0][3]; + + // Only perform new calculations if temperature and humidity values are within a good range + // We could get out of range values if the sensors have failed. + if (that.currentReport.Humidity > 0 && that.currentReport.Humidity <= 100 && + that.currentReport.Temperature > -100 && that.currentReport.Temperature < 100) { that.currentReport.DewPoint = wformula.kelvinToCelcius(wformula.dewPointMagnusFormula( wformula.celciusToKelvin(that.currentReport.Temperature), that.currentReport.Humidity)); - } - that.currentReport.TemperatureApparent = wformula.kelvinToCelcius(wformula.australianAapparentTemperature( + that.currentReport.TemperatureApparent = wformula.kelvinToCelcius(wformula.australianAapparentTemperature( wformula.celciusToKelvin(that.currentReport.Temperature), that.currentReport.Humidity, that.currentReport.WindSpeed)); + that.currentReport.TemperatureWetBulb = + converter.getWetBulbTemperature(that.currentReport.Temperature, that.currentReport.Humidity); + } that.currentReport.TemperatureMin = (that.currentReport.Temperature < that.currentReport.TemperatureMin) ? that.currentReport.Temperature : that.currentReport.TemperatureMin; - that.currentReport.TemperatureWetBulb = - converter.getWetBulbTemperature(that.currentReport.Temperature, that.currentReport.Humidity); that.currentReport.LightningStrikes = message.obs[0][4]; that.currentReport.LightningAvgDistance = message.obs[0][5]; + + // Report battery status. + var previousLevel = that.currentReport.AirSensorBatteryLevel; that.currentReport.AirSensorBatteryLevel = this.getBatteryPercent(message.obs[0][6]); + // If the AIR sensor has the lowest battery level, then report it as the Station battery level + if (that.currentReport.AirSensorBatteryLevel < that.currentReport.SkySensorBatteryLevel) { + that.currentReport.BatteryLevel = that.currentReport.AirSensorBatteryLevel; + // It could have a solar panel on it, so check to see if it is going up (charging) + that.currentReport.BatteryIsCharging = false; + if (that.currentReport.BatteryLevel > previousLevel) { + that.currentReport.BatteryIsCharging = true; + } + } } if (message.type == 'obs_sky') { @@ -326,10 +460,7 @@ class TempestAPI that.currentReport.ObservationStation = that.currentReport.SkySerialNumber; that.currentReport.SkyFirmware = message.firmware_revision; that.currentReport.ObservationTime = moment.unix(message.obs[0][0]).format('HH:mm:ss'); - if (that.currentReport.LightLevelSensorFail == 1) - that.currentReportLightLevel = 0; - else - that.currentReport.LightLevel = message.obs[0][1]; + that.currentReport.LightLevel = message.obs[0][1]; that.currentReport.UVIndex = message.obs[0][2]; that.currentReport.Rain1h = this.getHourlyAccumulatedRain(message.obs[0][0], message.obs[0][3]); @@ -342,7 +473,19 @@ class TempestAPI that.currentReport.WindSpeedLull = message.obs[0][4]; that.currentReport.WindSpeedMax = message.obs[0][6]; + // Report battery status. + var previousLevel = that.currentReport.SkySensorBatteryLevel; that.currentReport.SkySensorBatteryLevel = this.getBatteryPercent(message.obs[0][8]); + // If the SKY sensor has the lowest battery level, then report it as the Station battery level + if (that.currentReport.SkySensorBatteryLevel < that.currentReport.AirSensorBatteryLevel) { + that.currentReport.BatteryLevel = that.currentReport.SkySensorBatteryLevel; + // It could have a solar panel on it, so check to see if it is going up (charging) + that.currentReport.BatteryIsCharging = false; + if (that.currentReport.BatteryLevel > previousLevel) { + that.currentReport.BatteryIsCharging = true; + } + } + that.currentReport.SolarRadiation = message.obs[0][10]; // Note that Local Day Rain Accumulation (Field 11) is currently always null. Hence we have to approximate it with data available to us. that.currentReport.RainBool = message.obs[0][3] > 0 ? true : false; @@ -371,30 +514,26 @@ class TempestAPI //that.currentReport.WindDirection = converter.getWindDirection(message.obs[0][4]); that.currentReport.AirPressure = message.obs[0][6]; - if (that.currentReport.TemperatureSensorFail == 1) - that.currentReport.Temperature = 0; - else - that.currentReport.Temperature = message.obs[0][7]; - if (that.currentReport.HumiditySensorFail == 1) { - that.currentReport.Humidity = 0; - that.currentReport.DewPoint = 0; - } - else { - that.currentReport.Humidity = message.obs[0][8]; + that.currentReport.Temperature = message.obs[0][7]; + that.currentReport.Humidity = message.obs[0][8]; + + // Only perform new calculations if temperature and humidity values are within a good range + // We could get out of range values if the sensors have failed. + if (that.currentReport.Humidity > 0 && that.currentReport.Humidity <= 100 && + that.currentReport.Temperature > -100 && that.currentReport.Temperature < 100) { that.currentReport.DewPoint = wformula.kelvinToCelcius(wformula.dewPointMagnusFormula( wformula.celciusToKelvin(that.currentReport.Temperature), that.currentReport.Humidity)); - } - that.currentReport.TemperatureApparent = wformula.kelvinToCelcius(wformula.australianAapparentTemperature( + + that.currentReport.TemperatureApparent = wformula.kelvinToCelcius(wformula.australianAapparentTemperature( wformula.celciusToKelvin(that.currentReport.Temperature), that.currentReport.Humidity, message.obs[0][2])); - that.currentReport.TemperatureWetBulb = + that.currentReport.TemperatureWetBulb = converter.getWetBulbTemperature(that.currentReport.Temperature, that.currentReport.Humidity); - if (that.currentReport.LightLevelSensorFail == 1) - that.currentReport.LightLevel = 0; - else - that.currentReport.LightLevel = message.obs[0][9]; + } + + that.currentReport.LightLevel = message.obs[0][9]; that.currentReport.UVIndex = message.obs[0][10]; that.currentReport.SolarRadiation = message.obs[0][11]; that.currentReport.Rain1h = this.getHourlyAccumulatedRain(message.obs[0][0], message.obs[0][12]); @@ -423,14 +562,138 @@ class TempestAPI } } - parseForecasts(forecastObjs, timezone) + + getForecastConditionCategory(icon, detail = false) + { + // Convert the icon names to condition category + // See https://weatherflow.github.io/Tempest/api/swagger/#!/forecast/getBetterForecast + + if (icon.includes("thunderstorm") || icon.includes("windy")) + { + // Severe weather + return detail ? 9 : 2; + } + else if (icon.includes("snow")) + { + // Snow + return detail ? 8 : 3; + } + else if (icon.includes("sleet")) + { + // Hail + return detail ? 7 : 3; + } + else if (icon.includes("rain")) + { + // Rain + return detail ? 6 : 2; + } + else if (icon.includes("fog")) + { + // Fog + return detail ? 4 : 1; + } + else if (icon.includes("partly-cloudy")) + { + // Few Clouds + return detail ? 1 : 0; + } + else if (icon.includes("cloudy")) + { + // Overcast + return detail ? 3 : 1; + } + else if (icon.includes("clear")) + { + // Clear + return 0; + } + else + { + this.log.warn("Unknown Tempest Forecast icon " + icon); + return 0; + } + }; + + getForecastData(callback) { /* - TODO: Add support for forecasts Weatherflow do provide an API to get forecasts https://weatherflow.github.io/Tempest/api/remote-developer-policy.html + https://weatherflow.github.io/Tempest/api/swagger/#!/forecast/getBetterForecast */ - return []; + this.log.debug("Getting weather forecast for station %s", this.locationId); + + // Response defaults to metric + const queryUri = "https://swd.weatherflow.com/swd/rest/better_forecast?station_id=" + this.locationId + "&token=" + this.apiKey; + request(encodeURI(queryUri), (requestError, response, body) => + { + if (!requestError) + { + if (response.statusCode == 200) { + let parseError; + let weather + try + { + weather = JSON.parse(body); + } catch (e) + { + parseError = e; + } + callback(parseError, weather); + } else { + callback(true, body); + } + } + else + { + callback(requestError); + } + }); + } + + + parseForecasts(observation_time, values, timezone) + { + let forecasts = []; + + // Check to see if we have 'Todays' forecast as it may be too late + // in the day (11pm-midnight) for a daily forecast + // If we don't have 'Todays' forecast, then the forecasts will start from tomorrow. + let currentDay = moment.unix(observation_time).tz(timezone).date(); + let forecastOffset = values[0].day_num == currentDay ? 0 : 1; + this.log.debug("Weatherflow forecast. Today is:" + currentDay + " First forecast day:" + values[0].day_num); + + for (let i = 0; i < values.length; i++) { + // this.log(values[i]); + let forecast = {}; + forecast.Condition = values[i].conditions; + forecast.ConditionCategory = this.getForecastConditionCategory(values[i].icon, this.conditionDetail); + forecast.ForecastDay = moment.unix(values[i].day_start_local).tz(timezone).format('dddd'); + let detailedCondition = this.getForecastConditionCategory(values[i].icon, true); + forecast.RainBool = [5, 6, 9].includes(detailedCondition); + forecast.SnowBool = [7, 8].includes(detailedCondition); + forecast.SunriseTime = moment.unix(values[i].sunrise).tz(timezone).format('HH:mm:ss'); + forecast.SunsetTime = moment.unix(values[i].sunset).tz(timezone).format('HH:mm:ss'); + forecast.TemperatureMax = values[i].air_temp_high; + forecast.TemperatureMin = values[i].air_temp_low; + forecast.RainChance = values[i].precip_probability; + forecast.ObservationTime = moment.unix(observation_time).tz(timezone).format('HH:mm:ss'); + + forecasts[i+forecastOffset] = forecast; + } + + if (forecastOffset == 0) { + // If we have the current day forecast save it away + this.savedDayForecast = forecasts[0]; + this.log.debug("Weatherflow storing today's forecast"); + } else { + // If we don't have the current day forecast, present the last saved one + this.log.debug("Weatherflow inserting saved today's forecast"); + forecasts[0] = this.savedDayForecast; + } + + return forecasts; } } diff --git a/apis/weatherunderground.js b/apis/weatherunderground.js index 1b3942b..3b1942c 100644 --- a/apis/weatherunderground.js +++ b/apis/weatherunderground.js @@ -21,7 +21,8 @@ class WundergroundAPI 'AirPressure', 'WindSpeed', 'WindSpeedMax', - 'RainDay' + 'RainDay', + 'RainBool' ]; this.log = log; @@ -39,10 +40,10 @@ class WundergroundAPI let weather = {}; let that = this; - const queryUri = "https://api.weather.com/v2/pws/observations/current?apiKey=" + this.apiKey + "&stationId=" + this.location + "&format=json&units=" + this.units + '&numericPrecision=decimal'; + const queryUri = "https://api.weather.com/v2/pws/observations/current?apiKey=" + this.apiKey + "&stationId=" + this.location + "&format=json&units=m" + '&numericPrecision=decimal'; request(encodeURI(queryUri), function (err, response, body) { - if (!err) + if (!err && body.length > 0) { // Current weather report try @@ -61,14 +62,21 @@ class WundergroundAPI } catch (e) { that.log.error("Error retrieving weather report and forecast"); + that.log.error("Response Object: " + body); that.log.error("Error Message: " + e); callback(e); } } else { - that.log.error("Error retrieving weather report and forecast"); + that.log.error("Weather Underground Request failed"); that.log.error("Error Message: " + err); + if (typeof response !== 'undefined') { + that.log.error("Response statusCode: " + response.statusCode + " statusMessage: " + response.statusMessage); + if (response.statusCode == 204) { + that.log.error("Check to make sure your PWS is not offline. https://www.wunderground.com/member/devices") + } + } callback(err); } }.bind(this)); @@ -82,26 +90,7 @@ class WundergroundAPI try { let observation = json.observations[0]; - let values; - this.log.debug("Units: " + this.units); - - // Get values depending on chosen unit in request - if (this.units === 's') - { - values = observation.metric_si; - } - else if (this.units === 'm') - { - values = observation.metric; - } - else if (this.units === 'e') - { - values = observation.imperial; - } - else - { // 'h' - values = observation.uk_hybrid; - } + let values = observation.metric; report.ObservationStation = observation.stationID + " : " + observation.neighborhood; report.ObservationTime = moment(Date.parse(observation.obsTimeUtc)).format('HH:mm:ss'); @@ -115,6 +104,7 @@ class WundergroundAPI report.WindSpeed = isNaN(values.windSpeed) ? 0 : values.windSpeed; report.WindSpeedMax = isNaN(values.windGust) ? 0 : values.windGust; report.RainDay = isNaN(values.precipTotal) ? 0 : values.precipTotal; + report.RainBool = isNaN(values.precipRate) ? false : (values.precipRate > 0 ? true : false); } catch (error) { diff --git a/apis/yahoo.js b/apis/yahoo.js deleted file mode 100644 index e16ff5b..0000000 --- a/apis/yahoo.js +++ /dev/null @@ -1,168 +0,0 @@ -/*jshint esversion: 6,node: true,-W041: false */ -"use strict"; - -const request = require('request'), - converter = require('../util/converter'), - moment = require('moment-timezone'), - geoTz = require('geo-tz'); - - -class YahooAPI -{ - constructor(location, l) - { - this.attribution = 'Powered by Yahoo'; - this.reportCharacteristics = [ - 'AirPressure', - 'Condition', - 'ConditionCategory', - 'ForecastDay', - 'Humidity', - 'Temperature', - 'TemperatureMin', - 'Visibility', - 'WindDirection', - 'WindSpeed' - ]; - this.forecastCharacteristics = [ - 'Condition', - 'ConditionCategory', - 'ForecastDay', - 'Temperature', - 'TemperatureMin' - ]; - this.forecastDays = 10; - - this.location = location; - this.log = l; - } - - update(forecastDays, callback) - { - this.log.debug("Updating weather with Yahoo"); - - const queryUri = `https://query.yahooapis.com/v1/public/yql?q=select * from weather.forecast where u='c' AND woeid in (select woeid from geo.places(1) where text="${this.location}")&format=json`; - request(encodeURI(queryUri), function (err, response, body) - { - if (!err) - { - // Current weather report - const jsonObj = JSON.parse(body); - this.parseReport(jsonObj.query.results.channel, callback); - } - else - { - this.log.error("Error retrieving weather report and forecast"); - this.log.error("Error Message: " + err); - callback(err); - } - }.bind(this)); - } - - parseReport(values, callback) - { - let report = {}; - const timezone = String(geoTz(parseFloat(values.item.lat), parseFloat(values.item.long))); - this.log.debug("Using Timezone: " + timezone); - - report.AirPressure = parseInt(values.atmosphere.pressure); - report.Condition = values.item.condition.text; - report.ConditionCategory = this.getConditionCategory(parseInt(values.item.condition.code)); - report.ForecastDay = moment(values.item.forecast[0].date, "DD MMM YYYY").tz(timezone).format("dddd"); - report.Humidity = parseInt(values.atmosphere.humidity); - report.ObservationTime = moment(values.item.pubDate.substr(17), "hh:mm A [CEST]").tz(timezone).format('HH:mm:ss'); - report.Temperature = parseInt(values.item.condition.temp); - report.TemperatureMin = parseInt(values.item.forecast[0].low); - report.Visibility = parseFloat(values.atmosphere.visibility); - report.WindDirection = converter.getWindDirection(parseInt(values.wind.direction)); - report.WindSpeed = parseFloat(values.wind.speed); - - const weather = {}; - weather.report = report; - weather.forecasts = this.parseForecasts(values.item.forecast, timezone); - callback(null, weather); - } - - parseForecasts(forecastObjs, timezone) - { - let forecasts = []; - for (let i = 0; i < forecastObjs.length; i++) - { - const values = forecastObjs[i]; - const forecast = {}; - forecast.Condition = values.text; - forecast.ConditionCategory = this.getConditionCategory(parseInt(values.code)); - forecast.ForecastDay = moment(values.date, "DD MMM YYYY").tz(timezone).format("dddd"); - forecast.Temperature = values.high; - forecast.TemperatureMin = values.low; - forecasts[forecasts.length] = forecast; - } - return forecasts; - } - - getConditionCategory(code) - { - // See https://developer.yahoo.com/weather/documentation.html#codes - switch (code) - { - case 5: - case 6: - case 7: - case 8: - case 10: - case 13: - case 14: - case 15: - case 16: - case 17: - case 18: - case 35: - case 41: - case 42: - case 43: - case 46: - return 3; // snow - case 0: - case 1: - case 2: - case 3: - case 9: - case 11: - case 12: - case 37: - case 38: - case 39: - case 40: - case 45: - case 47: - return 2; // rain - case 19: - case 20: - case 21: - case 22: - case 23: - case 24: - case 26: - case 27: - case 28: - case 29: - case 30: - return 1; // cloudy - case 25: - case 31: - case 32: - case 33: - case 34: - case 36: - case 44: - case 3200: - return 0; - default: - return 0; // clear - } - }; -} - -module.exports = { - YahooAPI: YahooAPI -}; \ No newline at end of file diff --git a/config.schema.json b/config.schema.json index 2adf448..372c5d9 100644 --- a/config.schema.json +++ b/config.schema.json @@ -249,16 +249,16 @@ "fakegatoParameters": { "type": "string" }, - "tresholdAirPressure": { + "thresholdAirPressure": { "type": "integer" }, - "tresholdCloudCover": { + "thresholdCloudCover": { "type": "integer" }, - "tresholdUvIndex": { + "thresholdUvIndex": { "type": "integer" }, - "tresholdWindSpeed": { + "thresholdWindSpeed": { "type": "integer" } } @@ -308,42 +308,46 @@ "type": "help", "helpvalue": "
Generate your free api key after registering here.
", "condition": { - "functionBody": "return model.stations[0].service === 'openweathermap'" + "functionBody": "return model.stations[arrayIndices[0]].service === 'openweathermap'" } }, { "type": "help", "helpvalue": "Registration is available for users with a PWS. Get your free api key here.
", "condition": { - "functionBody": "return model.stations[0].service === 'weatherunderground'" + "functionBody": "return model.stations[arrayIndices[0]].service === 'weatherunderground'" } }, { "type": "help", "helpvalue": "URL for JSON File (Include full URL).
", "condition": { - "functionBody": "return model.stations[0].service === 'weewx'" + "functionBody": "return model.stations[arrayIndices[0]].service === 'weewx'" } }, { - "key": "stations[].key", - "notitle": true, + "type": "help", + "helpvalue": "(Optional) Personal Access Token to obtain forecasts. Log into Tempest and go to Settings -> Data Authorizations -> Create Token
", "condition": { - "functionBody": "return model.stations[0].service != 'tempest'" + "functionBody": "return model.stations[arrayIndices[0]].service === 'tempest'" } }, + { + "key": "stations[].key", + "notitle": true + }, { "type": "help", "helpvalue": "Find your city here.
", "condition": { - "functionBody": "return model.stations[0].service === 'openweathermap'" + "functionBody": "return model.stations[arrayIndices[0]].service === 'openweathermap'" } }, { "type": "help", "helpvalue": "City only needed with Tempest when Eve app is used with multiple weather stations (same or different homes).
", "condition": { - "functionBody": "return model.stations[0].service === 'tempest'" + "functionBody": "return model.stations[arrayIndices[0]].service === 'tempest'" } }, { @@ -351,14 +355,14 @@ "notitle": true, "placeholder": "Berlin, DE", "condition": { - "functionBody": "return model.stations[0].service === 'openweathermap' || model.stations[0].service === 'tempest'" + "functionBody": "return model.stations[arrayIndices[0]].service === 'openweathermap' || model.stations[arrayIndices[0]].service === 'tempest'" } }, { "type": "help", "helpvalue": "Optional: Enter geo coordinates (1. latitude, 2. longitude). Find your coordinates here.
", "condition": { - "functionBody": "return model.stations[0].service === 'openweathermap'" + "functionBody": "return model.stations[arrayIndices[0]].service === 'openweathermap'" } }, { @@ -373,14 +377,14 @@ } ], "condition": { - "functionBody": "return model.stations[0].service === 'openweathermap'" + "functionBody": "return model.stations[arrayIndices[0]].service === 'openweathermap'" } }, { "type": "help", "helpvalue": "