Instrument Neutral Distributed Interface INDI 2.2.5
weather_safety_alpaca.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2025 Jérémie Klein. All rights reserved.
3
4 ASCOM Alpaca Weather Safety INDI 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
26
27#include <httplib.h>
28#include <memory>
29#include <chrono>
30#include <thread>
31
32// We declare an auto pointer to WeatherSafetyAlpaca
33std::unique_ptr<WeatherSafetyAlpaca> weather_safety_alpaca(new WeatherSafetyAlpaca());
34
40
42{
43 return "Weather Safety Alpaca";
44}
45
47{
49
50 // Setup server address properties
51 ServerAddressTP[0].fill("HOST", "Host", ""); // Empty default to force configuration
52 ServerAddressTP[1].fill("PORT", "Port", "");
53 ServerAddressTP.fill(getDeviceName(), "SERVER_ADDRESS", "Server", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
54
55 // Setup device number property
56 DeviceNumberNP[0].fill("DEVICE_NUMBER", "Device Number", "%.0f", 0, 10, 1, 0);
57 DeviceNumberNP.fill(getDeviceName(), "DEVICE_NUMBER", "Alpaca Device", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
58
59 // Setup connection settings properties
60 ConnectionSettingsNP[0].fill("TIMEOUT", "Timeout (sec)", "%.0f", 1, 30, 1, 5);
61 ConnectionSettingsNP[1].fill("RETRIES", "Max Retries", "%.0f", 1, 10, 1, 3);
62 ConnectionSettingsNP[2].fill("RETRY_DELAY", "Retry Delay (ms)", "%.0f", 100, 5000, 100, 1000);
63 ConnectionSettingsNP.fill(getDeviceName(), "CONNECTION_SETTINGS", "Connection", SITE_TAB, IP_RW, 60, IPS_IDLE);
64
65 addParameter("WEATHER_SAFETY", "Weather Safety", 0, 1, 0); // min=0 (warning), max=1 (safe)
66 setCriticalParameter("WEATHER_SAFETY");
67
68 // Load config before setting any defaults
69 loadConfig(true);
70
72
73 return true;
74}
75
77{
79
80 // Always define these properties
81 defineProperty(ServerAddressTP);
82 defineProperty(DeviceNumberNP);
83 defineProperty(ConnectionSettingsNP);
84}
85
87{
88 if (ServerAddressTP[0].getText() == nullptr || ServerAddressTP[1].getText() == nullptr)
89 {
90 LOG_ERROR("Server address or port is not set.");
91 return false;
92 }
93
94 // Test connection by getting safety monitor status
95 nlohmann::json response;
96 bool success = retryRequest([this, &response]() {
97 std::string path = "/api/v1/safetymonitor/" + std::to_string(static_cast<int>(DeviceNumberNP[0].getValue())) + "/issafe";
98 return makeAlpacaRequest(path, response);
99 });
100
101 if (!success)
102 {
103 LOG_ERROR("Failed to connect to Alpaca safety monitor. Please check server address and port.");
104 return false;
105 }
106
107 LOG_INFO("Successfully connected to Alpaca safety monitor.");
109 return true;
110}
111
113{
114 LOG_INFO("Disconnected from Alpaca safety monitor.");
115 return true;
116}
117
124
126{
127 LOGF_DEBUG("Updating weather status for device %d",
128 static_cast<int>(DeviceNumberNP[0].getValue()));
129
130 nlohmann::json response;
131 bool success = retryRequest([this, &response]() {
132 std::string path = "/api/v1/safetymonitor/" + std::to_string(static_cast<int>(DeviceNumberNP[0].getValue())) + "/issafe";
133 LOGF_DEBUG("Requesting safety status with path: %s", path.c_str());
134 return makeAlpacaRequest(path, response);
135 });
136
137 if (!success)
138 {
139 LOG_DEBUG("Safety status request failed");
140 LastParseSuccess = false;
141 return IPS_ALERT;
142 }
143
144 try
145 {
146 LOGF_DEBUG("Full Alpaca response: %s", response.dump(2).c_str());
147
148 bool isSafe = response["Value"].get<bool>();
149 LOGF_DEBUG("Parsed safety status: %s", isSafe ? "SAFE" : "UNSAFE");
150
151 setParameterValue("WEATHER_SAFETY", isSafe ? 0 : 2); // 0 for safe, 1 for warning, 2 for danger
152
153 LastParseSuccess = true;
154 return IPS_OK;
155 }
157 {
158 LOGF_ERROR("JSON parsing error: %s (id: %d)", e.what(), e.id);
159 LastParseSuccess = false;
160 return IPS_ALERT;
161 }
162}
163
164bool WeatherSafetyAlpaca::makeAlpacaRequest(const std::string& path, nlohmann::json& response, bool isPut)
165{
166 if (ServerAddressTP[0].getText() == nullptr || ServerAddressTP[1].getText() == nullptr)
167 {
168 LOG_ERROR("Server address or port is not set.");
169 return false;
170 }
171
172 try
173 {
174 // Log connection details
175 LOGF_DEBUG("Creating HTTP client for host: %s, port: %s",
176 ServerAddressTP[0].getText(),
177 ServerAddressTP[1].getText());
178
179 httplib::Client cli(ServerAddressTP[0].getText(), std::stoi(ServerAddressTP[1].getText()));
180
181 // Log timeout settings
182 LOGF_DEBUG("Setting timeouts - Connection: %d sec, Read: %d sec",
183 static_cast<int>(ConnectionSettingsNP[0].getValue()),
184 static_cast<int>(ConnectionSettingsNP[0].getValue()));
185
186 cli.set_connection_timeout(static_cast<int>(ConnectionSettingsNP[0].getValue()));
187 cli.set_read_timeout(static_cast<int>(ConnectionSettingsNP[0].getValue()));
188
189 // Log request details
190 LOGF_DEBUG("Making %s request to path: %s",
191 isPut ? "PUT" : "GET",
192 path.c_str());
193
194 auto result = isPut ? cli.Put(path.c_str()) : cli.Get(path.c_str());
195
196 if (!result)
197 {
198 LOGF_ERROR("HTTP request failed: %s",
199 result.error() != httplib::Error::Success ? httplib::to_string(result.error()).c_str() : "Unknown error");
200 return false;
201 }
202
203 // Log response status and headers
204 LOGF_DEBUG("HTTP Status: %d", result->status);
205 for (const auto& header : result->headers)
206 {
207 LOGF_DEBUG("Response Header - %s: %s",
208 header.first.c_str(),
209 header.second.c_str());
210 }
211
212 // Log response body
213 LOGF_DEBUG("Response Body: %s", result->body.c_str());
214
215 if (result->status != 200)
216 {
217 LOGF_ERROR("HTTP error: %d", result->status);
218 return false;
219 }
220
221 response = nlohmann::json::parse(result->body);
222
223 // Log parsed JSON response
224 LOGF_DEBUG("Parsed JSON response: %s", response.dump(2).c_str());
225
226 if (response["ErrorNumber"].get<int>() != 0)
227 {
228 LOGF_ERROR("Alpaca error: %s (ErrorNumber: %d)",
229 response["ErrorMessage"].get<std::string>().c_str(),
230 response["ErrorNumber"].get<int>());
231 return false;
232 }
233
234 return true;
235 }
236 catch(const std::exception &e)
237 {
238 LOGF_ERROR("Request error: %s", e.what());
239 return false;
240 }
241}
242
243bool WeatherSafetyAlpaca::retryRequest(const std::function<bool()>& request)
244{
245 int maxRetries = static_cast<int>(ConnectionSettingsNP[1].getValue());
246 int retryDelay = static_cast<int>(ConnectionSettingsNP[2].getValue());
247
248 for (int attempt = 1; attempt <= maxRetries; attempt++)
249 {
250 try
251 {
252 if (request())
253 return true;
254 }
255 catch(const std::exception &e)
256 {
257 LOGF_ERROR("Request attempt %d failed: %s", attempt, e.what());
258 }
259
260 if (attempt < maxRetries)
261 {
262 LOGF_INFO("Retrying request in %d ms (attempt %d/%d)",
263 retryDelay, attempt, maxRetries);
264 std::this_thread::sleep_for(std::chrono::milliseconds(retryDelay));
265 }
266 }
267 return false;
268}
269
270bool WeatherSafetyAlpaca::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
271{
272 if (isDeviceNameMatch(dev) && ServerAddressTP.isNameMatch(name))
273 {
274 ServerAddressTP.update(texts, names, n);
275 ServerAddressTP.setState(IPS_OK);
276 ServerAddressTP.apply();
277 saveConfig();
278 return true;
279 }
280
281 return INDI::Weather::ISNewText(dev, name, texts, names, n);
282}
283
284bool WeatherSafetyAlpaca::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
285{
286 if (isDeviceNameMatch(dev))
287 {
288 if (ConnectionSettingsNP.isNameMatch(name))
289 {
290 ConnectionSettingsNP.update(values, names, n);
291 ConnectionSettingsNP.setState(IPS_OK);
292 ConnectionSettingsNP.apply();
293 saveConfig();
294 return true;
295 }
296 else if (DeviceNumberNP.isNameMatch(name))
297 {
298 if (isConnected())
299 {
300 LOG_WARN("Cannot change device number while connected.");
301 return false;
302 }
303
304 DeviceNumberNP.update(values, names, n);
305 DeviceNumberNP.setState(IPS_OK);
306 DeviceNumberNP.apply();
307 saveConfig();
308 return true;
309 }
310 }
311
312 return INDI::Weather::ISNewNumber(dev, name, values, names, n);
313}
314
316{
318
319 ServerAddressTP.save(fp);
320 DeviceNumberNP.save(fp);
321 ConnectionSettingsNP.save(fp);
322
323 return true;
324}
325
326bool WeatherSafetyAlpaca::loadConfig(bool silent, const char *property)
327{
328 bool result = INDI::Weather::loadConfig(silent, property);
329
330 if (property == nullptr)
331 {
332 result &= ServerAddressTP.load();
333 result &= DeviceNumberNP.load();
334 result &= ConnectionSettingsNP.load();
335 }
336
337 return result;
338}
bool isConnected() const
INDI::PropertyText getText(const char *name) const
bool isDeviceNameMatch(const char *otherName) const
Check that the device name matches the argument.
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.
virtual bool loadConfig(bool silent=false, const char *property=nullptr)
Load the last saved configuration file.
void defineProperty(INumberVectorProperty *property)
uint32_t getCurrentPollingPeriod() const
getCurrentPollingPeriod Return the current polling period.
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
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)
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 updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
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.
bool Disconnect() override
Disconnect from device.
virtual IPState updateWeather() override
updateWeather Update weather conditions from device or service. The function should not change the st...
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
virtual bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
const char * getDefaultName() override
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
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 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 loadConfig(bool silent=false, const char *property=nullptr) override
Load the last saved configuration file.
a class to store JSON values
string_t dump(const int indent=-1, const char indent_char=' ', const bool ensure_ascii=false, const error_handler_t error_handler=error_handler_t::strict) const
serialization
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 int id
the id of the exception
const char * what() const noexcept override
returns the explanatory string
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
const char * SITE_TAB
SITE_TAB Where all site information setting are located.
@ 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 LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOG_DEBUG(txt)
Definition indilogger.h:75
#define LOG_WARN(txt)
Definition indilogger.h:73
#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::unique_ptr< WeatherSafetyAlpaca > weather_safety_alpaca(new WeatherSafetyAlpaca())