Rewrite electricity-logger to use an sqlite3 database

This commit is contained in:
2022-06-25 22:17:46 +02:00
parent 458d824dc8
commit 5b09b06bcf
62 changed files with 5937 additions and 3 deletions

View File

@@ -0,0 +1,60 @@
#include <electricity/logger/database.hpp>
#include <exception>
#include <iomanip>
#include <spdlog/spdlog.h>
#include <sstream>
#include <util/date.hpp>
sqlite3 * OpenDatabase(std::string const & databaseFilePath)
{
sqlite3 * connectionPtr;
if(sqlite3_open(databaseFilePath.c_str(), &connectionPtr) != SQLITE_OK)
{
throw std::runtime_error("Error opening SQLite3 database");
}
sqlite3_extended_result_codes(connectionPtr, 1);
return connectionPtr;
}
std::string ToSqlInsertStatement(DSMR::Data const & data, time_t const time)
{
std::stringstream ss;
ss << "INSERT INTO ElectricityLog VALUES('" << Util::GetSqliteDate(time) << "','" << Util::GetSqliteUtcTime(time)
<< "'," << std::fixed << std::setprecision(2) << data.currentPowerUsageKw << ','
<< data.totalPowerConsumptionDayKwh << ',' << data.totalPowerConsumptionNightKwh << ','
<< data.currentPowerReturnKw << ',' << data.totalPowerReturnedDayKwh << ',' << data.totalPowerReturnedNightKwh
<< ',' << data.usingDayTarif << ',' << data.gasConsumptionCubicMeters << ");";
return ss.str();
}
void Database::Insert(DSMR::Data const & data, time_t const time)
{
std::stringstream transaction;
transaction << "BEGIN TRANSACTION;" << ToSqlInsertStatement(data, time) << "COMMIT;";
if(sqlite3_exec(connectionPtr, transaction.str().c_str(), nullptr, nullptr, nullptr) != SQLITE_OK)
{
spdlog::error("Failed to insert DSMR record into SQLite database: {}", sqlite3_errmsg(connectionPtr));
throw std::runtime_error("Failed to insert DSMR record into SQLite databas");
}
}
Database::Database(std::string const & databaseFilePath)
{
if(sqlite3_open(databaseFilePath.c_str(), &connectionPtr) != SQLITE_OK)
{
spdlog::error("Error whilst opening SQLite database {}", databaseFilePath);
throw std::runtime_error("Error opening SQLite3 database");
}
sqlite3_extended_result_codes(connectionPtr, 1);
}
Database::~Database()
{
if(connectionPtr)
{
sqlite3_close(connectionPtr);
}
}

View File

@@ -0,0 +1,174 @@
#include <electricity/logger/dsmr.hpp>
#include <sstream>
#include <utility>
namespace DSMR
{
const std::unordered_map<std::string, LineTag> & Data::GetMap()
{
static std::unordered_map<std::string, LineTag> map = {
{ "1-3:0.2.8", LineTag::DSMRversion },
{ "0-0:1.0.0", LineTag::DateTimeStamp },
{ "0-0:96.1.1", LineTag::SerialNo },
{ "1-0:1.8.1", LineTag::TotalPowerConsumedTariff1 },
{ "1-0:1.8.2", LineTag::TotalPowerConsumedTariff2 },
{ "1-0:2.8.1", LineTag::TotalReturnedPowerTariff1 },
{ "1-0:2.8.2", LineTag::TotalReturnedPowerTariff2 },
{ "0-0:96.14.0", LineTag::CurrentTarif }, // 1 == night, 2 == day,
{ "1-0:1.7.0", LineTag::CurrentPowerConsumption },
{ "1-0:2.7.0", LineTag::CurrentPowerReturn },
{ "0-0:96.7.21", LineTag::PowerFailureCount },
{ "0-0:96.7.9", LineTag::PowerFailureLongCount },
{ "1-0:99.97.0", LineTag::PowerFailureEventLog },
{ "1-0:32.32.0", LineTag::VoltageL1SagCount },
{ "1-0:52.32.0", LineTag::VoltageL2SagCount },
{ "1-0:72.32.0", LineTag::VoltageL3SagCount },
{ "1-0:32.36.0", LineTag::VoltageL1SwellCount },
{ "1-0:52.36.0", LineTag::VoltageL2SwellCount },
{ "1-0:72.36.0", LineTag::VoltageL3SwellCount },
{ "0-0:96.13.0", LineTag::TextMessageMaxChar },
{ "1-0:32.7.0", LineTag::InstantL1VoltageResolution },
{ "1-0:52.7.0", LineTag::InstantL2VoltageResolution },
{ "1-0:72.7.0", LineTag::InstantL3VoltageResolution },
{ "1-0:31.7.0", LineTag::InstantL1CurrentResolution },
{ "1-0:51.7.0", LineTag::InstantL2CurrentResolution },
{ "1-0:71.7.0", LineTag::InstantL3CurrentResolution },
{ "1-0:21.7.0", LineTag::InstantL1ActivePowerResolution },
{ "1-0:41.7.0", LineTag::InstantL2ActivePowerResolution },
{ "1-0:61.7.0", LineTag::InstantL3ActivePowerResolution },
{ "1-0:22.7.0", LineTag::InstantL1ActivePowerResolutionA },
{ "1-0:42.7.0", LineTag::InstantL2ActivePowerResolutionA },
{ "1-0:62.7.0", LineTag::InstantL3ActivePowerResolutionA },
{ "0-1:24.1.0", LineTag::DeviceType },
{ "0-1:96.1.0", LineTag::GasDeviceIdentifier },
{ "0-1:24.2.1", LineTag::GasTotalConsumptionLog },
};
return map;
}
void Data::RemoveUnit(std::string & value)
{
const auto fresult = value.find('*');
if(fresult == std::string::npos)
{
return;
}
value.resize(fresult);
}
std::pair<std::string, std::string> Data::GetKeyValuePair(const std::string & line)
{
const auto bracketOpen = line.find('(');
if(bracketOpen == std::string::npos)
{
return std::make_pair("", "");
}
const auto bracketClose = line.find(')', bracketOpen);
if(bracketClose == std::string::npos)
{
return std::make_pair(line.substr(0, bracketOpen), "");
}
return std::make_pair(
line.substr(0, bracketOpen),
line.substr(bracketOpen + 1, bracketClose - (bracketOpen + 1)));
}
void Data::ParseLine(const std::string & line)
{
const auto & map = GetMap();
auto pair = GetKeyValuePair(line);
auto & key = pair.first;
auto & value = pair.second;
const auto fresult = map.find(key);
if(fresult == map.end())
{
return;
}
RemoveUnit(value);
std::stringstream ss;
ss.str(value);
switch(fresult->second)
{
case LineTag::CurrentPowerConsumption:
ss >> currentPowerUsageKw;
break;
case LineTag::CurrentPowerReturn:
ss >> currentPowerReturnKw;
break;
case LineTag::TotalPowerConsumedTariff2:
ss >> totalPowerConsumptionDayKwh;
break;
case LineTag::TotalPowerConsumedTariff1:
ss >> totalPowerConsumptionNightKwh;
break;
case LineTag::TotalReturnedPowerTariff1:
ss >> totalPowerReturnedDayKwh;
break;
case LineTag::TotalReturnedPowerTariff2:
ss >> totalPowerReturnedNightKwh;
break;
case LineTag::CurrentTarif:
{
int tarif = 1;
ss >> tarif;
usingDayTarif = (tarif == 2);
}
break;
case LineTag::GasTotalConsumptionLog:
ss >> gasTimestamp;
{
const auto secondBracket = line.find('(', key.size() + value.size() + 2);
if(secondBracket == std::string::npos)
{
break;
}
const auto unitSymbol = line.find('*', secondBracket);
if(secondBracket == std::string::npos)
{
break;
}
const std::string consumption = line.substr(secondBracket + 1, unitSymbol - (secondBracket + 1));
ss.clear();
ss.str(consumption);
ss >> gasConsumptionCubicMeters;
}
break;
default:
break;
}
}
void Data::ParseLines(const std::vector<std::string> & lines)
{
for(const auto & line: lines)
{
ParseLine(line);
}
}
std::string Data::GetFormattedString(char const separator) const
{
std::stringstream ss;
ss << currentPowerUsageKw << separator << totalPowerConsumptionDayKwh << separator
<< totalPowerConsumptionNightKwh << separator << currentPowerReturnKw << separator
<< totalPowerReturnedDayKwh << separator << totalPowerReturnedNightKwh << separator
<< (usingDayTarif ? "day-tarif" : "night-tarif") << separator << gasTimestamp << separator
<< gasConsumptionCubicMeters;
return ss.str();
}
}

View File

@@ -0,0 +1,117 @@
#include <cxxopts.hpp>
#include <electricity/logger/database.hpp>
#include <electricity/logger/dsmr.hpp>
#include <electricity/logger/serialport.hpp>
#include <exception>
#include <fcntl.h>
#include <optional>
#include <spdlog/spdlog.h>
#include <stdexcept>
#include <string>
std::optional<cxxopts::ParseResult> ExtractArgs(int argc, char ** argv)
{
cxxopts::Options options(
"electricity-logger",
"electricity-logger is a small program that fetches power and gas statistics from an attached Landis Gyr E350 electricity meter and outputs this on stdout.");
options.add_options()(
"d,serial-device",
"Absolute path to the serial device to read from",
cxxopts::value<std::string>())(
"connection-string",
"Path to the sqlite3 database file",
cxxopts::value<std::string>());
if(argc == 1)
{
std::cout << options.help() << std::endl;
return {};
}
try
{
auto const parsed = options.parse(argc, argv);
return parsed;
}
catch(cxxopts::OptionException const & e)
{
spdlog::error(e.what());
return {};
}
}
DSMR::Data ReadData(std::string const & devicePath)
{
char const * serialDeviceValue = devicePath.c_str();
int fd = open(serialDeviceValue, O_RDONLY | O_NOCTTY | O_SYNC);
if(fd < 0)
{
spdlog::error("Error opening device {}", serialDeviceValue);
throw std::runtime_error("Error opening serial device for reading");
}
SerialPort serial(fd);
spdlog::info("Starting logging operation with serial device {}", serialDeviceValue);
// Find the first line, which starts with a forward slash
bool waiting = true;
do
{
try
{
auto line = serial.ReadLine();
if(line.size() > 0 && line[0] == '/')
{
waiting = false;
}
}
catch(std::exception const & ex)
{
spdlog::error("Error whilst seeking first output line from device {}: {}", serialDeviceValue, ex.what());
throw std::runtime_error("Error reading from serial device");
}
} while(waiting);
// We reached the interesting bits, parse them and keep what we want
DSMR::Data data;
int i = 1;
while(i < 37)
{
try
{
auto line = serial.ReadLine();
if(line.size() > 5)
{
data.ParseLine(line);
++i;
}
}
catch(std::exception const & ex)
{
spdlog::error("Error whilst parsing line from device {}: {}", serialDeviceValue, ex.what());
throw std::runtime_error("Error parsing data from serial device");
}
}
return data;
}
int main(int argc, char ** argv)
{
auto const maybeArgs = ExtractArgs(argc, argv);
if(!maybeArgs.has_value())
{
return 1;
}
auto const & args = maybeArgs.value();
auto const data = ReadData(args["serial-device"].as<std::string>());
auto const connectionStringValue = args["connection-string"].as<std::string>();
Database db(connectionStringValue);
db.Insert(data, std::time(nullptr));
return 0;
}

View File

@@ -0,0 +1,55 @@
#include <electricity/logger/serialport.hpp>
#include <stdexcept>
#include <string.h>
#include <unistd.h>
void SerialPort::SetAttributes(const speed_t baudrate)
{
cfsetospeed(&configuration, baudrate);
cfsetispeed(&configuration, baudrate);
configuration.c_cflag = (configuration.c_cflag & ~CSIZE) | CS8; // 8 bit chars
configuration.c_iflag &= ~IGNBRK;
configuration.c_lflag = 0;
configuration.c_oflag = 0;
configuration.c_cc[VMIN] = 0; // no blocking read
configuration.c_cc[VTIME] = 5;
configuration.c_iflag &= ~(IXON | IXOFF | IXANY);
configuration.c_cflag |= (CLOCAL | CREAD);
configuration.c_cflag &= ~(PARENB | PARODD);
configuration.c_cflag &= ~(CSTOPB | CRTSCTS);
if(tcsetattr(device, TCSANOW, &configuration) != 0)
{
throw std::runtime_error("Error setting configuration of device.");
}
}
std::string SerialPort::ReadLine()
{
std::string retval;
char c;
while(read(device, &c, 1) == 1)
{
retval += c;
if(c == '\n')
break;
}
return retval;
}
SerialPort::SerialPort(const int fd) : device(fd)
{
memset(&configuration, 0, sizeof(termios));
if(tcgetattr(fd, &configuration) != 0)
{
throw std::runtime_error("Error getting attributes from file descriptor.");
}
oldConfiguration = configuration;
SetAttributes();
}
SerialPort::~SerialPort() { tcsetattr(device, TCSANOW, &oldConfiguration); }

View File

@@ -0,0 +1,51 @@
#include <electricity/server/api.hpp>
#include <electricity/server/configuration.hpp>
#include <pistache/endpoint.h>
#include <tclap/CmdLine.h>
int main(int argc, char ** argv)
{
TCLAP::CmdLine cmd(
"electricity-server is a small Pistache based HTTP content server with a REST API to access the solar log database",
' ',
"1.0.0");
TCLAP::ValueArg<unsigned>
listeningPortArg("p", "listening-port", "TCP listening port number", true, 0u, "TCP listening port number");
cmd.add(listeningPortArg);
TCLAP::ValueArg<std::string> logDirectoryPath(
"d",
"data-directory",
"Absolute path pointing to the logging directory",
true,
"",
"Absolute path pointing to the logging directory");
cmd.add(logDirectoryPath);
TCLAP::ValueArg<std::string> serverDomain(
"s",
"server-domain",
"Domain this server is hosted on",
true,
"",
"Domain this server is hosted on");
cmd.add(serverDomain);
cmd.parse(argc, argv);
auto & config = Server::Configuration::Get();
config.Setup(logDirectoryPath.getValue(), serverDomain.getValue());
Pistache::Address address(Pistache::Ipv4::any(), listeningPortArg.getValue());
Pistache::Http::Endpoint server(address);
auto options = Pistache::Http::Endpoint::options().threads(2);
server.init(options);
Pistache::Rest::Router router;
Server::Api::SetupRouting(router);
server.setHandler(router.handler());
server.serve();
}

View File

@@ -0,0 +1,96 @@
#include <electricity/server/api.hpp>
#include <electricity/server/configuration.hpp>
#include <electricity/server/database.hpp>
#include <fstream>
#include <iomanip>
#include <spdlog/spdlog.h>
#include <sstream>
#include <util/date.hpp>
#include <vector>
namespace Server::Api
{
using Pistache::Http::Mime::Subtype;
using Pistache::Http::Mime::Type;
// Only allow serving to ourselves, all behind the same NAT address
bool IsRequesteeAllowed(Pistache::Http::Request const & request)
{
Configuration & config = Configuration::Get();
auto const & serverAddress = config.GetExternalServerIp();
auto realIpHeader = request.headers().tryGetRaw("X-Real-IP");
if(realIpHeader.isEmpty())
{
spdlog::error("Blocking request without X-Real-IP header");
return false;
}
if(realIpHeader.unsafeGet().value() != serverAddress)
{
spdlog::info(
"Blocking request {} due to host mismatch (expected {})",
realIpHeader.unsafeGet().value(),
serverAddress);
return false;
}
return true;
}
void GetDay(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite)
{
spdlog::info(
"{} {} {}",
Pistache::Http::methodString(request.method()),
request.resource(),
request.query().as_str());
if(!IsRequesteeAllowed(request))
{
responseWrite.send(Pistache::Http::Code::Unauthorized);
return;
}
auto const startQuery = request.query().get("start");
if(startQuery.isEmpty())
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
Util::Date const startDate(startQuery.unsafeGet());
if(!startDate.IsValid())
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
auto const stopQuery = request.query().get("stop");
if(stopQuery.isEmpty())
{
responseWrite.send(
Pistache::Http::Code::Ok,
Database::GetDetailedJsonOf(startDate),
MIME(Application, Json));
return;
}
Util::Date const stopDate(stopQuery.unsafeGet());
if(!stopDate.IsValid() || stopDate.IsBefore(startDate))
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
responseWrite.send(
Pistache::Http::Code::Ok,
Database::GetSummaryJsonOf(startDate, stopDate),
MIME(Application, Json));
}
void SetupRouting(Pistache::Rest::Router & router)
{
Pistache::Rest::Routes::Get(router, "/day", Pistache::Rest::Routes::bind(&GetDay));
}
}

View File

@@ -0,0 +1,98 @@
#include <arpa/inet.h>
#include <electricity/server/configuration.hpp>
#include <errno.h>
#include <netdb.h>
#include <regex>
#include <spdlog/spdlog.h>
#include <sys/socket.h>
namespace Server
{
Configuration::Configuration() : logDirectory() { }
void Configuration::RefreshExternalIp()
{
addrinfo hints;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
hints.ai_protocol = 0;
hints.ai_canonname = nullptr;
hints.ai_addr = nullptr;
hints.ai_next = nullptr;
addrinfo * serverInfo;
auto const result = getaddrinfo(serverDomain.c_str(), "http", &hints, &serverInfo);
if(result)
{
spdlog::error(
"Error {} when parsing domain {} to determine external IP address",
gai_strerror(result),
serverDomain);
lastExternalIp.clear();
return;
}
char addressBuffer[INET_ADDRSTRLEN];
sockaddr_in * socketAddress = reinterpret_cast<sockaddr_in *>(serverInfo->ai_addr);
if(inet_ntop(AF_INET, &socketAddress->sin_addr, addressBuffer, INET_ADDRSTRLEN) != nullptr)
{
lastIpCheckTimePoint = std::chrono::steady_clock::now();
lastExternalIp = std::string(addressBuffer);
spdlog::info("External IP address {} cached", lastExternalIp);
}
else
{
auto const error = errno;
spdlog::error("Error {} returned by inet_ntop when trying to determine external IP address", error);
lastExternalIp.clear();
}
freeaddrinfo(serverInfo);
return;
}
bool Configuration::ExternalIpRequiresRefresh() const
{
if(!std::regex_match(lastExternalIp, std::regex("^([0-9]+\\.){3}[0-9]+$")))
{
return true;
}
auto timeSinceLastRefresh = std::chrono::steady_clock::now() - lastIpCheckTimePoint;
return timeSinceLastRefresh >= std::chrono::minutes(5);
}
void Configuration::Setup(std::string & electricityLogDirectory, std::string const & _serverDomain)
{
logDirectory = electricityLogDirectory;
if(electricityLogDirectory.size() > 0 && electricityLogDirectory[electricityLogDirectory.size() - 1] != '/')
{
logDirectory += '/';
}
serverDomain = _serverDomain;
}
std::string const & Configuration::GetLogDirectory() const { return logDirectory; }
std::string const & Configuration::GetExternalServerIp()
{
if(ExternalIpRequiresRefresh())
{
std::lock_guard<std::mutex> lock(externalIpRefreshMutex);
if(ExternalIpRequiresRefresh())
{
RefreshExternalIp();
}
}
return lastExternalIp;
}
Configuration & Configuration::Get()
{
static Configuration c;
return c;
}
}

View File

@@ -0,0 +1,211 @@
#include <electricity/server/configuration.hpp>
#include <electricity/server/database.hpp>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <util/date.hpp>
#include <vector>
namespace Server::Database
{
struct Record
{
private:
bool isValid;
public:
long epoch;
double currentPowerUsage;
double totalPowerUsageDay;
double totalPowerUsageNight;
double currentPowerReturn;
double totalPowerReturnDay;
double totalPowerReturnNight;
double totalGasUsage;
bool IsValid() const { return isValid; }
void AppendAsJson(std::stringstream & ss) const
{
ss << "{\"dateTime\":" << epoch << ",\"totalPowerUse\":" << totalPowerUsageDay + totalPowerUsageNight
<< ",\"totalPowerReturn\":" << totalPowerReturnDay + totalPowerReturnNight
<< ",\"totalGasUse\":" << totalGasUsage << '}';
}
Record()
: isValid(false), currentPowerUsage(0.0), totalPowerUsageDay(0.0), totalPowerUsageNight(0.0),
currentPowerReturn(0.0), totalPowerReturnDay(0.0), totalPowerReturnNight(0.0), totalGasUsage(0.0)
{ }
Record(std::string const & line)
: isValid(false), currentPowerUsage(0.0), totalPowerUsageDay(0.0), totalPowerUsageNight(0.0),
currentPowerReturn(0.0), totalPowerReturnDay(0.0), totalPowerReturnNight(0.0), totalGasUsage(0.0)
{
std::vector<std::string> values;
char const delimiter = ',';
std::size_t previousIndex = 0;
while(true)
{
auto const commaIndex = line.find(delimiter, previousIndex + 1);
if(commaIndex != std::string::npos)
{
values.push_back(line.substr(previousIndex, commaIndex - previousIndex));
previousIndex = commaIndex + 1;
}
else
{
break;
}
}
if(previousIndex < line.size())
{
values.push_back(line.substr(previousIndex));
}
if(values.size() != 10)
{
return;
}
Util::Date const date(values[0]);
if(!date.IsValid())
{
return;
}
int hours, minutes, seconds;
if(std::sscanf(values[0].substr(11).c_str(), "%d:%d:%d", &hours, &minutes, &seconds) != 3)
{
return;
}
/* Fields are separated by comma's and represent:
1 datetime
2 currentPowerUsage (kw)
3 totalPowerConsumptionDay (kwh)
4 totalPowerConsumptionNight (kwh)
5 currentPowerReturn (kw)
6 totalPowerReturnedDay (kwh)
7 totalPowerReturnedNight (kwh)
8 tarif type (unused)
9 gas timestamp (unused)
10 gas consumption (M^3)
*/
epoch = date.ToEpoch() + hours * 3600 + minutes * 60 + seconds - 3600;
currentPowerUsage = std::atof(values[1].c_str());
totalPowerUsageDay = std::atof(values[2].c_str());
totalPowerUsageNight = std::atof(values[3].c_str());
currentPowerReturn = std::atof(values[4].c_str());
totalPowerReturnDay = std::atof(values[5].c_str());
totalPowerReturnNight = std::atof(values[6].c_str());
totalGasUsage = std::atof(values[9].c_str());
isValid = true;
}
};
std::string GetFileName(Util::Date const & date)
{
std::stringstream filePath;
filePath << Configuration::Get().GetLogDirectory() << date.Year() << '/' << std::setw(2) << std::setfill('0')
<< date.Month() << '_' << std::setw(2) << date.Day() << ".txt";
return filePath.str();
}
std::vector<Record> GetFileContents(Util::Date const & date)
{
std::ifstream logFile(GetFileName(date));
if(!logFile.is_open())
{
return std::vector<Record>();
}
std::vector<Record> retval;
std::string line;
while(std::getline(logFile, line))
{
Record record(line);
if(record.IsValid())
{
retval.push_back(record);
}
}
logFile.close();
return retval;
}
std::string GetDetailedJsonOf(Util::Date const & date)
{
auto const records = GetFileContents(date);
std::stringstream json;
json << '[';
bool first = true;
for(std::size_t i = 0; i < records.size(); ++i)
{
if(!first)
{
json << ',';
}
first = false;
records[i].AppendAsJson(json);
}
json << ']';
return json.str();
}
bool GetSummaryJsonOf(Util::Date const & date, bool const prependComma, std::stringstream & json)
{
if(prependComma)
{
json << ',';
}
auto const records = GetFileContents(date);
if(records.size() < 2)
{
Record record;
record.epoch = date.ToEpoch();
record.AppendAsJson(json);
return false;
}
auto const firstRecord = records[0];
auto lastRecord = records[records.size() - 1];
lastRecord.totalPowerUsageDay -= firstRecord.totalPowerUsageDay;
lastRecord.totalPowerUsageNight -= firstRecord.totalPowerUsageNight;
lastRecord.totalPowerReturnDay -= firstRecord.totalPowerReturnDay;
lastRecord.totalPowerReturnNight -= firstRecord.totalPowerReturnNight;
lastRecord.totalGasUsage -= firstRecord.totalGasUsage;
lastRecord.AppendAsJson(json);
return true;
}
std::string GetSummaryJsonOf(Util::Date const & startDate, Util::Date const & stopDate)
{
std::stringstream json;
json << '[';
long const dayInSeconds = 24 * 60 * 60;
long const start = startDate.ToEpoch();
long const stop = stopDate.ToEpoch();
bool first = true;
for(long current = start; current <= stop; current += dayInSeconds)
{
GetSummaryJsonOf(Util::Date(current), !first, json);
first = false;
}
json << ']';
return json.str();
}
}

32
src/migrator/main.cpp Normal file
View File

@@ -0,0 +1,32 @@
#include <cstdio>
#include <migrator/migrations.hpp>
#include <tclap/CmdLine.h>
int main(int argc, char ** argv)
{
TCLAP::CmdLine cmd("migrator is a small program designed to run migrations", ' ', "1.0");
TCLAP::ValueArg<std::string> migrationArg("m", "migration", "", true, "", "Name of the migration to run");
cmd.add(migrationArg);
TCLAP::ValueArg<std::string> srcArg("s", "source", "", false, "", "Source file or directory to use");
cmd.add(srcArg);
TCLAP::ValueArg<std::string> dstArg("d", "destination", "", false, "", "Destination file or directory to use");
cmd.add(dstArg);
cmd.parse(argc, argv);
auto const & migrationToRun = migrationArg.getValue();
if(migrationToRun == "epochToDateTime")
{
return Migrations::EpochToDateTime(srcArg.getValue(), dstArg.getValue());
}
if(migrationToRun == "updateSummaryTable")
{
return Migrations::UpdateSummaryTable(srcArg.getValue(), dstArg.getValue());
}
std::printf("Unknown migration %s, aborting.\n", migrationToRun.c_str());
return 1;
}

View File

@@ -0,0 +1,91 @@
#include <ctime>
#include <migrator/transaction.hpp>
#include <sqlite3.h>
#include <sstream>
#include <string>
std::string ZeroPadTwoDigitNumber(int number)
{
auto result = std::to_string(number);
if(result.size() == 1)
{
return "0" + result;
}
return result;
}
int SourceCallback(void * data, int argc, char ** argv, char ** columnNames)
{
if(argc != 3)
{
std::puts("Expected source database to have 3 columns: DateTimeUtc, Watt and KilowattHour.");
return 1;
}
Transaction * const transactionPtr = reinterpret_cast<Transaction *>(data);
auto const epochTime = std::atol(argv[0]);
auto const dateTime = *std::gmtime(&epochTime);
std::stringstream insertStream;
insertStream << "INSERT INTO SolarPanelOutput VALUES(" << '\'' << (dateTime.tm_year + 1900) << '-'
<< ZeroPadTwoDigitNumber(dateTime.tm_mon + 1) << '-' << ZeroPadTwoDigitNumber(dateTime.tm_mday)
<< "','" << ZeroPadTwoDigitNumber(dateTime.tm_hour) << ':' << ZeroPadTwoDigitNumber(dateTime.tm_min)
<< ':' << ZeroPadTwoDigitNumber(dateTime.tm_sec) << "'," << argv[1] << ',' << argv[2] << ");";
transactionPtr->AddStatement(insertStream.str());
if(transactionPtr->StatementCount() > 1000)
{
return transactionPtr->Execute();
}
return 0;
}
namespace Migrations
{
int EpochToDateTime(std::string const & sourceDatabase, std::string const & destinationDatabase)
{
if(sourceDatabase == destinationDatabase)
{
std::puts("The EpochToDateTime is not meant to be run on the same database.");
return 1;
}
sqlite3 * source;
if(sqlite3_open(sourceDatabase.c_str(), &source))
{
std::printf("Error opening source database %s\n", sourceDatabase.c_str());
return 1;
}
sqlite3 * destination;
if(sqlite3_open(destinationDatabase.c_str(), &destination))
{
std::printf("Error opening destination database %s\n", destinationDatabase.c_str());
return 1;
}
Transaction transaction(destination);
auto const sqlResult
= sqlite3_exec(source, "SELECT * FROM SolarPanelOutput;", SourceCallback, &transaction, nullptr);
if(sqlResult)
{
std::printf("Error %i during insertion of records into destination database\n", sqlResult);
return 1;
}
auto const commitResult = transaction.Execute();
if(commitResult)
{
std::printf("Error %i when committing last transaction\n", commitResult);
return 1;
}
sqlite3_close(source);
sqlite3_close(destination);
return 0;
}
}

View File

@@ -0,0 +1,68 @@
#include <migrator/transaction.hpp>
#include <sqlite3.h>
#include <sstream>
namespace Migrations
{
int SourceCallback(void * data, int argc, char ** argv, char ** columnNames)
{
// Expect Date and KilowattHour
if(argc != 2)
{
std::puts("Wrong number of columns received");
return 1;
}
Transaction * transactionPtr = reinterpret_cast<Transaction *>(data);
std::stringstream insertStream;
insertStream << "INSERT INTO SolarPanelSummary(Date,KilowattHour) VALUES(" << '\'' << argv[0] << "'," << argv[1]
<< ')' << "ON CONFLICT(Date) DO UPDATE SET KilowattHour = excluded.KilowattHour;";
transactionPtr->AddStatement(insertStream.str());
return 0;
}
int UpdateSummaryTable(std::string const & sourceDatabase, std::string const & destinationDatabase)
{
sqlite3 * source;
if(sqlite3_open(sourceDatabase.c_str(), &source))
{
std::printf("Error opening source database %s\n", sourceDatabase.c_str());
return 1;
}
sqlite3 * destination;
if(sqlite3_open(destinationDatabase.c_str(), &destination))
{
std::printf("Error opening destination database %s\n", destinationDatabase.c_str());
return 1;
}
Transaction transaction(destination);
auto const sqlResult = sqlite3_exec(
source,
"SELECT Date, MAX(KilowattHour) AS KilowattHour FROM SolarPanelOutput GROUP BY Date;",
SourceCallback,
&transaction,
nullptr);
if(sqlResult)
{
std::printf("Error %i during fetching of source database records\n", sqlResult);
return 1;
}
std::printf("UpdateSummaryTable: Upserting %u records...\n", transaction.StatementCount());
auto const commitResult = transaction.Execute();
if(commitResult)
{
std::printf("Error %i when committing transaction\n", commitResult);
return 1;
}
sqlite3_close(source);
sqlite3_close(destination);
return 0;
}
}

View File

@@ -0,0 +1,35 @@
#include <migrator/transaction.hpp>
void Transaction::Reset()
{
queryStream.clear();
queryStream.str(std::string());
queryCount = 0u;
queryStream << "PRAGMA journal_mode = OFF;"
<< "BEGIN TRANSACTION;";
}
void Transaction::AddStatement(std::string const & statement)
{
++queryCount;
queryStream << statement;
}
unsigned Transaction::StatementCount() const { return queryCount; }
int Transaction::Execute()
{
queryStream << "COMMIT;";
auto const result = sqlite3_exec(destination, queryStream.str().c_str(), nullptr, nullptr, nullptr);
Reset();
return result;
}
Transaction::Transaction(sqlite3 * const databaseToInsertIn)
: destination(databaseToInsertIn), queryCount(0u), queryStream()
{
Reset();
}

View File

@@ -0,0 +1,53 @@
#include <cstdlib>
#include <ctime>
#include <iomanip>
#include <solar/logger/database.hpp>
#include <sstream>
#include <stdexcept>
#include <util/date.hpp>
std::string Database::ToSqlInsertStatement(Row const & row) const
{
std::stringstream ss;
ss << "INSERT INTO SolarPanelOutput VALUES(" << '\'' << Util::GetSqliteDate(row.epochTime) << "'," << '\''
<< Util::GetSqliteUtcTime(row.epochTime) << "'," << row.watt << ", " << std::fixed << std::setprecision(2)
<< row.kilowattPerHour << ");";
return ss.str();
}
std::string Database::ToSqlUpsertStatement(Row const & row) const
{
std::stringstream ss;
ss << "INSERT INTO SolarPanelSummary(Date,KilowattHour) VALUES(" << '\'' << Util::GetSqliteDate(row.epochTime)
<< "'," << std::fixed << std::setprecision(2) << row.kilowattPerHour << ')'
<< "ON CONFLICT(Date) DO UPDATE SET KilowattHour = excluded.KilowattHour WHERE excluded.KilowattHour > SolarPanelSummary.KilowattHour;";
return ss.str();
}
bool Database::Insert(Row & row)
{
std::stringstream transaction;
transaction << "BEGIN TRANSACTION;" << ToSqlInsertStatement(row) << ToSqlUpsertStatement(row) << "COMMIT;";
return sqlite3_exec(connectionPtr, transaction.str().c_str(), nullptr, nullptr, nullptr);
}
Database::Database(std::string const & databasePath) : connectionPtr(nullptr)
{
if(sqlite3_open(databasePath.c_str(), &connectionPtr) != SQLITE_OK)
{
throw std::runtime_error("Error opening SQLite3 database");
}
sqlite3_extended_result_codes(connectionPtr, 1);
}
Database::~Database()
{
if(connectionPtr)
{
sqlite3_close(connectionPtr);
}
}

60
src/solar-logger/main.cpp Normal file
View File

@@ -0,0 +1,60 @@
#include <cstdio>
#include <iomanip>
#include <solar/logger/database.hpp>
#include <solar/logger/zeverdata.hpp>
#include <sstream>
#include <string>
#include <tclap/CmdLine.h>
int main(int argc, char ** argv)
{
TCLAP::CmdLine cmd(
"solar-logger is a small program that retrieves solarpower generation statistics from Zeverlution Sxxxx smart power inverters and stores it into a database",
' ',
"1.0");
TCLAP::ValueArg<std::string> urlArg(
"u",
"url",
"Fully qualified URL path and protocol to the home.cgi resource",
true,
"",
"Fully qualified URL path and protocol to the home.cgi resource");
cmd.add(urlArg);
TCLAP::ValueArg<unsigned int>
timeoutArg("t", "timeout", "Fetch time out in milliseconds", false, 1000U, "Fetch time out in milliseconds");
cmd.add(timeoutArg);
TCLAP::ValueArg<std::string> databasePathArg(
"d",
"database-path",
"Absolute path pointing to the solar SQLite *.db file",
true,
"",
"Absolute path pointing to the solar SQLite *.db file");
cmd.add(databasePathArg);
cmd.parse(argc, argv);
ZeverData zeverData;
if(!zeverData.FetchDataFromURL(urlArg.getValue(), timeoutArg.getValue()))
{
return -1;
}
Row row;
row.epochTime = std::time(nullptr); // now
row.watt = zeverData.watt;
row.kilowattPerHour = zeverData.kilowattPerHour;
Database db(databasePathArg.getValue());
auto const insertionResult = db.Insert(row);
if(insertionResult)
{
std::printf("Error %i during insertion of new value into database\n", insertionResult);
return -1;
}
return 0;
}

View File

@@ -0,0 +1,88 @@
#include <solar/logger/zeverdata.hpp>
namespace detail
{
// libcurl callback
size_t write_to_string(void * ptr, size_t size, size_t nmemb, void * stream)
{
std::string line((char *)ptr, nmemb);
std::string * buffer = (std::string *)stream;
buffer->append(line);
return nmemb * size;
}
}
bool ZeverData::ParseString(const std::string & str)
{
bool isValid = true;
std::stringstream ss;
ss.str(str);
ss >> number0;
ss >> number1;
ss >> registeryID;
ss >> registeryKey;
ss >> hardwareVersion;
std::string versionInfo;
ss >> versionInfo;
size_t splitPos = versionInfo.find('+');
appVersion = versionInfo.substr(0, splitPos);
++splitPos;
wifiVersion = versionInfo.substr(splitPos, versionInfo.size());
ss >> timeValue;
ss >> dateValue;
ss >> zeverCloudStatus;
ss >> number3;
ss >> inverterSN;
isValid = (ss >> watt && ss >> kilowattPerHour);
ss >> OKmsg;
ss >> ERRORmsg;
if(!isValid)
{
std::fprintf(stderr, "Error during parsing of zever data:\n%s\n", str.c_str());
return false;
}
return true;
}
bool ZeverData::FetchDataFromURL(const std::string & url, const unsigned int timeout)
{
curl_global_init(CURL_GLOBAL_ALL);
CURL * curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
//curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
std::string buffer;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, detail::write_to_string);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout);
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
buffer.reserve(256);
if(curl_easy_perform(curl))
{
watt = 0;
kilowattPerHour = 0.0;
std::fprintf(stderr, "Failed to fetch zever data from URL <%s>!\n", url.c_str());
curl_easy_cleanup(curl);
return false;
}
buffer.shrink_to_fit();
curl_easy_cleanup(curl);
return ParseString(buffer);
}
ZeverData::ZeverData() : watt(0), kilowattPerHour(0.0) { }

93
src/solar-server/api.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include <solar/server/api.hpp>
#include <solar/server/configuration.hpp>
#include <util/date.hpp>
namespace Api
{
using Pistache::Http::Mime::Subtype;
using Pistache::Http::Mime::Type;
Util::Date ParseUtcDate(std::string const & date, Util::Date const & fallbackValue)
{
Util::Date result;
if(!result.TryParse(date))
{
return fallbackValue;
}
return result;
}
Util::Date ParseUtcDate(Pistache::Optional<std::string> const & date, Util::Date const & fallbackValue)
{
if(date.isEmpty() || date.unsafeGet().size() != 10)
{
return fallbackValue;
}
return ParseUtcDate(date.unsafeGet(), fallbackValue);
}
void GetDay(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite)
{
Util::Date const start = ParseUtcDate(request.query().get("start"), Util::Date::UtcNow());
auto const stopQuery = request.query().get("stop");
if(stopQuery.isEmpty())
{
responseWrite.send(
Pistache::Http::Code::Ok,
Configuration::Get().GetDatabaseConnection().GetEntireDay(start),
MIME(Application, Json));
return;
}
Util::Date const stop = ParseUtcDate(request.query().get("stop"), start);
if(!start.IsBefore(stop))
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
responseWrite.send(
Pistache::Http::Code::Ok,
Configuration::Get().GetDatabaseConnection().GetSummarizedPerDayRecords(start, stop),
MIME(Application, Json));
}
void GetMonth(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite)
{
auto const startQuery = request.query().get("start");
auto const stopQuery = request.query().get("stop");
if(startQuery.isEmpty() || stopQuery.isEmpty())
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
auto const start = Util::Date(startQuery.unsafeGet());
auto stop = Util::Date(stopQuery.unsafeGet());
if(!start.IsValid() || !stop.IsValid())
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
stop.SetDayToEndOfMonth();
if(stop.IsBefore(start))
{
responseWrite.send(Pistache::Http::Code::Bad_Request);
return;
}
responseWrite.send(
Pistache::Http::Code::Ok,
Configuration::Get().GetDatabaseConnection().GetSummarizedPerMonthRecords(start, stop),
MIME(Application, Json));
}
void SetupRouting(Pistache::Rest::Router & router)
{
Pistache::Rest::Routes::Get(router, "/day", Pistache::Rest::Routes::bind(&Api::GetDay));
Pistache::Rest::Routes::Get(router, "/month", Pistache::Rest::Routes::bind(&Api::GetMonth));
}
}

View File

@@ -0,0 +1,24 @@
#include <filesystem>
#include <solar/server/configuration.hpp>
#include <stdexcept>
Configuration::Configuration() : database() { }
Configuration & Configuration::SetupDatabase(std::string const & filePath)
{
if(!database.Connect(filePath))
{
throw std::runtime_error("Cannot open SQLite database at " + filePath);
}
return *this;
}
Database::Connection Configuration::GetDatabaseConnection() const { return database.GetConnection(); }
Configuration & Configuration::Get()
{
static Configuration c;
return c;
}

View File

@@ -0,0 +1,241 @@
#include <array>
#include <cmath>
#include <iomanip>
#include <solar/server/database/connection.hpp>
#include <sstream>
#include <vector>
namespace Database
{
Connection::Connection(sqlite3 * const databaseConnectionPtr) : connectionPtr(databaseConnectionPtr) { }
class JsonResult {
private:
long dateEpoch;
std::stringstream jsonStream;
unsigned insertions;
void Reset()
{
insertions = 0u;
jsonStream.clear();
jsonStream.str(std::string());
jsonStream << std::fixed << std::setprecision(2);
jsonStream << '[';
}
public:
bool AddRecord(long const epoch, char const * watt, char const * kwh)
{
++insertions;
jsonStream << "{\"time\":" << epoch << ",\"watt\":" << watt << ",\"kwh\":" << kwh << "},";
return true;
}
bool AddRecord(char const * timeUtc, char const * watt, char const * kwh)
{
auto const timeInSeconds = Util::ToSecondsFromTimeString(timeUtc);
if(timeInSeconds < 0)
{
std::printf("AddRecord: cannot parse %s to hours, minutes and seconds\n", timeUtc);
return false;
}
return AddRecord(timeInSeconds + dateEpoch, watt, kwh);
}
bool
AddRecord(unsigned const year, unsigned const month, unsigned const day, char const * watt, char const * kwh)
{
return AddRecord(Util::GetEpoch(year, month, day), watt, kwh);
}
// Returns the records added thus far as JSON array and resets
// itself to an empty array afterwards.
std::string GetJsonArray()
{
if(insertions)
{
// Replace last inserted comma
jsonStream.seekp(-1, std::ios_base::end);
jsonStream << ']';
}
else
{
jsonStream << ']';
}
auto const result = jsonStream.str();
Reset();
return result;
}
JsonResult(long _dateEpoch) : dateEpoch(_dateEpoch), jsonStream(), insertions(0u) { Reset(); }
};
int DetailedCallback(void * data, int argc, char ** argv, char ** columnNames)
{
// TimeUtc, Watt and KilowattHour
if(argc != 3)
{
std::printf("DetailedCallback: unexpected number of arguments %i\n", argc);
return -1;
}
JsonResult * result = reinterpret_cast<JsonResult *>(data);
result->AddRecord(argv[0], argv[1], argv[2]);
return 0;
}
std::string Connection::GetEntireDay(Util::Date const & date)
{
std::stringstream queryStream;
queryStream << "SELECT TimeUtc, Watts, KilowattHour FROM SolarPanelOutput WHERE Date = " << '\''
<< date.ToISOString() << '\'' << " ORDER BY TimeUtc ASC;";
JsonResult result(date.ToEpoch());
auto const sqlResult
= sqlite3_exec(connectionPtr, queryStream.str().c_str(), DetailedCallback, &result, nullptr);
if(sqlResult)
{
std::printf("GetEntireDay: SQLite error code %i, returning empty JSON array.\n", sqlResult);
return "[]";
}
return result.GetJsonArray();
}
int SummaryCallback(void * data, int argc, char ** argv, char ** columnNames)
{
// Date and KilowattHour
if(argc != 2)
{
std::printf("SummaryCallback: unexpected number of arguments %i\n", argc);
return -1;
}
JsonResult * result = reinterpret_cast<JsonResult *>(data);
Util::Date recordDate(argv[0]);
result->AddRecord(recordDate.Year(), recordDate.Month(), recordDate.Day(), "0", argv[1]);
return 0;
}
std::string Connection::GetSummarizedPerDayRecords(Util::Date const & startDate, Util::Date const & endDate)
{
std::stringstream queryStream;
queryStream << "SELECT Date, KilowattHour FROM SolarPanelSummary"
<< " WHERE Date >= '" << startDate.ToISOString() << '\'' << " AND Date <= '"
<< endDate.ToISOString() << '\'' << " ORDER BY Date;";
JsonResult result(0);
auto const sqlResult
= sqlite3_exec(connectionPtr, queryStream.str().c_str(), SummaryCallback, &result, nullptr);
if(sqlResult)
{
std::printf("GetSummarizedPerDayRecords: SQLite error code %i, returning empty JSON array.\n", sqlResult);
return "[]";
}
return result.GetJsonArray();
}
struct YearResult
{
int year;
std::array<double, 12> monthValues;
// month is in range 1 to 12
void AddMonthValue(int month, double value) { monthValues[month - 1] += value; }
YearResult(int _year) : year(_year), monthValues() { }
};
int MonthSummaryCallback(void * data, int argc, char ** argv, char ** columnNames)
{
// Date and KilowattHour
if(argc != 2)
{
std::printf("MonthSummaryCallback: unexpected number of arguments %i\n", argc);
return -1;
}
Util::Date recordDate;
if(!recordDate.TryParse(argv[0]))
{
std::printf("MonthSummaryCallback: error parsing date %s\n", argv[0]);
return -1;
}
std::vector<YearResult> & yearResults = *reinterpret_cast<std::vector<YearResult> *>(data);
double const kwh = std::atof(argv[1]);
if(std::isnan(kwh) || kwh < 0.0)
{
// This value makes no sense, ignore it
std::printf("MonthSummaryCallback: ignoring bogus value for year month %s\n", argv[1]);
return 0;
}
for(std::size_t i = 0; i < yearResults.size(); ++i)
{
if(yearResults[i].year == recordDate.Year())
{
yearResults[i].AddMonthValue(recordDate.Month(), kwh);
return 0;
}
if(yearResults[i].year > recordDate.Year())
{
yearResults.insert(yearResults.begin() + i, YearResult(recordDate.Year()));
yearResults[i].AddMonthValue(recordDate.Month(), kwh);
return 0;
}
}
yearResults.push_back(YearResult(recordDate.Year()));
yearResults[yearResults.size() - 1].AddMonthValue(recordDate.Month(), kwh);
return 0;
}
std::string Connection::GetSummarizedPerMonthRecords(Util::Date const & startDate, Util::Date const & endDate)
{
std::stringstream queryStream;
queryStream << "SELECT Date, KilowattHour FROM SolarPanelSummary"
<< " WHERE Date >= '" << startDate.ToISOString() << '\'' << " AND Date <= '"
<< endDate.ToISOString() << "';";
std::vector<YearResult> yearResults;
auto const sqlResult
= sqlite3_exec(connectionPtr, queryStream.str().c_str(), MonthSummaryCallback, &yearResults, nullptr);
if(sqlResult || yearResults.size() == 0)
{
std::printf(
"GetSummarizedPerMonthRecords: SQLite return code %i and %lu years retrieved, returning empty JSON array.\n",
sqlResult,
yearResults.size());
return "[]";
}
JsonResult result(0);
for(std::size_t i = 0; i < yearResults.size(); ++i)
{
auto const year = yearResults[i].year;
for(int month = 0; month < yearResults[i].monthValues.size(); ++month)
{
if(startDate.IsAfter(year, month + 1, 1) || endDate.IsBefore(year, month + 1, 1))
{
continue;
}
auto const epoch = Util::GetEpoch(year, month + 1, 1);
auto const kwh = yearResults[i].monthValues[month];
result.AddRecord(epoch, "0", std::to_string(kwh).c_str());
}
}
return result.GetJsonArray();
}
}

View File

@@ -0,0 +1,33 @@
#include <solar/server/database/database.hpp>
namespace Database
{
bool Database::Connect(std::string const & path)
{
if(connectionPtr)
{
// Already connected
return true;
}
if(sqlite3_open(path.c_str(), &connectionPtr))
{
return false;
}
sqlite3_extended_result_codes(connectionPtr, 1);
return true;
}
Connection Database::GetConnection() const { return Connection(connectionPtr); }
Database::Database() : connectionPtr(nullptr) { }
Database::~Database()
{
if(connectionPtr)
{
sqlite3_close(connectionPtr);
}
}
}

42
src/solar-server/main.cpp Normal file
View File

@@ -0,0 +1,42 @@
#include <pistache/endpoint.h>
#include <solar/server/api.hpp>
#include <solar/server/configuration.hpp>
#include <tclap/CmdLine.h>
int main(int argc, char ** argv)
{
TCLAP::CmdLine cmd(
"solar-server is a small Pistache based HTTP content server with a REST API to access the solar log database",
' ',
"1.0.0");
TCLAP::ValueArg<unsigned>
listeningPortArg("p", "listening-port", "TCP listening port number", true, 0u, "TCP listening port number");
cmd.add(listeningPortArg);
TCLAP::ValueArg<std::string> databaseFileArg(
"d",
"database-file",
"Absolute path pointing to the solar SQLite *.db file",
true,
"",
"Absolute path pointing to the solar SQLite *.db file");
cmd.add(databaseFileArg);
cmd.parse(argc, argv);
Configuration & config = Configuration::Get();
config.SetupDatabase(databaseFileArg.getValue());
Pistache::Address address(Pistache::Ipv4::any(), listeningPortArg.getValue());
Pistache::Http::Endpoint server(address);
auto options = Pistache::Http::Endpoint::options().threads(2);
server.init(options);
Pistache::Rest::Router router;
Api::SetupRouting(router);
server.setHandler(router.handler());
server.serve();
}