Instrument Neutral Distributed Interface INDI 2.2.5
weather_safety_proxy.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2019 Hans Lambermont. All rights reserved.
3
4 INDI Weather Safety Proxy
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 <memory>
26#include <unistd.h>
27#include <sys/wait.h>
28#include <cstring>
29#include <curl/curl.h>
30
31#ifdef _USE_SYSTEM_JSONLIB
32#include <nlohmann/json.hpp>
33#else
34#include <indijson.hpp>
35#endif
36
38
40
41std::unique_ptr<WeatherSafetyProxy> weatherSafetyProxy(new WeatherSafetyProxy());
42
43static size_t WSP_WriteCallback(void *contents, size_t size, size_t nmemb, void *userp)
44{
45 ((std::string *)userp)->append((char *)contents, size * nmemb);
46 return size * nmemb;
47}
48
55
57
59{
60 return "Weather Safety Proxy";
61}
62
64{
65 return true;
66}
67
69{
70 return true;
71}
72
74{
76
77 keywordTP[0].fill("WEATHER_CONDITION", "Weather Condition", "condition");
78 keywordTP.fill(getDeviceName(), "KEYWORD", "Keywords", OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
79
80 ScriptsTP[WSP_SCRIPT].fill("WEATHER_SAFETY_SCRIPT", "Weather safety script",
81 "/usr/local/share/indi/scripts/weather_status.py");
82 ScriptsTP.fill(getDefaultName(), "WEATHER_SAFETY_SCRIPTS", "Script", OPTIONS_TAB,
83 IP_RW, 100, IPS_IDLE);
84
85 UrlTP[WSP_URL].fill("WEATHER_SAFETY_URL", "Weather safety URL", "http://0.0.0.0:5000/weather/safety");
86 UrlTP.fill(getDefaultName(), "WEATHER_SAFETY_URLS", "Url", OPTIONS_TAB, IP_RW, 100,
87 IPS_IDLE);
88
89 ScriptOrCurlSP[WSP_USE_SCRIPT].fill("Use script", "", ISS_ON);
90 ScriptOrCurlSP[WSP_USE_CURL].fill("Use url", "", ISS_OFF);
91 ScriptOrCurlSP.fill(getDeviceName(), "SCRIPT_OR_CURL", "Script or url",
93
94 softErrorHysteresisNP[WSP_SOFT_ERROR_MAX].fill("SOFT_ERROR_MAX", "Max soft errors", "%g", 0.0, 1000.0, 1.0, 30.0);
95 softErrorHysteresisNP[WSP_SOFT_ERROR_RECOVERY].fill("SOFT_ERROR_RECOVERY", "Minimum soft error for recovery", "%g",
96 0.0, 1000.0, 1.0, 7.0);
97 softErrorHysteresisNP.fill(getDeviceName(), "SOFT_ERROR_HYSTERESIS",
98 "Soft error hysterese", OPTIONS_TAB, IP_RW, 0, IPS_IDLE);
99
100 addParameter("WEATHER_SAFETY", "Weather Safety", 0.9, 1.1, 0); // 0 is unsafe, 1 is safe
101 setCriticalParameter("WEATHER_SAFETY");
102
103 reasonsTP[0].fill("Reasons", "", nullptr);
104 reasonsTP.fill(getDeviceName(), "WEATHER_SAFETY_REASONS", "Weather Safety Reasons",
106
108
109 return true;
110}
111
113{
115
116 if (isConnected())
117 {
118 defineProperty(reasonsTP);
119 }
120 else
121 {
122 deleteProperty(reasonsTP);
123 }
124
125 return true;
126}
127
129{
131 ScriptsTP.save(fp);
132 UrlTP.save(fp);
133 ScriptOrCurlSP.save(fp);
134 softErrorHysteresisNP.save(fp);
135 return true;
136}
137
139{
141 static bool once = true;
142 if (once)
143 {
144 once = false;
145 defineProperty(ScriptsTP);
146 defineProperty(UrlTP);
147 defineProperty(ScriptOrCurlSP);
148 defineProperty(softErrorHysteresisNP);
149 loadConfig(false, "WEATHER_SAFETY_SCRIPTS");
150 loadConfig(false, "WEATHER_SAFETY_URLS");
151 loadConfig(false, "SCRIPT_OR_CURL");
152 loadConfig(false, "SOFT_ERROR_HYSTERESIS");
153 }
154}
155
156bool WeatherSafetyProxy::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
157{
158 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
159 {
160 if (softErrorHysteresisNP.isNameMatch(name))
161 {
162 softErrorHysteresisNP.update(values, names, n);
163 softErrorHysteresisNP.setState(IPS_OK);
164 softErrorHysteresisNP.apply();
165 return true;
166 }
167 }
168 return INDI::Weather::ISNewNumber(dev, name, values, names, n);
169}
170
171bool WeatherSafetyProxy::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
172{
173 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
174 {
175 if (keywordTP.isNameMatch(name))
176 {
177 keywordTP.setState(IPS_OK);
178 keywordTP.update(texts, names, n);
179 // update client display
180 keywordTP.apply();
181 return true;
182 }
183 if (ScriptsTP.isNameMatch(name))
184 {
185 ScriptsTP.setState(IPS_OK);
186 ScriptsTP.update(texts, names, n);
187 // update client display
188 ScriptsTP.apply();
189 return true;
190 }
191 if (UrlTP.isNameMatch(name))
192 {
193 UrlTP.setState(IPS_OK);
194 UrlTP.update(texts, names, n);
195 // update client display
196 UrlTP.apply( );
197 return true;
198 }
199 }
200
201 return INDI::Weather::ISNewText(dev, name, texts, names, n);
202}
203
204bool WeatherSafetyProxy::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
205{
206 if (!strcmp(getDeviceName(), dev))
207 {
208 if (ScriptOrCurlSP.isNameMatch(name))
209 {
210 LOG_DEBUG("WeatherSafetyProxy::ISNewSwitch");
211 ScriptOrCurlSP.update(states, names, n);
212 ScriptOrCurlSP.setState(IPS_OK);
213 ScriptOrCurlSP.apply();
214 return true;
215 }
216 }
217
218 return INDI::Weather::ISNewSwitch(dev, name, states, names, n);
219}
220
221// Called by Weather::TimerHit every UpdatePeriodN[0].value seconds if we return IPS_OK or every 5 seconds otherwise
223{
224 IPState ret = IPS_ALERT;
225 if (ScriptOrCurlSP[WSP_USE_SCRIPT].getState() == ISS_ON)
226 {
227 ret = executeScript();
228 }
229 else
230 {
231 ret = executeCurl();
232 }
233 if (ret != IPS_OK)
234 {
235 if (Safety == WSP_SAFE)
236 {
237 SofterrorCount++;
238 LOGF_WARN("Soft error %d occurred during SAFE conditions, counting", SofterrorCount);
239 if (SofterrorCount > softErrorHysteresisNP[WSP_SOFT_ERROR_MAX].getValue())
240 {
241 char Warning[] = "Max softerrors reached while Weather was SAFE";
242 LOG_WARN(Warning);
243 Safety = WSP_UNSAFE;
244 setParameterValue("WEATHER_SAFETY", WSP_UNSAFE);
245 reasonsTP[0].setText(Warning);
246 reasonsTP.setState(IPS_OK);
247 reasonsTP.apply();
248 SofterrorRecoveryMode = true;
249 ret = IPS_OK; // So that indiweather actually syncs the CriticalParameters we just set
250 }
251 }
252 else
253 {
254 LOG_WARN("Soft error occurred during UNSAFE conditions, ignore");
255 SofterrorCount = 0;
256 SofterrorRecoveryCount = 0;
257 }
258 }
259 else
260 {
261 SofterrorCount = 0;
262 }
263 return ret;
264}
265
266IPState WeatherSafetyProxy::executeScript()
267{
268 const char *cmd = ScriptsTP[WSP_SCRIPT].getText();
269
270 if (access(cmd, F_OK | X_OK) == -1)
271 {
272 LOGF_ERROR("Cannot use script [%s], check its existence and permissions", cmd);
273 LastParseSuccess = false;
274 return IPS_ALERT;
275 }
276
277 LOGF_DEBUG("Run script: %s", cmd);
278 FILE *handle = popen(cmd, "r");
279 if (handle == nullptr)
280 {
281 LOGF_ERROR("Failed to run script [%s]", strerror(errno));
282 LastParseSuccess = false;
283 return IPS_ALERT;
284 }
285 char buf[BUFSIZ];
286 size_t byte_count = fread(buf, 1, BUFSIZ - 1, handle);
287 pclose(handle);
288 buf[byte_count] = 0;
289 if (byte_count == 0)
290 {
291 LOGF_ERROR("Got no output from script [%s]", cmd);
292 LastParseSuccess = false;
293 return IPS_ALERT;
294 }
295 LOGF_DEBUG("Read %d bytes output [%s]", byte_count, buf);
296
297 return parseSafetyJSON(buf);
298}
299
300IPState WeatherSafetyProxy::executeCurl()
301{
302 CURL *curl_handle;
303 CURLcode res;
304 std::string readBuffer;
305
306 curl_handle = curl_easy_init();
307 if (curl_handle)
308 {
309 curl_easy_setopt(curl_handle, CURLOPT_URL, UrlTP[WSP_URL].getText());
310 curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WSP_WriteCallback);
311 curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &readBuffer);
312 curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0");
313 LOGF_DEBUG("Call curl %s", UrlTP[WSP_URL].getText());
314 res = curl_easy_perform(curl_handle);
315 if (res != CURLE_OK)
316 {
317 LOGF_ERROR("curl_easy_perform failed with [%s]", curl_easy_strerror(res));
318 return IPS_ALERT;
319 }
320 curl_easy_cleanup(curl_handle);
321 LOGF_DEBUG("Read %d bytes output [%s]", readBuffer.size(), readBuffer.c_str());
322 return parseSafetyJSON(readBuffer);
323 }
324 else
325 {
326 LOG_ERROR("curl_easy_init failed");
327 return IPS_ALERT;
328 }
329}
330
331IPState WeatherSafetyProxy::parseSafetyJSON(const std::string &buffer)
332{
333 json report = json::parse(buffer);
334 try
335 {
336 int NewSafety = 0;
337 report["roof_status"]["open_ok"].get_to(NewSafety);
338
339 if (NewSafety != Safety)
340 {
341 if (NewSafety == WSP_UNSAFE)
342 {
343 LOG_WARN("Weather is UNSAFE");
344 }
345 else if (NewSafety == WSP_SAFE)
346 {
347 if (SofterrorRecoveryMode == true)
348 {
349 SofterrorRecoveryCount++;
350 if (SofterrorRecoveryCount > softErrorHysteresisNP[WSP_SOFT_ERROR_RECOVERY].getValue())
351 {
352 LOG_INFO("Minimum soft recovery errors reached while Weather was SAFE");
353 SofterrorRecoveryCount = 0;
354 SofterrorRecoveryMode = false;
355 }
356 else
357 {
358 LOGF_INFO("Weather is SAFE but soft error recovery %d is still counting", SofterrorRecoveryCount);
359 NewSafety = WSP_UNSAFE;
360 }
361 }
362 else
363 {
364 LOG_INFO("Weather is SAFE");
365 }
366 }
367 Safety = NewSafety;
368 }
369 setParameterValue("WEATHER_SAFETY", NewSafety);
370
371 // Optional reasons?
372 try
373 {
374 std::string reasons;
375 report["roof_status"]["reasons"].get_to(reasons);
376 if (SofterrorRecoveryMode == true)
377 {
378 char newReasons[MAXRBUF];
379 snprintf(newReasons, MAXRBUF, "SofterrorRecoveryMode, %s", reasons.c_str());
380 reasonsTP[0].setText(newReasons);
381 }
382 else
383 {
384 reasonsTP[0].setText(reasons.c_str());
385 }
386 reasonsTP.setState(IPS_OK);
387 reasonsTP.apply();
388 }
389 catch (json::exception &e) {}
390 }
391 catch (json::exception &e)
392 {
393 // output exception information
394 LOGF_ERROR("Error parsing weather report %s id: %d", e.what(), e.id);
395 return IPS_ALERT;
396 }
397
398 return IPS_OK;
399}
bool isConnected() const
INDI::PropertyText getText(const char *name) const
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.
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
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
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 ISState states[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, 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 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.
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch 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 saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
const char * getDefaultName() override
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....
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
bool Disconnect() override
Disconnect from device.
a class to store JSON values
ValueType & get_to(ValueType &v) const noexcept(noexcept(JSONSerializer< ValueType >::from_json(std::declval< const basic_json_t & >(), v)))
get a 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 * OPTIONS_TAB
OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls,...
int errno
ISState
Switch state.
Definition indiapi.h:148
@ ISS_OFF
Definition indiapi.h:149
@ ISS_ON
Definition indiapi.h:150
@ IP_RW
Definition indiapi.h:184
@ IP_RO
Definition indiapi.h:182
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
@ ISR_1OFMANY
Definition indiapi.h:171
#define MAXRBUF
Definition indidriver.c:52
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOG_DEBUG(txt)
Definition indilogger.h:75
#define LOGF_WARN(fmt,...)
Definition indilogger.h:81
#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
basic_json<> json
default specialization
uint8_t cmd[4]
Definition pwc-ioctl.h:2
std::unique_ptr< WeatherSafetyProxy > weatherSafetyProxy(new WeatherSafetyProxy())
@ WSP_USE_SCRIPT
@ WSP_USE_CURL
@ WSP_SOFT_ERROR_MAX
@ WSP_SOFT_ERROR_RECOVERY