Add envoy support to solar logger
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
@@ -6,27 +7,37 @@
|
||||
#include <stdexcept>
|
||||
#include <util/date.hpp>
|
||||
|
||||
std::string Database::ToSqlInsertStatement(Row const & row) const
|
||||
std::string ToSqlInsertStatement(ZeverRow const & row)
|
||||
{
|
||||
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 << ");";
|
||||
ss << "INSERT INTO ZeverLogs VALUES(" << '\'' << Util::GetSqliteDate(row.epochTime) << "'," << '\''
|
||||
<< Util::GetSqliteUtcTime(row.epochTime) << "'," << row.watt << ','
|
||||
<< static_cast<std::int64_t>(round(row.kilowattPerHour * 1000)) << ");";
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string Database::ToSqlUpsertStatement(Row const & row) const
|
||||
std::string ToSqlInsertStatement(EnvoyRow const & row)
|
||||
{
|
||||
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;";
|
||||
ss << "INSERT INTO EnvoyLogs VALUES(" << '\'' << Util::GetSqliteDate(row.epochTime) << "'," << '\''
|
||||
<< Util::GetSqliteUtcTime(row.epochTime) << "'," << row.currentWatt << ',' << row.lifetimeWattHour << ','
|
||||
<< row.inverterCount << ");";
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
bool Database::Insert(Row & row)
|
||||
std::string ToSqlUpsertStatement(ZeverRow const & row)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "INSERT INTO ZeverSummary(Date,TotalWatts) VALUES(" << '\'' << Util::GetSqliteDate(row.epochTime) << "',"
|
||||
<< std::fixed << std::setprecision(2) << static_cast<std::int64_t>(round(row.kilowattPerHour * 1000)) << ')'
|
||||
<< "ON CONFLICT(Date) DO UPDATE SET TotalWatts = excluded.TotalWatts WHERE excluded.TotalWatts > ZeverSummary.TotalWatts;";
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
bool Database::Insert(ZeverRow & row)
|
||||
{
|
||||
std::stringstream transaction;
|
||||
transaction << "BEGIN TRANSACTION;" << ToSqlInsertStatement(row) << ToSqlUpsertStatement(row) << "COMMIT;";
|
||||
@@ -34,6 +45,11 @@ bool Database::Insert(Row & row)
|
||||
return sqlite3_exec(connectionPtr, transaction.str().c_str(), nullptr, nullptr, nullptr);
|
||||
}
|
||||
|
||||
bool Database::Insert(EnvoyRow & row)
|
||||
{
|
||||
return sqlite3_exec(connectionPtr, ToSqlInsertStatement(row).c_str(), nullptr, nullptr, nullptr);
|
||||
}
|
||||
|
||||
Database::Database(std::string const & databasePath) : connectionPtr(nullptr)
|
||||
{
|
||||
if(sqlite3_open(databasePath.c_str(), &connectionPtr) != SQLITE_OK)
|
||||
|
||||
69
src/solar-logger/envoydata.cpp
Normal file
69
src/solar-logger/envoydata.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#include <exception>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <solar/logger/envoydata.hpp>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <stdexcept>
|
||||
|
||||
nlohmann::basic_json<> ExtractEnvoyProductionInvertersObject(std::string const & json)
|
||||
{
|
||||
auto const parsed = nlohmann::json::parse(json);
|
||||
if(!parsed.is_object())
|
||||
{
|
||||
throw std::runtime_error("Invalid JSON");
|
||||
}
|
||||
|
||||
auto const production = parsed["production"];
|
||||
if(!production.is_array())
|
||||
{
|
||||
throw std::runtime_error("Invalid production property type");
|
||||
}
|
||||
|
||||
auto const invertersObject = std::find_if(
|
||||
production.begin(),
|
||||
production.end(),
|
||||
[](nlohmann::basic_json<> const & item)
|
||||
{ return item.is_object() && item["type"].is_string() && item["type"].get<std::string>() == "inverters"; });
|
||||
if(invertersObject == production.end())
|
||||
{
|
||||
throw std::runtime_error("Failed to find JSON object of type inverters in production array");
|
||||
}
|
||||
|
||||
if(!(*invertersObject)["activeCount"].is_number_integer())
|
||||
{
|
||||
throw std::runtime_error("Invalid activeCount property in inverters object");
|
||||
}
|
||||
if(!(*invertersObject)["readingTime"].is_number_integer())
|
||||
{
|
||||
throw std::runtime_error("Invalid readingTime property in inverters object");
|
||||
}
|
||||
if(!(*invertersObject)["wNow"].is_number_integer())
|
||||
{
|
||||
throw std::runtime_error("Invalid wNow property in inverters object");
|
||||
}
|
||||
if(!(*invertersObject)["whLifetime"].is_number_integer())
|
||||
{
|
||||
throw std::runtime_error("Invalid whLifetime property in inverters object");
|
||||
}
|
||||
|
||||
return *invertersObject;
|
||||
}
|
||||
|
||||
std::optional<EnvoyData> EnvoyData::ParseJson(std::string const & json)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto const parsed = ExtractEnvoyProductionInvertersObject(json);
|
||||
return EnvoyData {
|
||||
.type = parsed["type"].get<std::string>(),
|
||||
.activeCount = parsed["activeCount"].get<std::int32_t>(),
|
||||
.readingTime = parsed["readingTime"].get<std::int64_t>(),
|
||||
.wNow = parsed["wNow"].get<std::int32_t>(),
|
||||
.whLifetime = parsed["whLifetime"].get<std::int64_t>(),
|
||||
};
|
||||
}
|
||||
catch(std::exception & ex)
|
||||
{
|
||||
spdlog::error("Unexpected exception {} whilst parsing JSON {}", ex.what(), json);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,171 @@
|
||||
#include <cstdio>
|
||||
#include <cxxopts.hpp>
|
||||
#include <iomanip>
|
||||
#include <optional>
|
||||
#include <solar/logger/database.hpp>
|
||||
#include <solar/logger/envoydata.hpp>
|
||||
#include <solar/logger/zeverdata.hpp>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <tclap/CmdLine.h>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<cxxopts::ParseResult> ExtractArgs(int argc, char ** argv)
|
||||
{
|
||||
cxxopts::Options options(
|
||||
"solar-logger",
|
||||
"solar-logger is a small program that retrieves solarpower generation statistics from smart inverters. It supports the Zeverlution Sxxxx and the Envoy S Metered (v5.x.x)");
|
||||
|
||||
options.add_options()(
|
||||
"u,url",
|
||||
"Fully qualified URL path and protocol to the web resource containing the stastics. For the Zeverlution platform this is /home.cgi and for the Envoy S this is /production.json.",
|
||||
cxxopts::value<std::string>())(
|
||||
"connection-string",
|
||||
"Path to the sqlite3 database file",
|
||||
cxxopts::value<std::string>())(
|
||||
"type",
|
||||
"Type of web resource to fetch; \"Zeverlution\" or \"Envoy\"",
|
||||
cxxopts::value<std::string>())(
|
||||
"t,timeout",
|
||||
"Fetch time out in milliseconds",
|
||||
cxxopts::value<unsigned>()->default_value("1000"));
|
||||
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> 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);
|
||||
const auto result = curl_easy_perform(curl);
|
||||
if(result)
|
||||
{
|
||||
spdlog::error("Failed to fetch data from URL {} with cURL error code {}", url.c_str(), result);
|
||||
curl_easy_cleanup(curl);
|
||||
return {};
|
||||
}
|
||||
|
||||
buffer.shrink_to_fit();
|
||||
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
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()))
|
||||
auto const maybeArgs = ExtractArgs(argc, argv);
|
||||
if(!maybeArgs.has_value())
|
||||
{
|
||||
return -1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
Row row;
|
||||
row.epochTime = std::time(nullptr); // now
|
||||
row.watt = zeverData.watt;
|
||||
row.kilowattPerHour = zeverData.kilowattPerHour;
|
||||
auto const & args = maybeArgs.value();
|
||||
|
||||
Database db(databasePathArg.getValue());
|
||||
auto const insertionResult = db.Insert(row);
|
||||
if(insertionResult)
|
||||
Database db(args["connection-string"].as<std::string>());
|
||||
auto const now = std::time(nullptr);
|
||||
auto const resourceType = args["type"].as<std::string>();
|
||||
|
||||
if(resourceType == "Zeverlution")
|
||||
{
|
||||
std::printf("Error %i during insertion of new value into database\n", insertionResult);
|
||||
auto const webResource = FetchDataFromURL(args["url"].as<std::string>(), args["timeout"].as<unsigned>());
|
||||
if(!webResource.has_value())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto const zeverData = ZeverData::ParseString(webResource.value());
|
||||
if(!zeverData.has_value())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
ZeverRow row = {
|
||||
.epochTime = now,
|
||||
.watt = zeverData->watt,
|
||||
.kilowattPerHour = zeverData->kilowattPerHour,
|
||||
};
|
||||
|
||||
auto const insertionResult = db.Insert(row);
|
||||
if(insertionResult)
|
||||
{
|
||||
spdlog::error("Error {} during insertion of new value into database", insertionResult);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if(resourceType == "Envoy")
|
||||
{
|
||||
auto const webResource = FetchDataFromURL(args["url"].as<std::string>(), args["timeout"].as<unsigned>());
|
||||
if(!webResource.has_value())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto const parsed = EnvoyData::ParseJson(webResource.value());
|
||||
if(!parsed.has_value())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
EnvoyRow row = {
|
||||
.epochTime = now,
|
||||
.inverterCount = parsed->activeCount,
|
||||
.currentWatt = parsed->wNow,
|
||||
.lifetimeWattHour = parsed->whLifetime,
|
||||
.inverterTime = parsed->readingTime,
|
||||
};
|
||||
|
||||
auto const insertionResult = db.Insert(row);
|
||||
if(insertionResult)
|
||||
{
|
||||
spdlog::error("Error {} during insertion of new value into database", insertionResult);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
spdlog::error("Unrecognized Type argument {}", resourceType);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,88 +1,41 @@
|
||||
#include <solar/logger/zeverdata.hpp>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
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)
|
||||
std::optional<ZeverData> ZeverData::ParseString(const std::string & str)
|
||||
{
|
||||
ZeverData data;
|
||||
bool isValid = true;
|
||||
std::stringstream ss;
|
||||
ss.str(str);
|
||||
|
||||
ss >> number0;
|
||||
ss >> number1;
|
||||
ss >> registeryID;
|
||||
ss >> registeryKey;
|
||||
ss >> hardwareVersion;
|
||||
ss >> data.number0;
|
||||
ss >> data.number1;
|
||||
ss >> data.registeryID;
|
||||
ss >> data.registeryKey;
|
||||
ss >> data.hardwareVersion;
|
||||
std::string versionInfo;
|
||||
ss >> versionInfo;
|
||||
size_t splitPos = versionInfo.find('+');
|
||||
appVersion = versionInfo.substr(0, splitPos);
|
||||
data.appVersion = versionInfo.substr(0, splitPos);
|
||||
++splitPos;
|
||||
wifiVersion = versionInfo.substr(splitPos, versionInfo.size());
|
||||
data.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;
|
||||
ss >> data.timeValue;
|
||||
ss >> data.dateValue;
|
||||
ss >> data.zeverCloudStatus;
|
||||
ss >> data.number3;
|
||||
ss >> data.inverterSN;
|
||||
isValid = (ss >> data.watt && ss >> data.kilowattPerHour);
|
||||
ss >> data.OKmsg;
|
||||
ss >> data.ERRORmsg;
|
||||
|
||||
if(!isValid)
|
||||
{
|
||||
std::fprintf(stderr, "Error during parsing of zever data:\n%s\n", str.c_str());
|
||||
return false;
|
||||
spdlog::error("Error parsing zever data {}", str);
|
||||
return {};
|
||||
}
|
||||
|
||||
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);
|
||||
return data;
|
||||
}
|
||||
|
||||
ZeverData::ZeverData() : watt(0), kilowattPerHour(0.0) { }
|
||||
|
||||
Reference in New Issue
Block a user