Add envoy support to solar logger

This commit is contained in:
2022-08-15 13:14:16 +02:00
parent 642170172a
commit 35e6ec08d7
16 changed files with 355 additions and 309 deletions

View File

@@ -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)

View 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 {};
}
}

View File

@@ -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;
}

View File

@@ -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) { }