From 8d361d3baf0df4474f6efcdd6039fa2645be6775 Mon Sep 17 00:00:00 2001 From: David Carson <44933987+dacarson@users.noreply.github.com> Date: Mon, 18 Mar 2024 02:29:52 -0700 Subject: [PATCH] Fix tile labels, add sunrise/sunset and report sensor failure (#285) * Add support to report failing Tempest sensors Fix #279 Fully parse the sensor failure number, and log the failure. Add support to report SensorFault to HomeKit. There are only two options, No failure or General Failure. Tested by setting random numbers for message.sensor_status, and verifying that failures appeared in the log, and that Eve app reports general error to the user. Apple's Home app doesn't seem to expose Sensor Fault. * Ignore other values from sensor_status #279 While testing, I noticed that sometimes my Tempest unit would have values in the sensor_status that are not documented. The documentation does say to ignore them. * Update Tile text with value #247 The value for unsupported fields were being reported in the title of a sensor. It seems this functionality broke in iOS 16, when they changed the behaviour of Characteristic.Name. To fix this, we need to use Characteristic.ConfiguredName. * Drop duplicate messages Seeing duplicate UDP messages events, though duplicates are not appearing on the wire/network. Duplicate messages are a problem for rain totals, as the rain amount is added twice. Add some code to drop duplicate messages. * Add Openweather Sunrise & Sunset support #240 Sunrise/Sunset Openweather API 2.5 has Sunrise and Sunset values. This patch adds support for it. I also saw that 3.0 API also has support for it for the current conditions. This patch also adds support for that too. * Update config.schema.json Fix for #122 * Add Tempest forecast support Add the support for Tempest weather station forecasts. The weather station broadcasts new current conditions each minute so the refresh interval is hardcoded to 1 minute. However forecasts don't update that frequently, so I have forecast updating each hour. * Improve error reporting for OpenWeatherMap fails To help diagnose #282 and to help with general debugging, log not just the result but the error when OpenWeatherMap fails to load the URL. * Add light level history Fix #287 Through fakegato "custom" history support, the light level (lux) history is now supported. Adjust history logging to include light level. Tested with Tempest weather station and OpenWeatherMap V2.5 API (that does *not* include light level). * Remove unsupported Yahoo weather. #45 Yahoo doesn't provide weather information anymore. Support for the Yahoo weather service has been removed, so remove the api file. Update the packages.json file, remove yahoo. Add the company that makes the Tempest weather station, Weatherflow. * Add better error handling to Weather Underground. Debug #246 Add a check to see if WeatherUnderground it returning correctly, but the PWS is offline. * Increase maximum UV Index value Australia's outback has higher UV Index values than 12. I see it hit 13 at times. * Add rain sensor for weather underground Fix: #112 Add support for the rain sensor (RainBool). If the current precipitation rate is greater than zero, then it is raining. Also, always use metric values. Conversion from metric to desired unit happens during rendering the value. Tested with both an offline PWS and online PWS. * Report values even for sensor failed Fix #288 Even if the sensor has been reported as failed, it has been observed that values may still be good. As the values could be out of range, don't use them for calculations. * Calculate values only if numbers in range #285 Patch feedback. Calculate the calculated values only if temperature and humidity values are within range. The values may be out of range if the sensors have failed. * Log sensor failures once an hour #285. Patch feedback Log is spammed if a sensor fails, so only log once every hour. Also, don't need to track Temperature and Humidity sensor failures specifically anymore as calculations are bounds checked. * Revert "Log sensor failures once an hour" This reverts commit e8ca35018dd568bce15b5d657d0075c21091e112. * Log sensor failures once an hour #285. Patch feedback Log is spammed if a sensor fails, so only log once every hour. As sensor status for AIR unit could be ok, but the sensor status for SKY unit be bad, I need to track sensor logging separately. I also overload the AIR unit to report Tempest unit data. Also, don't need to track Temperature and Humidity sensor failures specifically anymore as calculations are bounds checked. * Fix forecasts to be error resilient #285 Patch feedback Fix crash when the apiKey and/or stationId is missing. Use the lastForecastUpdate variable to indicate if we have valid parameters at all. If they are not set, don't define the variable, and as such never attempt to fetch a forecast. This is a valid configuration so I don't log this state. If invalid apiKey and/or stationId is used, then the URL request will fail with a response code other than 200 (OK). If that happens, fail with an error. This error will get logged. As the forecast is only fetched every hour, this error will only appear once an hour and won't spam the logs. * Sensor error string format #285 Patch feedback Add better formatting for sensor error logs, so that each failing sensor is comma delimited. Also fix range checking for calculated values, allow temperatures to be between -100 to +100 degC. * Fix battery reporting for AIR/SKY #285 Patch feedback Move battery reporting/handling to one place, in the observation messages. Tempest handling was already correct. Add reporting for AIR/SKY battery level. As the levels can be different but we only have one battery value we can report, report the lowest one. Change AIR/SKY battery percentage calculator to be the same as Tempest. * AIR/SKY battery level never goes up I should not compare to the previous reported value, as it will never increase. I need to compare Sky battery with Air battery and vice versa when determining the lowest value to report. * Increase robustness of Forecast Patch feedback #285 Even though the server says it returned forecast ok with a 200 OK status code, parsing sometimes fails and crashes. Catch this error and print it out if it happens, including the full received result and the parse error. * Forecast not updating Patch feedback #285 Forecast wasn't updating when it clocked over to the new year. The code was testing for an undefined variable incorrectly. * Forecast said there will be rain when there won't be Patch feedback #285 Should not use the precipitation icons for snow and rain state as the chance precipitation can be zero, but there are still icons. Instead do the same as OpenWeatherMap, normalize the conditions to the condition details numbered values, then check to see if there will be rain or snow. * Fix crash when Wunderground server unreachable There could be no response object if the internet and/or weather underground server is not reachable. So before trying to read the response from the server, make sure that there is a response. * Fix today's forecast Today's forecast isn't sent after around 11pm. To detect this, make sure that the current month day is the same as the month day of the forecast. If they are different, then offset the forecasts. Save the current day forecast, so that when it is not available, the last forecast can be inserted into the forecasts array. --- CHANGELOG.md | 14 ++ README.md | 22 +- accessories/currentConditions.js | 13 +- apis/openweathermap.js | 46 ++-- apis/weatherflow.js | 399 +++++++++++++++++++++++++------ apis/weatherunderground.js | 38 ++- apis/yahoo.js | 168 ------------- config.schema.json | 52 ++-- index.js | 28 ++- package.json | 6 +- util/characteristics.js | 2 +- util/compatibility.js | 12 + 12 files changed, 476 insertions(+), 324 deletions(-) delete mode 100644 apis/yahoo.js 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": "