Torc  0.1
torccentral.cpp
Go to the documentation of this file.
1 /* Class TorcCentral
2 *
3 * Copyright (C) Mark Kendall 2015-18
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 */
19 
20 // Qt
21 #include <QFile>
22 #include <QFileInfo>
23 #include <QProcess>
24 #include <QJsonDocument>
25 
26 // Torc
27 #include "torcexitcodes.h"
28 #include "torccoreutils.h"
29 #include "torcevent.h"
30 #include "torclogging.h"
31 #include "torcdirectories.h"
32 #include "torclanguage.h"
33 #include "torclocalcontext.h"
34 #include "torc/torcadminthread.h"
35 #include "inputs/torcinputs.h"
36 #include "outputs/torcoutputs.h"
37 #include "controls/torccontrols.h"
38 #include "notify/torcnotify.h"
39 #include "torcxmlreader.h"
40 #include "torccentral.h"
41 
42 #ifdef USING_XMLPATTERNS
43 #include "torcxmlvalidator.h"
44 #elif USING_LIBXML2
45 #include "torclibxmlvalidator.h"
46 #endif
47 
48 #ifdef USING_GRAPHVIZ_LIBS
49 #include <graphviz/gvc.h>
50 #endif
51 
52 // for system
53 #include <stdlib.h>
54 
55 // default to metric temperature measurements
57 
59 {
60  return gTemperatureUnits;
61 }
62 
64  : QObject(),
65  TorcHTTPService(this, QStringLiteral("central"), QStringLiteral("central"), TorcCentral::staticMetaObject, QStringLiteral("")),
66  m_config(QVariantMap()),
67  m_graph(),
68  temperatureUnits(QStringLiteral("celsius"))
69 {
70  // reset state graph and clear out old files
71  // content directory should already have been created by TorcHTMLDynamicContent
72  QString graphdot = QStringLiteral("%1stategraph.dot").arg(GetTorcContentDir());
73  QString graphsvg = QStringLiteral("%1stategraph.svg").arg(GetTorcContentDir());
74  QString config = QStringLiteral("%1%2").arg(GetTorcContentDir(), TORC_CONFIG_FILE);
75  QString current = QStringLiteral("%1/%2").arg(GetTorcConfigDir(), TORC_CONFIG_FILE);
76 
77  if (QFile::exists(graphdot))
78  QFile::remove(graphdot);
79  if (QFile::exists(graphsvg))
80  QFile::remove(graphsvg);
81  if (QFile::exists(config))
82  QFile::remove(config);
83 
84  QFile currentconfig(current);
85  if (!currentconfig.copy(config))
86  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Failed to copy current config file to content directory"));
87 
88  // listen for interesting events
90 
91  if (LoadConfig())
92  {
93  TemperatureUnits temperatureunits = Celsius; // default to metric
94 
95  // handle settings now
96  if (m_config.contains(QStringLiteral("settings")))
97  {
98  QVariantMap settings = m_config.value(QStringLiteral("settings")).toMap();
99 
100  // applicationname
101  if (settings.contains(QStringLiteral("applicationname")))
102  {
103  QString name = settings.value(QStringLiteral("applicationname")).toString().trimmed();
104  if (!name.isEmpty())
105  {
106  QCoreApplication::setApplicationName(name);
107  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Changed application name to '%1'").arg(name));
108  }
109  }
110 
111  // temperature units - metric or imperial...
112  if (settings.contains(QStringLiteral("temperatureunits")))
113  {
114  QString units = settings.value(QStringLiteral("temperatureunits")).toString().trimmed().toLower();
115  if (units == QStringLiteral("metric") || units == QStringLiteral("celsius"))
116  temperatureunits = Celsius;
117  else if (units == QStringLiteral("imperial") || units == QStringLiteral("fahrenheit"))
118  temperatureunits = Fahrenheit;
119  else
120  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Unknown temperature units - defaulting to metric (Celsius)"));
121  }
122  }
123 
124  TorcCentral::gTemperatureUnits = temperatureunits;
125  temperatureUnits = TorcCoreUtils::EnumToLowerString<TorcCentral::TemperatureUnits>(temperatureunits);
126  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Using '%1' temperature units").arg(temperatureUnits));
127 
128  // create the devices
129  TorcDeviceHandler::Start(m_config);
130 
131  // connect controls to sensors/outputs/other controls
132  TorcControls::gControls->Validate();
133 
134  // setup notifications/notifiers
135  (void)TorcNotify::gNotify->Validate();
136 
137  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Initialising state machine"));
138  // initialise the state machine
139  {
140  QMutexLocker lock(TorcDevice::gDeviceListLock);
141 
142  QHash<QString,TorcDevice*>::const_iterator it = TorcDevice::gDeviceList->constBegin();
143  for( ; it != TorcDevice::gDeviceList->constEnd(); ++it)
144  it.value()->Start();
145  }
146 
147  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Notifying start"));
149 
150  // iff we have got this far, then create the graph
151  // start the graph
152  m_graph.clear();
153  m_graph.append(QStringLiteral("strict digraph \"%1\" {\r\n"
154  " rankdir=\"LR\";\r\n"
155  " node [shape=rect];\r\n")
156  .arg(TORC_TORC));
157 
158  // build the graph contents
159  TorcInputs::gInputs->Graph(&m_graph);
160  TorcOutputs::gOutputs->Graph(&m_graph);
161  TorcControls::gControls->Graph(&m_graph);
162  TorcNotify::gNotify->Graph(&m_graph);
163 
164  // complete the graph
165  m_graph.append("}\r\n");
166 
167  QFile file(graphdot);
168  if (file.open(QIODevice::ReadWrite))
169  {
170  file.write(m_graph);
171  file.flush();
172  file.close();
173  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Saved state graph as %1").arg(graphdot));
174  }
175  else
176  {
177  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to open '%1' to write state graph").arg(graphdot));
178  }
179 
180  // create a representation of the state graph
181 #ifdef USING_GRAPHVIZ_LIBS
182  bool created = false;
183 
184  FILE *handle = fopen(graphsvg.toLocal8Bit().data(), "w");
185  if (handle)
186  {
187  GVC_t *gvc = gvContext();
188  Agraph_t *g = agmemread(m_graph.data());
189  gvLayout(gvc, g, "dot");
190  gvRender(gvc, g, "svg", handle);
191  gvFreeLayout(gvc,g);
192  agclose(g);
193  gvFreeContext(gvc);
194  fclose(handle);
195  created = true;
196  }
197  else
198  {
199  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Failed to open '%1' for writing (err: %2)").arg(graphsvg, strerror(errno)));
200  }
201 
202  if (!created)
203  {
204 #endif
205  // NB QProcess appears to be fatally broken. Just use system instead
206  QString command = QStringLiteral("dot -Tsvg -o %1 %2").arg(graphsvg, graphdot);
207  int err = system(command.toLocal8Bit());
208  if (err < 0)
209  {
210  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to create stategraph representation (err: %1)").arg(strerror(errno)));
211  }
212  else
213  {
214  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Saved state graph representation as %1").arg(graphsvg));
215  if (err > 0)
216  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("dot returned an unexpected result - stategraph may be incomplete or absent"));
217  }
218  }
219 #ifdef USING_GRAPHVIZ_LIBS
220  }
221 #endif
222  // no need for the graph data from here
223  m_graph.clear();
224 }
225 
227 {
228  // deregister for events
230 
231  // cleanup any devices
233 }
234 
236 {
237  return tr("Central");
238 }
239 
241 {
242  return temperatureUnits;
243 }
244 
245 void TorcCentral::SubscriberDeleted(QObject *Subscriber)
246 {
248 }
249 
250 bool TorcCentral::LoadConfig(void)
251 {
252  bool skipvalidation = false;
253 
254  if (!qEnvironmentVariableIsEmpty("TORC_NO_VALIDATION"))
255  {
256  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Skipping configuration file validation (command line)."));
257  skipvalidation = true;
258  }
259 
260  QString xml = GetTorcConfigDir() + "/" + TORC_CONFIG_FILE;
261  QFileInfo config(xml);
262  if (!skipvalidation && !config.exists())
263  {
264  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to find configuration file '%1'").arg(xml));
265  return false;
266  }
267 
268 #if defined(USING_XMLPATTERNS) || defined(USING_LIBXML2)
269  QString customxsd = GetTorcContentDir() + "torc.xsd";
270  // we always want to delete the old xsd - if it isn't present, it wasn't used!
271  // so retrieve now and then delete
272  QByteArray oldxsd;
273  if (QFile::exists(customxsd))
274  {
275  QFile customxsdfile(customxsd);
276  if (customxsdfile.open(QIODevice::ReadOnly))
277  {
278  oldxsd = customxsdfile.readAll();
279  customxsdfile.close();
280  }
281  QFile::remove(customxsd);
282  }
283 
284  QString basexsd = GetTorcShareDir() + "/html/torc.xsd";
285  if (!QFile::exists(basexsd))
286  {
287  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to find base XSD file '%1'").arg(basexsd));
288  return false;
289  }
290 
291  QByteArray newxsd;
292  if (!skipvalidation)
293  {
294  // customise and save the xsd now - even if validation is skipped from here, we need the
295  // xsd for reference
296  newxsd = GetCustomisedXSD(basexsd);
297  if (!newxsd.isEmpty())
298  {
299  // save the XSD for the user to inspect if necessary
300  QFile customxsdfile(customxsd);
301  if (customxsdfile.open(QIODevice::ReadWrite))
302  {
303  customxsdfile.write(newxsd);
304  customxsdfile.flush();
305  customxsdfile.close();
306  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Saved current XSD as '%1'").arg(customxsd));
307  }
308  else
309  {
310  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Failed to open '%1' for writing").arg(customxsd));
311  }
312  }
313  else
314  {
315  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Strange - empty xsd..."));
316  return false;
317  }
318  }
319 
320  // validation can take a while on slower machines (e.g. single core raspberry pi).
321  // try and skip if the config has not been modified
322  QString lastvalidated = gLocalContext->GetSetting(QStringLiteral("configLastValidated"), QStringLiteral("never"));
323  if (!skipvalidation && lastvalidated != QStringLiteral("never"))
324  {
325  bool xsdmodified = qstrcmp(oldxsd.constData(), newxsd.constData()) != 0;
326  bool configmodified = config.lastModified() >= QDateTime::fromString(lastvalidated);
327 
328  if (xsdmodified)
329  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("XSD file changed since last validation"));
330  else
331  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("XSD unchanged since last validation"));
332 
333  if (configmodified)
334  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Configuration file changed since last validation"));
335  else
336  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Configuration file unchanged since last validation:"));
337 
338  skipvalidation = !xsdmodified && !configmodified;
339  }
340 
341  if (!skipvalidation)
342  {
343  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Starting validation of configuration file"));
344  TorcXmlValidator validator(xml, newxsd);
345  if (!validator.Validated())
346  {
347  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Configuration file '%1' failed validation").arg(xml));
348  // make sure we re-validate
349  gLocalContext->SetSetting(QStringLiteral("configLastValidated"), QStringLiteral("never"));
350  return false;
351  }
352 
353  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Configuration successfully validated"));
354  gLocalContext->SetSetting(QStringLiteral("configLastValidated"), QDateTime::currentDateTime().toString());
355  }
356  else
357  {
358  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Skipping validation of configuration file"));
359  }
360 #else
361  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Xml validation unavailable - not validating configuration file."));
362 #endif
363 
364  TorcXMLReader reader(xml);
365  QString error;
366 
367  if (!reader.IsValid(error))
368  {
369  LOG(VB_GENERAL, LOG_ERR, error);
370  return false;
371  }
372 
373  QVariantMap result = reader.GetResult();
374 
375  // root object should be 'torc'
376  if (!result.contains(QStringLiteral("torc")))
377  {
378  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to find 'torc' root element in '%1'").arg(xml));
379  return false;
380  }
381 
382  m_config = result.value(QStringLiteral("torc")).toMap();
383 
384  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Loaded config from %1").arg(xml));
385  return true;
386 }
387 
388 QByteArray TorcCentral::GetCustomisedXSD(const QString &BaseXSDFile)
389 {
390  QByteArray result;
391  if (!QFile::exists(BaseXSDFile))
392  {
393  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Base XSD file '%1' does not exist").arg(BaseXSDFile));
394  return result;
395  }
396 
397  QFile xsd(BaseXSDFile);
398  if (!xsd.open(QIODevice::ReadOnly))
399  {
400  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Failed to open base XSD file '%1'").arg(BaseXSDFile));
401  return result;
402  }
403 
404  result = xsd.readAll();
405  xsd.close();
407  return result;
408 }
409 
411 bool TorcCentral::event(QEvent *Event)
412 {
413  TorcEvent* torcevent = dynamic_cast<TorcEvent*>(Event);
414  if (torcevent)
415  {
416  int event = torcevent->GetEvent();
417  switch (event)
418  {
419  case Torc::ShuttingDown:
420  case Torc::Suspending:
421  case Torc::Restarting:
422  case Torc::Hibernating:
423  // NB this just ensures outputs (e.g. PWM drivers) are set to sensible
424  // defaults if we shut down. There is currently no handling of waking, which
425  // would need more thought about resetting state etc.
426  LOG(VB_GENERAL, LOG_INFO, QStringLiteral("Resetting devices to defaults."));
427  {
428  QMutexLocker lock(TorcDevice::gDeviceListLock);
429 
430  QHash<QString,TorcDevice*>::const_iterator it = TorcDevice::gDeviceList->constBegin();
431  for( ; it != TorcDevice::gDeviceList->constEnd(); ++it)
432  it.value()->Stop();
433  }
434 
435  break;
436  default: break;
437  }
438  }
439 
440  return QObject::event(Event);
441 }
442 
445 {
446  Q_DECLARE_TR_FUNCTIONS(TorcCentralObject)
447 
448  public:
450  : TorcAdminObject(TORC_ADMIN_LOW_PRIORITY - 5), // start last
451  m_object(nullptr)
452  {
453  TorcCommandLine::RegisterEnvironmentVariable(QStringLiteral("TORC_NO_VALIDATION"), QStringLiteral("Disable validation of configuration file. This may speed up start times."));
454  }
455 
457  {
458  Destroy();
459  }
460 
461  void GetStrings(QVariantMap &Strings)
462  {
463  Strings.insert(QStringLiteral("CelsiusTr"), QCoreApplication::translate("TorcCentral", "Celsius"));
464  Strings.insert(QStringLiteral("CelsiusUnitsTr"), QCoreApplication::translate("TorcCentral", "°C"));
465  Strings.insert(QStringLiteral("FahrenheitTr"), QCoreApplication::translate("TorcCentral", "Fahrenheit"));
466  Strings.insert(QStringLiteral("FahrenheitUnitsTr"), QCoreApplication::translate("TorcCentral", "°F"));
467  }
468 
469  void Create(void)
470  {
471  Destroy();
472  m_object = new TorcCentral();
473  }
474 
475  void Destroy(void)
476  {
477  delete m_object;
478  m_object = nullptr;
479  }
480 
481  private:
482  Q_DISABLE_COPY(TorcCentralObject)
483  TorcCentral *m_object;
485 
487 
489  : nextTorcXSDFactory(gTorcXSDFactory)
490 {
491  gTorcXSDFactory = this;
492 }
493 
495 {
496  return gTorcXSDFactory;
497 }
498 
500 {
501  return nextTorcXSDFactory;
502 }
503 
506 void TorcXSDFactory::CustomiseXSD(QByteArray &XSD)
507 {
508  QStringList identifiers;
512 
513  QMultiMap<QString,QString> xsds;
515  for ( ; factory; factory = factory->NextFactory())
516  factory->GetXSD(xsds);
517 
518  foreach (const QString &ident, identifiers)
519  {
520  QString replacewith;
521  QMultiMap<QString,QString>::const_iterator it = xsds.constBegin();
522  for ( ; it != xsds.constEnd(); ++it)
523  if (it.key() == ident)
524  replacewith += it.value();
525  XSD.replace(ident, replacewith.toLatin1());
526  }
527 }
TorcXSDFactory * NextFactory(void) const
static QByteArray GetCustomisedXSD(const QString &BaseXSDFile)
void SubscriberDeleted(QObject *Subscriber)
#define XSD_CONTROLTYPES
Definition: torccentral.h:56
#define XSD_UNIQUE
Definition: torccentral.h:64
TorcLocalContext * gLocalContext
#define XSD_NOTIFIERTYPES
Definition: torccentral.h:60
A factory class for automatically running objects outside of the main loop.
int GetEvent(void)
Return the Torc action associated with this event.
Definition: torcevent.cpp:65
static void Stop(void)
#define XSD_CONTROLS
Definition: torccentral.h:57
QString GetTorcShareDir(void)
Return the path to the installed Torc shared resources.
void Graph(QByteArray *Data)
Definition: torcnotify.cpp:64
#define XSD_NOTIFIERS
Definition: torccentral.h:61
QString GetTemperatureUnits(void)
virtual void GetXSD(QMultiMap< QString, QString > &XSD)=0
static TemperatureUnits gTemperatureUnits
Definition: torccentral.h:42
Create the central controller object.
static TorcXSDFactory * GetTorcXSDFactory(void)
#define XSD_INPUTS
Definition: torccentral.h:55
#define XSD_NOTIFICATIONTYPES
Definition: torccentral.h:62
static void CustomiseXSD(QByteArray &XSD)
Customise the given base XSD with additional elements.
void SetSetting(const QString &Name, const QString &Value)
static TemperatureUnits GetGlobalTemperatureUnits(void)
Definition: torccentral.cpp:58
static void NotifyEvent(int Event)
static QMutex * gDeviceListLock
Definition: torcdevice.h:69
static QHash< QString, TorcDevice * > * gDeviceList
Definition: torcdevice.h:68
static void Start(const QVariantMap &Details)
#define TORC_ADMIN_LOW_PRIORITY
QVariantMap GetResult(void) const
bool IsValid(QString &Message) const
TorcCentralObject TorcCentralObject
TorcXSDFactory * nextTorcXSDFactory
Definition: torccentral.h:81
bool event(QEvent *Event) override
Handle Exit events.
#define XSD_NOTIFICATIONS
Definition: torccentral.h:63
static bool RegisterEnvironmentVariable(const QString &Var, const QString &Description)
Register an environment variable for display via the help option.
QString GetUIName(void) override
#define TORC_CONFIG_FILE
Definition: torclocaldefs.h:18
void HandleSubscriberDeleted(QObject *Subscriber)
QString GetSetting(const QString &Name, const QString &DefaultValue)
#define TORC_TORC
Definition: torclocaldefs.h:8
#define XSD_OUTPUTTYPES
Definition: torccentral.h:58
QString GetTorcContentDir(void)
brief Return the path to generated content
A general purpose event object.
Definition: torcevent.h:9
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: torclogging.h:20
void AddObserver(QObject *Observer)
brief Register the given object to receive events.
#define XSD_OUTPUTS
Definition: torccentral.h:59
static TorcXSDFactory * gTorcXSDFactory
Definition: torccentral.h:80
void GetStrings(QVariantMap &Strings)
#define XSD_CAMERATYPES
Definition: torccentral.h:65
A factory class to register translatable strings for use with external interfaces/applications.
Definition: torclanguage.h:66
#define XSD_INPUTTYPES
Definition: torccentral.h:54
QString temperatureUnits
Definition: torccentral.h:25
void Destroy(void)
void RemoveObserver(QObject *Observer)
brief Deregister the given object.
bool Validated(void) const
QString GetTorcConfigDir(void)
Return the path to the application configuration directory.
static TorcNotify * gNotify
Definition: torcnotify.h:19
#define XSD_TYPES
Definition: torccentral.h:53