Instrument Neutral Distributed Interface INDI 2.2.5
weatherflow.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2024 WeatherFlow Tempest Weather Driver
3
4 INDI WeatherFlow Tempest Weather Driver
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the Free
8 Software Foundation; either version 2 of the License, or (at your option)
9 any later version.
10
11 This program is distributed in the hope that it will be useful, but WITHOUT
12 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20
21 The full GNU General Public License is included in this distribution in the
22 file called LICENSE.
23*******************************************************************************/
24
25#include "weatherflow.h"
26
27#include "locale_compat.h"
28
29#include <sstream>
30#include <iomanip>
31#include <httplib.h>
32#include <limits>
33#include <thread>
34
35// We declare an auto pointer to WeatherFlow.
36std::unique_ptr<WeatherFlow> weatherFlow(new WeatherFlow());
37
39{
40 setVersion(1, 0);
42
43 // Initialize rate limiting
44 m_lastRequestTime = std::chrono::system_clock::now();
45 m_requestCount = 0;
46}
47
51
53{
54 return "WeatherFlow";
55}
56
57void WeatherFlow::ISGetProperties(const char *dev)
58{
60 defineProperty(wfAPIKeyTP);
61 defineProperty(wfStationIDTP);
62 defineProperty(wfSettingsNP);
63}
64
66{
67 if (wfAPIKeyTP[0].isEmpty())
68 {
69 LOG_ERROR("WeatherFlow API Key is not available. Please register your API key at "
70 "https://tempestwx.com/settings/tokens and save it under Options.");
71 return false;
72 }
73
74 // Check rate limiting
75 auto now = std::chrono::system_clock::now();
76 auto timeSinceLastRequest = std::chrono::duration_cast<std::chrono::seconds>(now - m_lastRequestTime).count();
77
78 if (timeSinceLastRequest >= RATE_LIMIT_PERIOD)
79 {
80 m_requestCount = 0;
81 m_lastRequestTime = now;
82 }
83
84 if (m_requestCount >= RATE_LIMIT_REQUESTS)
85 {
86 LOG_ERROR("API rate limit exceeded. Please wait before making more requests.");
87 return false;
88 }
89
90 // Fetch station information if station ID is not provided
91 if (wfStationIDTP[0].isEmpty())
92 {
93 if (!fetchStationInfo())
94 {
95 LOG_ERROR("Failed to fetch station information. Please provide a station ID manually.");
96 return false;
97 }
98 }
99 else
100 {
101 m_stationID = wfStationIDTP[0].getText();
102 }
103
104 m_isConnected = true;
105 LOG_INFO("WeatherFlow connection established successfully.");
106 return true;
107}
108
110{
111 m_isConnected = false;
112 LOG_INFO("WeatherFlow disconnected.");
113 return true;
114}
115
117{
119
120 // API Key
121 wfAPIKeyTP[0].fill("API_KEY", "API Key", "");
122 wfAPIKeyTP.fill(getDeviceName(), "WF_API_KEY", "WeatherFlow", OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
123 wfAPIKeyTP.load();
124
125 // Station ID (optional)
126 wfStationIDTP[0].fill("STATION_ID", "Station ID", "");
127 wfStationIDTP.fill(getDeviceName(), "WF_STATION_ID", "WeatherFlow", OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
128 wfStationIDTP.load();
129
130 // Settings
131 wfSettingsNP[WF_UPDATE_INTERVAL].fill("UPDATE_INTERVAL", "Update Interval (s)", "%.0f", 30, 3600, 30, 60);
132 wfSettingsNP[WF_CONNECTION_TIMEOUT].fill("CONNECTION_TIMEOUT", "Connection Timeout (s)", "%.0f", 5, 120, 5, 30);
133 wfSettingsNP[WF_RETRY_ATTEMPTS].fill("RETRY_ATTEMPTS", "Retry Attempts", "%.0f", 1, 10, 1, 3);
134 wfSettingsNP.fill(getDeviceName(), "WF_SETTINGS", "Settings", OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
135
136 // Weather parameters with appropriate ranges for observatory safety
137 addParameter("WEATHER_TEMPERATURE", "Temperature (°C)", -40, 50, 15);
138 addParameter("WEATHER_HUMIDITY", "Humidity (%)", 0, 100, 15);
139 addParameter("WEATHER_PRESSURE", "Pressure (hPa)", 800, 1200, 15);
140 addParameter("WEATHER_WIND_SPEED", "Wind Speed (m/s)", 0, 30, 15);
141 addParameter("WEATHER_WIND_GUST", "Wind Gust (m/s)", 0, 50, 15);
142 addParameter("WEATHER_WIND_DIRECTION", "Wind Direction (degrees)", 0, 360, 15);
143 addParameter("WEATHER_RAIN_HOUR", "Rain Accumulation (mm)", 0, 200, 15);
144 addParameter("WEATHER_RAIN_RATE", "Rain Rate (mm/hr)", 0, 100, 15);
145 addParameter("WEATHER_SOLAR_RADIATION", "Solar Radiation (W/m²)", 0, 1200, 15);
146 addParameter("WEATHER_UV", "UV Index", 0, 15, 15);
147
148 // Set critical parameters for observatory safety
149 setCriticalParameter("WEATHER_TEMPERATURE");
150 setCriticalParameter("WEATHER_HUMIDITY");
151 setCriticalParameter("WEATHER_PRESSURE");
152 setCriticalParameter("WEATHER_WIND_SPEED");
153 setCriticalParameter("WEATHER_WIND_GUST");
154 setCriticalParameter("WEATHER_RAIN_HOUR");
155 setCriticalParameter("WEATHER_RAIN_RATE");
156
158
159 return true;
160}
161
162bool WeatherFlow::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
163{
164 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
165 {
166 if (wfAPIKeyTP.isNameMatch(name))
167 {
168 wfAPIKeyTP.update(texts, names, n);
169 wfAPIKeyTP.setState(IPS_OK);
170 wfAPIKeyTP.apply();
171 saveConfig(wfAPIKeyTP);
172 return true;
173 }
174
175 if (wfStationIDTP.isNameMatch(name))
176 {
177 wfStationIDTP.update(texts, names, n);
178 wfStationIDTP.setState(IPS_OK);
179 wfStationIDTP.apply();
180 saveConfig(wfStationIDTP);
181 return true;
182 }
183 }
184
185 return INDI::Weather::ISNewText(dev, name, texts, names, n);
186}
187
188bool WeatherFlow::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
189{
190 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
191 {
192 if (wfSettingsNP.isNameMatch(name))
193 {
194 wfSettingsNP.update(values, names, n);
195 wfSettingsNP.setState(IPS_OK);
196 wfSettingsNP.apply();
197 saveConfig(wfSettingsNP);
198 return true;
199 }
200 }
201
202 return INDI::Weather::ISNewNumber(dev, name, values, names, n);
203}
204
205bool WeatherFlow::updateLocation(double latitude, double longitude, double elevation)
206{
207 INDI_UNUSED(elevation);
208
209 // WeatherFlow uses its own station location, so we don't need to update location
210 // But we can log it for debugging purposes
211 LOGF_DEBUG("Location updated: Lat %.6f, Lon %.6f", latitude, longitude);
212
213 return true;
214}
215
217{
218 if (!m_isConnected)
219 {
220 LOG_ERROR("WeatherFlow is not connected.");
221 return IPS_ALERT;
222 }
223
224 // Check rate limiting
225 auto now = std::chrono::system_clock::now();
226 auto timeSinceLastRequest = std::chrono::duration_cast<std::chrono::seconds>(now - m_lastRequestTime).count();
227
228 if (timeSinceLastRequest >= RATE_LIMIT_PERIOD)
229 {
230 m_requestCount = 0;
231 m_lastRequestTime = now;
232 }
233
234 if (m_requestCount >= RATE_LIMIT_REQUESTS)
235 {
236 LOG_ERROR("API rate limit exceeded. Please wait before making more requests.");
237 return IPS_ALERT;
238 }
239
240 // Fetch current weather observations
241 if (!fetchCurrentObservations())
242 {
243 LOG_ERROR("Failed to fetch weather observations.");
244 return IPS_ALERT;
245 }
246
247 // Update weather parameters with the latest data
248 setParameterValue("WEATHER_TEMPERATURE", m_lastData.air_temperature);
249 setParameterValue("WEATHER_HUMIDITY", m_lastData.relative_humidity);
250 setParameterValue("WEATHER_PRESSURE", m_lastData.barometric_pressure);
251 setParameterValue("WEATHER_WIND_SPEED", m_lastData.wind_avg);
252 setParameterValue("WEATHER_WIND_GUST", m_lastData.wind_gust);
253 setParameterValue("WEATHER_WIND_DIRECTION", m_lastData.wind_direction);
254 setParameterValue("WEATHER_RAIN_HOUR", m_lastData.precip_accum_local_day);
255 setParameterValue("WEATHER_RAIN_RATE", m_lastData.precip_rate);
256 setParameterValue("WEATHER_SOLAR_RADIATION", m_lastData.solar_radiation);
257 setParameterValue("WEATHER_UV", m_lastData.uv);
258
259 m_lastUpdate = now;
260 LOGF_DEBUG("Weather data updated: Temp=%.1f°C, Humidity=%.1f%%, Wind=%.1fm/s",
261 m_lastData.air_temperature, m_lastData.relative_humidity, m_lastData.wind_avg);
262
263 return IPS_OK;
264}
265
267{
269
270 wfAPIKeyTP.save(fp);
271 wfStationIDTP.save(fp);
272 wfSettingsNP.save(fp);
273
274 return true;
275}
276
277bool WeatherFlow::fetchStationInfo()
278{
279 std::string response;
280 std::string endpoint = STATIONS_ENDPOINT + "?token=" + wfAPIKeyTP[0].getText();
281
282 if (!makeAPIRequest(endpoint, response))
283 {
284 LOG_ERROR("Failed to fetch station information from WeatherFlow API.");
285 return false;
286 }
287
288 return parseStationResponse(response);
289}
290
291bool WeatherFlow::fetchCurrentObservations()
292{
293 std::string response;
294 std::string endpoint;
295
296 // Always use station observations: the device endpoint returns arrays, not
297 // key-value objects, so named fields like barometric_pressure are not parseable.
298 endpoint = OBSERVATIONS_ENDPOINT + m_stationID + "?token=" + wfAPIKeyTP[0].getText();
299
300 if (!makeAPIRequest(endpoint, response))
301 {
302 LOG_ERROR("Failed to fetch current observations from WeatherFlow API.");
303 return false;
304 }
305
306 return parseObservationsResponse(response);
307}
308
309bool WeatherFlow::parseStationResponse(const std::string &response)
310{
311 try
312 {
313 json stationData = json::parse(response);
314
315 if (stationData.contains("status") && stationData["status"]["status_code"] == 0)
316 {
317 if (stationData.contains("stations") && !stationData["stations"].empty())
318 {
319 auto station = stationData["stations"][0];
320 m_stationID = std::to_string(station["station_id"].get<int>());
321
322 if (station.contains("devices") && !station["devices"].empty())
323 {
324 auto device = station["devices"][0];
325 m_deviceID = std::to_string(device["device_id"].get<int>());
326 }
327
328 LOGF_INFO("Auto-detected station ID: %s, device ID: %s", m_stationID.c_str(), m_deviceID.c_str());
329 return true;
330 }
331 }
332 else
333 {
334 std::string errorMsg = "Unknown error";
335 if (stationData.contains("status") && stationData["status"].contains("status_message"))
336 {
337 errorMsg = stationData["status"]["status_message"].get<std::string>();
338 }
339 LOGF_ERROR("WeatherFlow API error: %s", errorMsg.c_str());
340 }
341 }
342 catch (json::exception &e)
343 {
344 LOGF_ERROR("Error parsing station response: %s", e.what());
345 }
346
347 return false;
348}
349
350bool WeatherFlow::parseObservationsResponse(const std::string &response)
351{
352 try
353 {
354 json obsData = json::parse(response);
355
356 if (obsData.contains("status") && obsData["status"]["status_code"] == 0)
357 {
358 if (obsData.contains("obs") && !obsData["obs"].empty())
359 {
360 auto observation = obsData["obs"][0];
361
362 // Extract weather data with proper error handling
363 auto extractDouble = [&observation](const std::string & key, double defaultValue = 0.0) -> double
364 {
365 if (observation.contains(key) && !observation[key].is_null())
366 {
367 return observation[key].get<double>();
368 }
369 return defaultValue;
370 };
371
372 m_lastData.air_temperature = extractDouble("air_temperature");
373 m_lastData.relative_humidity = extractDouble("relative_humidity");
374 m_lastData.barometric_pressure = extractDouble("barometric_pressure");
375 m_lastData.wind_avg = extractDouble("wind_avg");
376 m_lastData.wind_gust = extractDouble("wind_gust");
377 m_lastData.wind_direction = extractDouble("wind_direction");
378 m_lastData.precip_accum_local_day = extractDouble("precip_accum_local_day");
379 m_lastData.precip_rate = extractDouble("precip_rate");
380 m_lastData.solar_radiation = extractDouble("solar_radiation");
381 m_lastData.uv = extractDouble("uv");
382 m_lastData.timestamp = std::chrono::system_clock::now();
383
384 return true;
385 }
386 }
387 else
388 {
389 std::string errorMsg = "Unknown error";
390 if (obsData.contains("status") && obsData["status"].contains("status_message"))
391 {
392 errorMsg = obsData["status"]["status_message"].get<std::string>();
393 }
394 LOGF_ERROR("WeatherFlow API error: %s", errorMsg.c_str());
395 }
396 }
397 catch (json::exception &e)
398 {
399 LOGF_ERROR("Error parsing observations response: %s", e.what());
400 }
401
402 return false;
403}
404
405bool WeatherFlow::makeAPIRequest(const std::string &endpoint, std::string &response)
406{
407 return retryRequest([this, &endpoint, &response]() -> bool
408 {
409 try
410 {
411 httplib::Client cli(API_HOST, 80);
412 cli.set_connection_timeout(static_cast<int>(wfSettingsNP[WF_CONNECTION_TIMEOUT].getValue()));
413 cli.set_read_timeout(static_cast<int>(wfSettingsNP[WF_CONNECTION_TIMEOUT].getValue()));
414
415 // Set headers
416 httplib::Headers headers;
417 headers.emplace("Authorization", "Bearer " + std::string(wfAPIKeyTP[0].getText()));
418 headers.emplace("Content-Type", "application/json");
419 headers.emplace("User-Agent", "INDI-WeatherFlow/1.0");
420
421 auto result = cli.Get(endpoint, headers);
422
423 if (!result)
424 {
425 LOGF_ERROR("HTTP request failed: %s",
426 result.error() != httplib::Error::Success ? httplib::to_string(result.error()).c_str() : "Unknown error");
427 return false;
428 }
429
430 if (result->status != 200)
431 {
432 LOGF_ERROR("HTTP error: %d", result->status);
433 return false;
434 }
435
436 response = result->body;
437
438 // Update rate limiting
439 m_requestCount++;
440 m_lastRequestTime = std::chrono::system_clock::now();
441
442 return true;
443 }
444 catch (const std::exception &e)
445 {
446 LOGF_ERROR("Request error: %s", e.what());
447 return false;
448 }
449 });
450}
451
452bool WeatherFlow::retryRequest(const std::function < bool() > &request)
453{
454 int maxRetries = static_cast < int > (wfSettingsNP[WF_RETRY_ATTEMPTS].getValue());
455
456 for (int attempt = 0; attempt <= maxRetries; ++attempt)
457 {
458 if (request())
459 {
460 return true;
461 }
462
463 if (attempt < maxRetries)
464 {
465 int delaySeconds = (1 << attempt); // Exponential backoff: 1, 2, 4, 8, 16...
466 LOGF_DEBUG("Request failed, retrying in %d seconds (attempt %d/%d)...",
467 delaySeconds, attempt + 1, maxRetries + 1);
468 std::this_thread::sleep_for(std::chrono::seconds(delaySeconds));
469 }
470 }
471
472 LOG_ERROR("All retry attempts failed.");
473 return false;
474}
hid_device * device
INDI::PropertyText getText(const char *name) const
virtual bool saveConfig(bool silent=false, const char *property=nullptr)
Save the current properties in a configuration file.
void setVersion(uint16_t vMajor, uint16_t vMinor)
Set driver version information to be defined in DRIVER_INFO property as vMajor.vMinor.
void defineProperty(INumberVectorProperty *property)
void addDebugControl()
Add Debug control to the driver.
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool load()
load Attempt to load property values from configuration file.
void save(FILE *f) const
save Save property to configuration file.
bool isNameMatch(const char *otherName) const
bool update(const double values[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
bool update(const char *const texts[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
INDI::PropertyViewText * getText() const
void addParameter(std::string name, std::string label, double numMinOk, double numMaxOk, double percWarning, bool flipWarning=false)
Add a physical weather measurable parameter to the weather driver.
bool setCriticalParameter(std::string name)
setCriticalParameter Set parameter that is considered critical to the operation of the observatory....
void setParameterValue(std::string name, double value)
setParameterValue Update weather parameter value
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
virtual void ISGetProperties(const char *dev) override
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
void setWeatherConnection(const uint8_t &value)
setWeatherConnection Set Weather connection mode. Child class should call this in the constructor bef...
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
virtual void ISGetProperties(const char *dev) override
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
virtual ~WeatherFlow()
virtual IPState updateWeather() override
updateWeather Update weather conditions from device or service. The function should not change the st...
bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool updateLocation(double latitude, double longitude, double elevation) override
Update weather station location.
const char * getDefaultName() override
bool Disconnect() override
Disconnect from device.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
a class to store JSON values
bool contains(const typename object_t::key_type &key) const
check the existence of an element in a JSON object
auto get() const noexcept(noexcept(std::declval< const basic_json_t & >().template get_impl< ValueType >(detail::priority_tag< 4 > {}))) -> decltype(std::declval< const basic_json_t & >().template get_impl< ValueType >(detail::priority_tag< 4 > {}))
get a (pointer) value (explicit)
general exception of the basic_json class
const char * what() const noexcept override
returns the explanatory string
const char * OPTIONS_TAB
OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls,...
@ IP_RW
Definition indiapi.h:184
IPState
Property state.
Definition indiapi.h:158
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
#define INDI_UNUSED(x)
Definition indidevapi.h:132
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOGF_DEBUG(fmt,...)
Definition indilogger.h:83
#define LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
Definition indilogger.h:72
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
#define LOG_INFO(txt)
Definition indilogger.h:74
#define getDeviceName
std::string to_string(Error error)
Definition httplib.h:9275
std::unordered_multimap< std::string, std::string, detail::case_ignore::hash, detail::case_ignore::equal_to > Headers
Definition httplib.h:607
std::unique_ptr< WeatherFlow > weatherFlow(new WeatherFlow())