Rewrite electricity-logger to use an sqlite3 database
This commit is contained in:
107
.clang-format
Normal file
107
.clang-format
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
Language: Cpp
|
||||
|
||||
DisableFormat: false
|
||||
Standard: Latest
|
||||
BasedOnStyle: WebKit
|
||||
TabWidth: 4
|
||||
IndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
ConstructorInitializerIndentWidth: 1
|
||||
UseTab: Never
|
||||
ColumnLimit: 120
|
||||
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: AlwaysBreak
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignConsecutiveMacros: false
|
||||
AlignEscapedNewlines: DontAlign
|
||||
AlignOperands: false
|
||||
AlignTrailingComments: false
|
||||
AllowAllArgumentsOnNextLine: false
|
||||
AllowAllConstructorInitializersOnNextLine: false
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortLambdasOnASingleLine: Inline
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: No
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
|
||||
# Configure each individual brace in BraceWrapping
|
||||
BreakBeforeBraces: Custom
|
||||
# Control of individual brace wrapping cases
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: true
|
||||
AfterClass: false
|
||||
AfterControlStatement: Always
|
||||
AfterEnum: true
|
||||
AfterFunction: true
|
||||
AfterNamespace: true
|
||||
AfterStruct: true
|
||||
AfterUnion: true
|
||||
AfterExternBlock: true
|
||||
BeforeCatch: true
|
||||
BeforeElse: true
|
||||
BeforeLambdaBody: true
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: false
|
||||
SplitEmptyRecord: false
|
||||
SplitEmptyNamespace: true
|
||||
|
||||
BreakAfterJavaFieldAnnotations: true
|
||||
BreakBeforeTernaryOperators: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakStringLiterals: false
|
||||
CommentPragmas: "^ IWYU pragma:"
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
Cpp11BracedListStyle: true
|
||||
FixNamespaceComments: false
|
||||
IndentCaseLabels: true
|
||||
IndentPPDirectives: BeforeHash
|
||||
IndentWrappedFunctionNames: false
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
MacroBlockBegin: ""
|
||||
MacroBlockEnd: ""
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: All
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Middle
|
||||
# QualifierAlignment: Right # Only supported in clang-format 14+
|
||||
# ReferenceAlignmentStyle: Middle # Only supported in clang-format 14+
|
||||
SortIncludes: true
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCpp11BracedList: true
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: Never
|
||||
SpaceBeforeRangeBasedForLoopColon: false
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
|
||||
# Comments are for developers, they should arrange them
|
||||
ReflowComments: false
|
||||
|
||||
IncludeBlocks: Preserve
|
||||
---
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
build/
|
||||
|
||||
# ---> C++
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
19
.vscode/c_cpp_properties.json
vendored
Normal file
19
.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Linux",
|
||||
"intelliSenseMode": "gcc-x64",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/include"
|
||||
],
|
||||
"defines": [
|
||||
"SPDLOG_FMT_EXTERNAL"
|
||||
],
|
||||
"forcedInclude": [],
|
||||
"compilerPath": "/usr/bin/gcc",
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++17"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
1
.vscode/configurationCache.log
vendored
Normal file
1
.vscode/configurationCache.log
vendored
Normal file
File diff suppressed because one or more lines are too long
55
.vscode/dryrun.log
vendored
Normal file
55
.vscode/dryrun.log
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
make --dry-run --always-make --keep-going --print-directory
|
||||
make: Entering directory '/home/tijmen/project/home-data-collection-tools'
|
||||
|
||||
mkdir -p build/solar-logger
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/solar-logger/main.cpp -o build/solar-logger/main.o
|
||||
mkdir -p build/solar-logger
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/solar-logger/zeverdata.cpp -o build/solar-logger/zeverdata.o
|
||||
mkdir -p build/solar-logger
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/solar-logger/database.cpp -o build/solar-logger/database.o
|
||||
mkdir -p bin
|
||||
g++ build/solar-logger/main.o build/solar-logger/zeverdata.o build/solar-logger/database.o -lsqlite3 -lcurl -o bin/solar-logger
|
||||
mkdir -p build/solar-server/database
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/solar-server/database/connection.cpp -o build/solar-server/database/connection.o
|
||||
mkdir -p build/solar-server/database
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/solar-server/database/database.cpp -o build/solar-server/database/database.o
|
||||
mkdir -p build/solar-server
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/solar-server/api.cpp -o build/solar-server/api.o
|
||||
mkdir -p build/solar-server
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/solar-server/main.cpp -o build/solar-server/main.o
|
||||
mkdir -p build/solar-server
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/solar-server/configuration.cpp -o build/solar-server/configuration.o
|
||||
mkdir -p bin
|
||||
g++ build/solar-server/database/connection.o build/solar-server/database/database.o build/solar-server/api.o build/solar-server/main.o build/solar-server/configuration.o -lpistache -lsqlite3 -lstdc++fs -lspdlog -lfmt -o bin/solar-server
|
||||
mkdir -p build/migrator/migrations
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/migrator/migrations/updatesummarytable.cpp -o build/migrator/migrations/updatesummarytable.o
|
||||
mkdir -p build/migrator/migrations
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/migrator/migrations/epochtodatetime.cpp -o build/migrator/migrations/epochtodatetime.o
|
||||
mkdir -p build/migrator
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/migrator/main.cpp -o build/migrator/main.o
|
||||
mkdir -p build/migrator
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/migrator/transaction.cpp -o build/migrator/transaction.o
|
||||
mkdir -p bin
|
||||
g++ build/migrator/migrations/updatesummarytable.o build/migrator/migrations/epochtodatetime.o build/migrator/main.o build/migrator/transaction.o -lsqlite3 -o bin/migrator
|
||||
mkdir -p build/electricity-logger
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/electricity-logger/serialport.cpp -o build/electricity-logger/serialport.o
|
||||
mkdir -p build/electricity-logger
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/electricity-logger/dsmr.cpp -o build/electricity-logger/dsmr.o
|
||||
mkdir -p build/electricity-logger
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/electricity-logger/main.cpp -o build/electricity-logger/main.o
|
||||
mkdir -p build/electricity-logger
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/electricity-logger/database.cpp -o build/electricity-logger/database.o
|
||||
mkdir -p bin
|
||||
g++ build/electricity-logger/serialport.o build/electricity-logger/dsmr.o build/electricity-logger/main.o build/electricity-logger/database.o -lsqlite3 -lspdlog -lfmt -o bin/electricity-logger
|
||||
mkdir -p build/electricity-server
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/electricity-server/main.cpp -o build/electricity-server/main.o
|
||||
mkdir -p build/electricity-server/server
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/electricity-server/server/api.cpp -o build/electricity-server/server/api.o
|
||||
mkdir -p build/electricity-server/server
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/electricity-server/server/configuration.cpp -o build/electricity-server/server/configuration.o
|
||||
mkdir -p build/electricity-server/server
|
||||
g++ -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD -c src/electricity-server/server/database.cpp -o build/electricity-server/server/database.o
|
||||
mkdir -p bin
|
||||
g++ build/electricity-server/main.o build/electricity-server/server/api.o build/electricity-server/server/configuration.o build/electricity-server/server/database.o -lpistache -lsqlite3 -lstdc++fs -lspdlog -lfmt -o bin/electricity-server
|
||||
make: Leaving directory '/home/tijmen/project/home-data-collection-tools'
|
||||
|
||||
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"llvm-vs-code-extensions.vscode-clangd",
|
||||
]
|
||||
}
|
||||
79
.vscode/settings.json
vendored
Normal file
79
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"editor.detectIndentation": true,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 4,
|
||||
"files.associations": {
|
||||
"string": "cpp",
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"hash_map": "cpp",
|
||||
"bit": "cpp",
|
||||
"*.tcc": "cpp",
|
||||
"bitset": "cpp",
|
||||
"cctype": "cpp",
|
||||
"chrono": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"compare": "cpp",
|
||||
"concepts": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"csignal": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"forward_list": "cpp",
|
||||
"list": "cpp",
|
||||
"map": "cpp",
|
||||
"set": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"unordered_set": "cpp",
|
||||
"vector": "cpp",
|
||||
"exception": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"functional": "cpp",
|
||||
"iterator": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"random": "cpp",
|
||||
"ratio": "cpp",
|
||||
"regex": "cpp",
|
||||
"string_view": "cpp",
|
||||
"system_error": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"utility": "cpp",
|
||||
"fstream": "cpp",
|
||||
"future": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"istream": "cpp",
|
||||
"limits": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"numbers": "cpp",
|
||||
"ostream": "cpp",
|
||||
"semaphore": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"stop_token": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"thread": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"typeindex": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"valarray": "cpp",
|
||||
"variant": "cpp",
|
||||
"strstream": "cpp"
|
||||
}
|
||||
}
|
||||
829
.vscode/targets.log
vendored
Normal file
829
.vscode/targets.log
vendored
Normal file
@@ -0,0 +1,829 @@
|
||||
make all --print-data-base --no-builtin-variables --no-builtin-rules --question
|
||||
# GNU Make 4.3
|
||||
# Built for x86_64-pc-linux-gnu
|
||||
# Copyright (C) 1988-2020 Free Software Foundation, Inc.
|
||||
# License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
|
||||
# This is free software: you are free to change and redistribute it.
|
||||
# There is NO WARRANTY, to the extent permitted by law.
|
||||
|
||||
# Make data base, printed on Mon Jun 27 19:09:36 2022
|
||||
|
||||
# Variables
|
||||
|
||||
# environment
|
||||
GDK_BACKEND = x11
|
||||
# environment
|
||||
LC_ALL = C
|
||||
# environment
|
||||
NO_AT_BRIDGE = 1
|
||||
# environment
|
||||
GTK_RC_FILES = /etc/gtk/gtkrc:/home/tijmen/.gtkrc:/home/tijmen/.config/gtkrc
|
||||
# makefile (from 'makefile', line 16)
|
||||
MIGRATOR_CPPS = $(shell find src/migrator/ -name *.cpp)
|
||||
# makefile (from 'makefile', line 30)
|
||||
MIGRATOR_BINARY_PATH = bin/migrator
|
||||
# makefile (from 'makefile', line 9)
|
||||
SOLAR_LOGGER_OBJS = $(patsubst src/%.cpp, build/%.o, ${SOLAR_LOGGER_CPPS})
|
||||
# environment
|
||||
VSCODE_IPC_HOOK_EXTHOST = /run/user/1000/vscode-ipc-582970dc-f3ab-44e6-8729-662e27dd760f.sock
|
||||
# environment
|
||||
KONSOLE_DBUS_SERVICE = :1.81
|
||||
|
||||
# environment
|
||||
LC_NUMERIC = en_US.UTF-8
|
||||
# environment
|
||||
VSCODE_CWD = /home/tijmen/project/home-data-collection-tools
|
||||
# environment
|
||||
WINDOWID = 115343367
|
||||
# environment
|
||||
WINDOWPATH = 1
|
||||
# makefile (from 'makefile', line 17)
|
||||
MIGRATOR_OBJS = $(patsubst src/%.cpp, build/%.o, ${MIGRATOR_CPPS})
|
||||
# default
|
||||
MAKE_COMMAND := make
|
||||
# automatic
|
||||
@D = $(patsubst %/,%,$(dir $@))
|
||||
# makefile (from 'makefile', line 31)
|
||||
ELECT_LOGGER_BINARY_PATH = bin/electricity-logger
|
||||
# environment
|
||||
KONSOLE_VERSION = 220402
|
||||
# environment
|
||||
VSCODE_HANDLES_UNCAUGHT_ERRORS = true
|
||||
# default
|
||||
.VARIABLES :=
|
||||
# environment
|
||||
PWD = /home/tijmen/project/home-data-collection-tools
|
||||
# automatic
|
||||
%D = $(patsubst %/,%,$(dir $%))
|
||||
# environment
|
||||
MAIL = /var/spool/mail/tijmen
|
||||
# makefile (from 'makefile', line 24)
|
||||
ELECT_SRV_CPPS = $(shell find src/electricity-server/ -name *.cpp)
|
||||
# environment
|
||||
OLDPWD = /home/tijmen
|
||||
# environment
|
||||
KONSOLE_DBUS_WINDOW = /Windows/1
|
||||
# automatic
|
||||
^D = $(patsubst %/,%,$(dir $^))
|
||||
# makefile (from 'makefile', line 25)
|
||||
ELECT_SRV_OBJS = $(patsubst src/%.cpp, build/%.o, ${ELECT_SRV_CPPS})
|
||||
# environment
|
||||
VSCODE_LOG_STACK = false
|
||||
# automatic
|
||||
%F = $(notdir $%)
|
||||
# makefile (from 'makefile', line 5)
|
||||
SERVER_LFLAGS = -lpistache -lsqlite3 -lstdc++fs -lspdlog -lfmt
|
||||
# environment
|
||||
VSCODE_CODE_CACHE_PATH = /home/tijmen/.config/Code/CachedData/30d9c6cd9483b2cc586687151bcbcd635f373630
|
||||
# environment
|
||||
LANG = C
|
||||
# environment
|
||||
XAUTHORITY = /home/tijmen/.Xauthority
|
||||
# default
|
||||
.LOADED :=
|
||||
# default
|
||||
.INCLUDE_DIRS = /usr/include /usr/local/include /usr/include
|
||||
# environment
|
||||
COLORFGBG = 15;0
|
||||
# makefile
|
||||
MAKEFLAGS = pqrR
|
||||
# makefile
|
||||
CURDIR := /home/tijmen/project/home-data-collection-tools
|
||||
# environment
|
||||
VSCODE_PIPE_LOGGING = true
|
||||
# environment
|
||||
APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL = 1
|
||||
# automatic
|
||||
*D = $(patsubst %/,%,$(dir $*))
|
||||
# environment
|
||||
MFLAGS = -pqrR
|
||||
# default
|
||||
.SHELLFLAGS := -c
|
||||
# makefile (from 'makefile', line 3)
|
||||
ELECTRICITY_LOGGER_LFLAGS = -lsqlite3 -lspdlog -lfmt
|
||||
# makefile (from 'makefile', line 29)
|
||||
SOLAR_SRV_BINARY_PATH = bin/solar-server
|
||||
# environment
|
||||
XDG_CONFIG_DIRS = /home/tijmen/.config/kdedefaults:/etc/xdg
|
||||
# automatic
|
||||
+D = $(patsubst %/,%,$(dir $+))
|
||||
# environment
|
||||
XCURSOR_THEME = breeze_cursors
|
||||
# makefile (from 'build/electricity-logger/database.d', line 1)
|
||||
MAKEFILE_LIST := makefile build/electricity-logger/serialport.d build/electricity-logger/dsmr.d build/electricity-logger/main.d build/electricity-logger/database.d
|
||||
# automatic
|
||||
@F = $(notdir $@)
|
||||
# environment
|
||||
VSCODE_VERBOSE_LOGGING = true
|
||||
# environment
|
||||
VSCODE_PID = 2616
|
||||
# environment
|
||||
XDG_SESSION_TYPE = tty
|
||||
# automatic
|
||||
?D = $(patsubst %/,%,$(dir $?))
|
||||
# environment
|
||||
SESSION_MANAGER = local/ARCHDESKTOP:@/tmp/.ICE-unix/942,unix/ARCHDESKTOP:/tmp/.ICE-unix/942
|
||||
# automatic
|
||||
*F = $(notdir $*)
|
||||
# environment
|
||||
CHROME_DESKTOP = code-url-handler.desktop
|
||||
# environment
|
||||
DBUS_SESSION_BUS_ADDRESS = unix:path=/run/user/1000/bus
|
||||
# automatic
|
||||
<D = $(patsubst %/,%,$(dir $<))
|
||||
# makefile (from 'makefile', line 20)
|
||||
ELECT_LOGGER_CPPS = $(shell find src/electricity-logger/ -name *.cpp)
|
||||
# environment
|
||||
VSCODE_NLS_CONFIG = {"locale":"en-us","availableLanguages":{},"_languagePackSupport":true}
|
||||
# default
|
||||
MAKE_HOST := x86_64-pc-linux-gnu
|
||||
# makefile
|
||||
SHELL = /bin/sh
|
||||
# makefile (from 'makefile', line 13)
|
||||
SOLAR_SRV_OBJS = $(patsubst src/%.cpp, build/%.o, ${SOLAR_SRV_CPPS})
|
||||
# default
|
||||
MAKECMDGOALS := all
|
||||
# environment
|
||||
DOTNET_BUNDLE_EXTRACT_BASE_DIR = /home/tijmen/.cache/dotnet_bundle_extract
|
||||
# environment
|
||||
SHLVL = 4
|
||||
# environment
|
||||
PROFILEHOME =
|
||||
# environment
|
||||
MAKELEVEL := 0
|
||||
# default
|
||||
MAKE = $(MAKE_COMMAND)
|
||||
# environment
|
||||
PATH = /home/tijmen/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/home/tijmen/.dotnet/tools:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:~/.dotnet/tools:~/.dotnet/tools
|
||||
# makefile (from 'makefile', line 26)
|
||||
ELECT_SRV_DEPS = $(patsubst src/%.cpp, build/%.d, ${ELECT_SRV_CPPS})
|
||||
# default
|
||||
MAKEFILES :=
|
||||
# environment
|
||||
LANGUAGE = en_US:en_GB
|
||||
# environment
|
||||
MOTD_SHOWN = pam
|
||||
# automatic
|
||||
^F = $(notdir $^)
|
||||
# environment
|
||||
LC_TIME = nl_NL.UTF-8
|
||||
# makefile (from 'makefile', line 14)
|
||||
SOLAR_SRV_DEPS = $(patsubst src/%.cpp, build/%.d, ${SOLAR_SRV_CPPS})
|
||||
# environment
|
||||
INVOCATION_ID = 7964f389a987484a81d3bf63c69a1864
|
||||
# makefile (from 'makefile', line 28)
|
||||
SOLAR_LOGGER_BINARY_PATH = bin/solar-logger
|
||||
# environment
|
||||
VSCODE_LOG_NATIVE = false
|
||||
# automatic
|
||||
?F = $(notdir $?)
|
||||
# environment
|
||||
KDE_APPLICATIONS_AS_SCOPE = 1
|
||||
# environment
|
||||
XDG_SEAT = seat0
|
||||
# makefile (from 'makefile', line 32)
|
||||
ELECT_SRV_BINARY_PATH = bin/electricity-server
|
||||
# environment
|
||||
XDG_CURRENT_DESKTOP = KDE
|
||||
# automatic
|
||||
+F = $(notdir $+)
|
||||
# environment
|
||||
ORIGINAL_XDG_CURRENT_DESKTOP = KDE
|
||||
# 'override' directive
|
||||
GNUMAKEFLAGS :=
|
||||
# makefile (from 'makefile', line 4)
|
||||
SOLAR_LOGGER_LFLAGS = -lsqlite3 -lcurl
|
||||
# environment
|
||||
LOGNAME = tijmen
|
||||
# makefile (from 'makefile', line 37)
|
||||
SOLAR_TEST_DATABASE = solarpaneloutput.db
|
||||
# environment
|
||||
XDG_VTNR = 1
|
||||
# makefile
|
||||
.DEFAULT_GOAL := all
|
||||
# environment
|
||||
SYSTEMD_EXEC_PID = 994
|
||||
# makefile (from 'makefile', line 8)
|
||||
SOLAR_LOGGER_CPPS = $(shell find src/solar-logger/ -name *.cpp)
|
||||
# environment
|
||||
DISPLAY = :0
|
||||
# environment
|
||||
USER = tijmen
|
||||
# makefile (from 'makefile', line 6)
|
||||
MIGRATOR_LFLAGS = -lsqlite3
|
||||
# default
|
||||
MAKE_VERSION := 4.3
|
||||
# environment
|
||||
KDE_SESSION_UID = 1000
|
||||
# 'override' directive
|
||||
.SHELLSTATUS := 0
|
||||
# environment
|
||||
KONSOLE_DBUS_SESSION = /Sessions/1
|
||||
# environment
|
||||
MANAGERPID = 848
|
||||
# environment
|
||||
DEBUGINFOD_URLS = https://debuginfod.archlinux.org
|
||||
# environment
|
||||
_ = /usr/bin/make
|
||||
# makefile (from 'makefile', line 22)
|
||||
ELECT_LOGGER_DEPS = $(patsubst src/%.cpp, build/%.d, ${ELECT_LOGGER_CPPS})
|
||||
# environment
|
||||
XDG_RUNTIME_DIR = /run/user/1000
|
||||
# makefile (from 'makefile', line 2)
|
||||
CFLAGS = -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD
|
||||
# makefile (from 'makefile', line 10)
|
||||
SOLAR_LOGGER_DEPS = $(patsubst src/%.cpp, build/%.d, ${SOLAR_LOGGER_CPPS})
|
||||
# environment
|
||||
COLORTERM = truecolor
|
||||
# environment
|
||||
LC_CTYPE = en_US.UTF-8
|
||||
# environment
|
||||
JOURNAL_STREAM = 8:22271
|
||||
# environment
|
||||
XDG_SESSION_CLASS = user
|
||||
# makefile (from 'makefile', line 34)
|
||||
INSTALL_PATH = /usr/local/bin/
|
||||
# environment
|
||||
VSCODE_AMD_ENTRYPOINT = vs/workbench/api/node/extensionHostProcess
|
||||
# environment
|
||||
GTK2_RC_FILES = /etc/gtk-2.0/gtkrc:/home/tijmen/.gtkrc-2.0:/home/tijmen/.config/gtkrc-2.0
|
||||
# environment
|
||||
HOME = /home/tijmen
|
||||
# environment
|
||||
ELECTRON_RUN_AS_NODE = 1
|
||||
# environment
|
||||
VSCODE_IPC_HOOK = /run/user/1000/vscode-a4fa6f01-1.68.1-main.sock
|
||||
# environment
|
||||
TERM = xterm-256color
|
||||
# makefile (from 'makefile', line 18)
|
||||
MIGRATOR_DEPS = $(patsubst src/%.cpp, build/%.d, ${MIGRATOR_CPPS})
|
||||
# environment
|
||||
XDG_SESSION_ID = 1
|
||||
# makefile (from 'makefile', line 12)
|
||||
SOLAR_SRV_CPPS = $(shell find src/solar-server/ -name *.cpp)
|
||||
# environment
|
||||
XCURSOR_SIZE = 24
|
||||
# default
|
||||
.RECIPEPREFIX :=
|
||||
# automatic
|
||||
<F = $(notdir $<)
|
||||
# default
|
||||
SUFFIXES :=
|
||||
# environment
|
||||
QT_AUTO_SCREEN_SCALE_FACTOR = 0
|
||||
# environment
|
||||
VSCODE_CLI = 1
|
||||
# environment
|
||||
ELECTRON_NO_ATTACH_CONSOLE = 1
|
||||
# environment
|
||||
SHELL_SESSION_ID = 4be1cedc5cde42dc8e443d540fd6a96c
|
||||
# environment
|
||||
KDE_SESSION_VERSION = 5
|
||||
# default
|
||||
.FEATURES := target-specific order-only second-expansion else-if shortest-stem undefine oneshell nocomment grouped-target extra-prereqs archives jobserver output-sync check-symlink guile load
|
||||
# environment
|
||||
INSTALL4J_JAVA_HOME = /usr/lib/jvm/java-15-adoptopenjdk
|
||||
# makefile (from 'makefile', line 1)
|
||||
CC = g++
|
||||
# makefile (from 'makefile', line 21)
|
||||
ELECT_LOGGER_OBJS = $(patsubst src/%.cpp, build/%.o, ${ELECT_LOGGER_CPPS})
|
||||
# environment
|
||||
KDE_FULL_SESSION = true
|
||||
# environment
|
||||
DOTNET_ROOT = /usr/share/dotnet
|
||||
# variable set hash-table stats:
|
||||
# Load=140/1024=14%, Rehash=0, Collisions=23/264=9%
|
||||
|
||||
# Pattern-specific Variable Values
|
||||
|
||||
# No pattern-specific variable values.
|
||||
|
||||
# Directories
|
||||
|
||||
# src (device 66306, inode 41815769): No files, no impossibilities so far.
|
||||
# . (device 66306, inode 20840561): 17 files, no impossibilities.
|
||||
# src/solar-logger (device 66306, inode 42734060): 2 files, no impossibilities so far.
|
||||
|
||||
# 19 files, no impossibilities in 3 directories.
|
||||
|
||||
# Implicit Rules
|
||||
|
||||
build/%.o: src/%.cpp
|
||||
# recipe to execute (from 'makefile', line 102):
|
||||
mkdir -p ${@D}
|
||||
${CC} ${CFLAGS} -c $< -o $@
|
||||
|
||||
# 1 implicit rules, 0 (0.0%) terminal.
|
||||
# Files
|
||||
|
||||
# Not a target:
|
||||
src/electricity-logger/database.cpp:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/electricity-server/server/configuration.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
bin/solar-logger: build/solar-logger/main.o build/solar-logger/zeverdata.o build/solar-logger/database.o
|
||||
# Implicit rule search has not been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Needs to be updated (-q is set).
|
||||
# variable set hash-table stats:
|
||||
# Load=0/32=0%, Rehash=0, Collisions=0/6=0%
|
||||
# recipe to execute (from 'makefile', line 82):
|
||||
mkdir -p ${@D}
|
||||
${CC} $^ ${SOLAR_LOGGER_LFLAGS} -o $@
|
||||
|
||||
# Not a target:
|
||||
build/solar-server/configuration.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
check-electricity-server: bin/electricity-server
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 54):
|
||||
./${ELECT_SRV_BINARY_PATH} -d /var/data0/electricity -p 8081 -s valkendaal.duckdns.org
|
||||
|
||||
bin/electricity-logger: build/electricity-logger/serialport.o build/electricity-logger/dsmr.o build/electricity-logger/main.o build/electricity-logger/database.o
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 94):
|
||||
mkdir -p ${@D}
|
||||
${CC} $^ ${ELECTRICITY_LOGGER_LFLAGS} -o $@
|
||||
|
||||
# Not a target:
|
||||
build/electricity-server/server/api.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/electricity-server/server/configuration.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/solar-server/configuration.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
build/migrator/main.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
build/migrator/migrations/epochtodatetime.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/solar-logger/database.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
build/electricity-logger/dsmr.o: src/electricity-logger/dsmr.cpp include/electricity/logger/dsmr.hpp
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
src/electricity-logger/serialport.cpp:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/electricity-server/main.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/solar-server/api.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
check-solar-server: bin/solar-server solarpaneloutput.db
|
||||
|
||||
# Phony target (prerequisite of .PHONY).
|
||||
# Implicit rule search has not been done.
|
||||
# File does not exist.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 48):
|
||||
./${SOLAR_SRV_BINARY_PATH} -d ${SOLAR_TEST_DATABASE} -c public/ -p 8080
|
||||
|
||||
bin/solar-server: build/solar-server/database/connection.o build/solar-server/database/database.o build/solar-server/api.o build/solar-server/main.o build/solar-server/configuration.o
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 86):
|
||||
mkdir -p ${@D}
|
||||
${CC} $^ ${SERVER_LFLAGS} -o $@
|
||||
|
||||
build/electricity-logger/database.o: src/electricity-logger/database.cpp include/electricity/logger/database.hpp include/electricity/logger/dsmr.hpp include/util/date.hpp
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
include/electricity/logger/serialport.hpp:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/migrator/main.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
build/electricity-logger/main.o: src/electricity-logger/main.cpp include/cxxopts.hpp include/electricity/logger/database.hpp include/electricity/logger/dsmr.hpp include/electricity/logger/serialport.hpp
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
include/cxxopts.hpp:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
install-loggers: bin/solar-logger bin/electricity-logger
|
||||
# Phony target (prerequisite of .PHONY).
|
||||
# Implicit rule search has not been done.
|
||||
# File does not exist.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 59):
|
||||
cp -uv ${SOLAR_LOGGER_BINARY_PATH} ${INSTALL_PATH}
|
||||
cp -uv ${ELECT_LOGGER_BINARY_PATH} ${INSTALL_PATH}
|
||||
|
||||
# Not a target:
|
||||
build/solar-server/database/connection.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
build/electricity-logger/serialport.o: src/electricity-logger/serialport.cpp include/electricity/logger/serialport.hpp
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
clean:
|
||||
# Phony target (prerequisite of .PHONY).
|
||||
# Implicit rule search has not been done.
|
||||
# File does not exist.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 42):
|
||||
-rm -rf bin/* build/*
|
||||
|
||||
# Not a target:
|
||||
build/solar-server/main.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
src/electricity-logger/dsmr.cpp:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
install-electricity-server: bin/electricity-server
|
||||
# Phony target (prerequisite of .PHONY).
|
||||
# Implicit rule search has not been done.
|
||||
# File does not exist.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 66):
|
||||
cp -uv ${ELECT_SRV_BINARY_PATH} ${INSTALL_PATH}
|
||||
|
||||
# Not a target:
|
||||
build/migrator/migrations/updatesummarytable.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
src/electricity-logger/main.cpp:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/electricity-server/server/api.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
build/electricity-server/server/database.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
src/solar-logger/main.cpp:
|
||||
# Implicit rule search has been done.
|
||||
# Last modified 2021-10-29 21:40:37.24067233
|
||||
# File has been updated.
|
||||
# Successfully updated.
|
||||
|
||||
# Not a target:
|
||||
include/electricity/logger/database.hpp:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/migrator/transaction.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/solar-logger/zeverdata.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
build/solar-logger/database.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
.DEFAULT:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
all: bin/solar-logger bin/solar-server bin/migrator bin/electricity-logger bin/electricity-server
|
||||
# Phony target (prerequisite of .PHONY).
|
||||
# Command line target.
|
||||
# Implicit rule search has not been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Needs to be updated (-q is set).
|
||||
# variable set hash-table stats:
|
||||
# Load=0/32=0%, Rehash=0, Collisions=0/6=0%
|
||||
|
||||
install-solar-server: bin/solar-server
|
||||
# Phony target (prerequisite of .PHONY).
|
||||
# Implicit rule search has not been done.
|
||||
# File does not exist.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 63):
|
||||
cp -uv ${SOLAR_SRV_BINARY_PATH} ${INSTALL_PATH}
|
||||
|
||||
# Not a target:
|
||||
build/solar-server/api.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
build/solar-logger/zeverdata.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/electricity-server/server/database.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/migrator/migrations/updatesummarytable.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
build/electricity-logger/dsmr.d:
|
||||
# Implicit rule search has been done.
|
||||
# Last modified 2022-06-15 22:19:00.047060852
|
||||
# File has been updated.
|
||||
# Successfully updated.
|
||||
|
||||
# Not a target:
|
||||
build/migrator/migrations/epochtodatetime.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
include/util/date.hpp:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
check-solar-logger: bin/solar-logger solarpaneloutput.db
|
||||
# Phony target (prerequisite of .PHONY).
|
||||
# Implicit rule search has not been done.
|
||||
# File does not exist.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 45):
|
||||
./${SOLAR_LOGGER_BINARY_PATH} -d ${SOLAR_TEST_DATABASE} -u "http://192.168.2.26/home.cgi"
|
||||
|
||||
# Not a target:
|
||||
build/solar-server/database/database.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
build/solar-server/main.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
makefile:
|
||||
# Implicit rule search has been done.
|
||||
# Last modified 2022-06-15 22:18:56.077059679
|
||||
# File has been updated.
|
||||
# Successfully updated.
|
||||
|
||||
install: install-loggers install-servers
|
||||
# Phony target (prerequisite of .PHONY).
|
||||
# Implicit rule search has not been done.
|
||||
# File does not exist.
|
||||
# File has not been updated.
|
||||
|
||||
bin/migrator: build/migrator/migrations/updatesummarytable.o build/migrator/migrations/epochtodatetime.o build/migrator/main.o build/migrator/transaction.o
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 90):
|
||||
mkdir -p ${@D}
|
||||
${CC} $^ ${MIGRATOR_LFLAGS} -o $@
|
||||
|
||||
# Not a target:
|
||||
build/electricity-logger/database.d:
|
||||
# Implicit rule search has been done.
|
||||
# Last modified 2022-06-15 22:19:12.810397961
|
||||
# File has been updated.
|
||||
# Successfully updated.
|
||||
|
||||
solarpaneloutput.db:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 79):
|
||||
./script/createdb.sh ${SOLAR_TEST_DATABASE}
|
||||
|
||||
# Not a target:
|
||||
build/solar-logger/main.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
include/electricity/logger/dsmr.hpp:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/electricity-server/main.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
# Not a target:
|
||||
build/migrator/transaction.d:
|
||||
# Implicit rule search has been done.
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Failed to be updated.
|
||||
|
||||
bin/electricity-server: build/electricity-server/main.o build/electricity-server/server/api.o build/electricity-server/server/configuration.o build/electricity-server/server/database.o
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 98):
|
||||
mkdir -p ${@D}
|
||||
${CC} $^ ${SERVER_LFLAGS} -o $@
|
||||
|
||||
.PHONY: all clean check-solar-logger check-solar-server install install-loggers install-solar-server install-electricity-server install-servers
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/electricity-logger/serialport.d:
|
||||
# Implicit rule search has been done.
|
||||
# Last modified 2022-06-15 22:18:59.477060683
|
||||
# File has been updated.
|
||||
# Successfully updated.
|
||||
|
||||
# Not a target:
|
||||
build/electricity-logger/main.d:
|
||||
# Implicit rule search has been done.
|
||||
# Last modified 2022-06-25 22:16:39.877980958
|
||||
# File has been updated.
|
||||
# Successfully updated.
|
||||
|
||||
build/solar-logger/main.o: src/solar-logger/main.cpp
|
||||
# Implicit rule search has been done.
|
||||
# Implicit/static pattern stem: 'solar-logger/main'
|
||||
# File does not exist.
|
||||
# File has been updated.
|
||||
# Needs to be updated (-q is set).
|
||||
# automatic
|
||||
# @ := build/solar-logger/main.o
|
||||
# automatic
|
||||
# * := solar-logger/main
|
||||
# automatic
|
||||
# < := src/solar-logger/main.cpp
|
||||
# automatic
|
||||
# + := src/solar-logger/main.cpp
|
||||
# automatic
|
||||
# % :=
|
||||
# automatic
|
||||
# ^ := src/solar-logger/main.cpp
|
||||
# automatic
|
||||
# ? := src/solar-logger/main.cpp
|
||||
# automatic
|
||||
# | :=
|
||||
# variable set hash-table stats:
|
||||
# Load=8/32=25%, Rehash=0, Collisions=2/17=12%
|
||||
# recipe to execute (from 'makefile', line 102):
|
||||
mkdir -p ${@D}
|
||||
${CC} ${CFLAGS} -c $< -o $@
|
||||
|
||||
check-electricity-logger: bin/electricity-logger
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
# recipe to execute (from 'makefile', line 51):
|
||||
./${ELECT_LOGGER_BINARY_PATH} -d /dev/ttyUSB0 -c additional_column_value
|
||||
|
||||
# Not a target:
|
||||
build/solar-server/database/database.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
# Not a target:
|
||||
build/solar-server/database/connection.o:
|
||||
# Implicit rule search has not been done.
|
||||
# Modification time never checked.
|
||||
# File has not been updated.
|
||||
|
||||
install-servers: install-solar-server install-electricity-server
|
||||
# Phony target (prerequisite of .PHONY).
|
||||
# Implicit rule search has not been done.
|
||||
# File does not exist.
|
||||
# File has not been updated.
|
||||
|
||||
# files hash-table stats:
|
||||
# Load=71/1024=7%, Rehash=0, Collisions=9/178=5%
|
||||
# VPATH Search Paths
|
||||
|
||||
# No 'vpath' search paths.
|
||||
|
||||
# No general ('VPATH' variable) search path.
|
||||
|
||||
# strcache buffers: 1 (0) / strings = 94 / storage = 2263 B / avg = 24 B
|
||||
# current buf: size = 8162 B / used = 2263 B / count = 94 / avg = 24 B
|
||||
|
||||
# strcache performance: lookups = 180 / hit rate = 47%
|
||||
# hash-table stats:
|
||||
# Load=94/8192=1%, Rehash=0, Collisions=2/180=1%
|
||||
# Finished Make data base on Mon Jun 27 19:09:36 2022
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
Copyright (c) 2022 Tijmen van Nesselrooij
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
51
README.md
51
README.md
@@ -1,3 +1,50 @@
|
||||
# home-data-collection-tools
|
||||
# Home Data Collection Tools
|
||||
|
||||
A collection of tools to log collected data from a Zeverlution smart inverter and DSMR protocol supporting meters.
|
||||
A mono repository containing all the homebrew collectors and REST APIs running on the local server. See it in action [here](https://valkendaal.duckdns.org).
|
||||
|
||||
## Contents
|
||||
|
||||
This repository houses various projects. All are listed below.
|
||||
|
||||
- The Zeverlution S3000 logger, [documentation](docs/SOLAR_LOGGER.md)
|
||||
- The Landis Gyr E350 logger, [documentation](docs/ELECTRICITY_LOGGER.md)
|
||||
- Two pistache REST HTTP APIs, [documentation](docs/SERVER.md)
|
||||
- A solar panel log server, [documentation](docs/SOLAR_API.md)
|
||||
- An electricity log server, [documentation](docs/ELECTRICITY_API.md)
|
||||
- A migrator to convert and build data structures yielded by the loggers, [documentation](docs/MIGRATOR.md)
|
||||
|
||||
## Project Directory
|
||||
|
||||
|- `.vscode` => A folder with Visual Studio Code IDE specific files, for developing this project </br>
|
||||
|- `docs` => A folder housing all the documentation files (`*.md`) </br>
|
||||
|- `include` => A folder housing all the header files used by the source files in `src/` </br>
|
||||
|- `script` => A folder with handy bash scripts to create and migrate databases </br>
|
||||
|- `src` => A folder with all the source files of the different projects that require compilation </br>
|
||||
|....|- `electricity-logger` => The Landis Gyr E350 logger source files </br>
|
||||
|....|- `electricity-server` => The pistache REST API sources for serving the electricity logs </br>
|
||||
|....|- `migrator ` => The migrator source files </br>
|
||||
|....|- `solar-logger` => The Zeverlution S3000 logger source files </br>
|
||||
|....|- `solar-server` => The pistache REST API sources for serving the solar panel logs </br>
|
||||
|- `systemd` => A folder with example systemd service files for the servers </br>
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
A few benchmarks have been done for the solar-server project, which can be found [here](docs/BENCHMARK.md).
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime (server)
|
||||
|
||||
- Curl: the multiprotocol file tranfser library, [website](https://curl.haxx.se/libcurl/)
|
||||
- cxxopts, [website](https://github.com/jarro2783/cxxopts/tree/v3.0.0)
|
||||
- Docker CE, [website](https://www.docker.com/)
|
||||
- GNU Make, [website](https://www.gnu.org/software/make/)
|
||||
- Pistache: an elegant C++ REST framework, [website](http://pistache.io/)
|
||||
- Spdlog, [website](https://github.com/gabime/spdlog)
|
||||
- Sqlite3: a small SQL database engine, [website](https://www.sqlite.org/index.html)
|
||||
|
||||
### Development
|
||||
|
||||
In addition to all the runtime dependencies the following dependencies are required for development:
|
||||
|
||||
- A C++20 compatible compiler
|
||||
|
||||
BIN
bin/electricity-logger
Executable file
BIN
bin/electricity-logger
Executable file
Binary file not shown.
6
compile_flags.txt
Normal file
6
compile_flags.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
-xc++
|
||||
-Wall
|
||||
-Wextra
|
||||
-DSPDLOG_FMT_EXTERNAL
|
||||
-Iinclude/
|
||||
-std=c++17
|
||||
75
docs/BENCHMARK.md
Normal file
75
docs/BENCHMARK.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# solar-server Benchmarks
|
||||
|
||||
Comparing a NodeJS REST API using ExpressJS and a file based logging solution against a Pistache REST API using an Sqlite3 logging solution. Measurements are made with Firefox, server is a Raspberry Pi 3 with all content stored on a microsd card (slow!). See the conclusion at the end if you want a TLDR;.
|
||||
|
||||
## Raspberry Pi Model 3 B Benchmarks
|
||||
Iteration #1: Fetching stuff from the database on a day by day basis (1 day = 1 query)
|
||||
|
||||
| Operation | NodeJS | Pistache | Difference |
|
||||
|-----------|-------:|---------:|-----------:|
|
||||
| Day | 11ms | 6ms | -5ms |
|
||||
| Month | 38ms | 80ms | +80ms |
|
||||
| Year | 2122ms | 3353ms | +1231ms |
|
||||
|
||||
Iteration #2: Fetching stuff from the database in a single query for all requests
|
||||
+ Index on DateTimeUtc
|
||||
|
||||
| Operation | NodeJS | Pistache | Difference |
|
||||
|-----------|-------:|---------:|-----------:|
|
||||
| Day | 12ms | 16ms | +4ms |
|
||||
| Month | 40ms | 127ms | +87ms |
|
||||
| Year | 1281ms | 6143ms | +4942ms |
|
||||
|
||||
Iteration #3: Tweaks
|
||||
+ Index on KilowattHour
|
||||
+ Reindex
|
||||
|
||||
**No difference**
|
||||
|
||||
Iteration #4: Split DateTimeUtc into separate TEXT columns
|
||||
|
||||
| Operation | NodeJS | Pistache | Difference |
|
||||
|-----------|-------:|---------:|-----------:|
|
||||
| Day | 10ms | 18ms | +8ms |
|
||||
| Month | 34ms | 45ms | +11ms |
|
||||
| Year | 1151ms | 2110ms | +959ms |
|
||||
|
||||
## Pentium G5400 Benchmarks
|
||||
|
||||
Iteration #5: Hardware change (Raspberry Pi 3+ to Pentium G5400)
|
||||
|
||||
| Operation | NodeJS | Pistache | Difference |
|
||||
|-----------|-------:|---------:|-----------:|
|
||||
| Day | - | 3ms | - |
|
||||
| Month | - | 5ms | - |
|
||||
| Year | - | 83ms | - |
|
||||
|
||||
Iteration #6: Keep a separate summary table
|
||||
|
||||
| Operation | NodeJS | Pistache | Difference |
|
||||
|-----------|-------:|---------:|-----------:|
|
||||
| Day | - | 3ms | - |
|
||||
| Month | - | 1ms | - |
|
||||
| Year | - | 4ms | - |
|
||||
|
||||
Comparison to the raspberry pi running the same software:
|
||||
|
||||
| Operation | RPi | Pentium | Difference |
|
||||
|-----------|-------:|---------:|-----------:|
|
||||
| Day | 14ms | 3ms | -11ms |
|
||||
| Month | 5ms | 1ms | -4ms |
|
||||
| Year | 16ms | 4ms | -12ms |
|
||||
|
||||
## Conclusion
|
||||
|
||||
Clarification:
|
||||
- All platforms run Debian Buster
|
||||
- RPi = Raspbery Pi Model 3 B+
|
||||
- NodeJS = NodeJS Express.js server serving the solar logs stored as file per day (/var/log/solar/[yyyy]/[mm_dd].txt)
|
||||
- Pistache = Pistache server serving the solar logs stored in a SQLite3 database
|
||||
|
||||
| Operation | RPi NodeJS | RPi Pistache | Pentium G5400 Pistache |
|
||||
|-----------|-----------:|-------------:|-----------------------:|
|
||||
| Day | 11ms | 14ms | 3ms |
|
||||
| Month | 38ms | 5ms | 1ms |
|
||||
| Year | 2122ms | 16ms | 4ms |
|
||||
37
docs/ELECTRICITY_API.md
Normal file
37
docs/ELECTRICITY_API.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# The electricity-server REST API
|
||||
|
||||
This project depends on the `pistache` HTTP framework, see their [website](http://pistache.io/) for more info.
|
||||
|
||||
## Endpoints
|
||||
All endpoints respond in JSON. Examples can be found in the Examples section. Dates are always in ISO format, meaning they take the shape of `year-month-day` with leading zeroes for month and day. Example: `2019-01-11` is the eleventh day of January, 2019.
|
||||
|
||||
- `/day?start=[yyyy-MM-dd]` This tells the server what date to serve all the collected records from the specified day.
|
||||
- `/day?start=[yyyy-MM-dd]&stop=[yyyy-MM-dd]` This tells the server what date range to serve a per day summary for.
|
||||
|
||||
### Example Response /day?start=2020-01-10
|
||||
Will give you as many objects as there are recorded values for the 10th of January, 2020. Ordered from earliest to latest. If there are no records for the given day an empty array is returned.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"dateTime":1578614401,
|
||||
"totalPowerUse":3282.95,
|
||||
"totalPowerReturn":2790.88,
|
||||
"totalGasUse":3769.23
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### Example Response /day?start=2020-01-01&stop=2020-01-19
|
||||
Will give you a summary for each recorded day in the range 1st of January 2020 to the 19th of January 2020. If no logs exist for the day on the server it will send an object with a valid dateTime for that day but with 0 values for the other fields.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"dateTime":1578614401,
|
||||
"totalPowerUse": 8.324,
|
||||
"totalPowerReturn": 3.62,
|
||||
"totalGasUse": 11.05
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
24
docs/ELECTRICITY_LOGGER.md
Normal file
24
docs/ELECTRICITY_LOGGER.md
Normal file
@@ -0,0 +1,24 @@
|
||||
## About
|
||||
|
||||
This small tool is meant for logging the electricity values from the `Landis Gyr E350` electricity meter. As a bonus it also stores the gas concumption values.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
directoryPath="/var/log/electricity/$(date +%Y/)"
|
||||
if ! [ -d "$directoryPath" ]; then
|
||||
mkdir -p "$directoryPath"
|
||||
fi
|
||||
|
||||
outputFile="$directoryPath$(date +%m_%d.txt)"
|
||||
if ! [ -f "$outputFile" ]; then
|
||||
touch "$outputFile"
|
||||
fi
|
||||
|
||||
datestr="$(date +%Y-%m-%dT%T)"
|
||||
electricity-logger -c "$datestr" /dev/ttyUSB0 >> "$outputFile"
|
||||
```
|
||||
|
||||
Use the `--help` switch for more information about the possible arguments `electricity-logger` can take.
|
||||
43
docs/SERVER.md
Normal file
43
docs/SERVER.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Serving Data
|
||||
|
||||
This project includes to data servers: the solar server and electricity server. Both host the data collected by their respective logger counterparts.
|
||||
|
||||
## API Description
|
||||
|
||||
[The solar api description](./SOLAR_API.md)
|
||||
[The electricity api description](./ELECTRICITY_API.md)
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
It is recommended to use a reverse proxy setup to make all servers and content reachable through standard HTTP(S) ports. When using nginx something like this suffices, using the solar server as example:
|
||||
```
|
||||
server {
|
||||
server_name solar.valkendaal.duckdns.org;
|
||||
location / {
|
||||
if ($request_method = 'GET') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET';
|
||||
}
|
||||
proxy_pass http://localhost:3001;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The additional if statement within the location scope allows fetching of solar resources by any other domain. This is used to display data exposed by the solar server on the website (`public/`).
|
||||
|
||||
## Restricting Access
|
||||
|
||||
The electricity server exposes sensitive data, hence it shouldn't be accessible to anyone except users of the household. The easy solution here is to verify the requestee's IP address, since anyone making the request from the household itself should share the same external IP address, including the server. Therefore the electricity server checks the requestee's IP address against its own external one (by resolving the given domain parameter argument to an IP address).
|
||||
|
||||
For this a little hackery was necessary in the reverse proxy, since it normally makes the request to the electricity server on its own behalf. This would result in all requests originating from 127.0.0.1 (localhost). To solve this the following line was added before the `proxy_pass` directive:
|
||||
```
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
```
|
||||
|
||||
The value of the X-Real-IP HTTP header is then used by the electricity server to validate against the domain resolved IP address. If they match it means the request came from the same network as the server.
|
||||
|
||||
This obviously is not watertight, but it serves the purpose well enough and avoids having to lay down more complicated authorization infrastructure.
|
||||
|
||||
## Launch Parameters
|
||||
|
||||
Both servers use TCLAP for their launch parameters. Simply run either executable without any parameters or use the `--help` switch to get all available commands.
|
||||
42
docs/SOLAR_API.md
Normal file
42
docs/SOLAR_API.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# The solar-server REST API
|
||||
|
||||
This project depends on the `pistache` HTTP framework, see their [website](http://pistache.io/) for more info.
|
||||
|
||||
## Endpoints
|
||||
All endpoints respond in JSON. Examples can be found in the Examples section. Dates are always in ISO format, meaning they take the shape of `year-month-day` with leading zeroes for month and day. Example: `2019-01-11` is the eleventh day of January, 2019.
|
||||
|
||||
- `/day?start=[yyyy-MM-dd]` This tells the server what date to serve all the collected records from the specified day.
|
||||
- `/day?start=[yyyy-MM-dd]&stop=[yyyy-MM-dd]` This tells the server what date to serve the highest recorded kilowatt hour collected for each day in the specified date range. Stop date is inclusive.
|
||||
- `/month?&start=[yyyy-mm]&stop=[yyyy-mm]` This returns the total kilowatthours generated for a given month. The stop month is inclusive.
|
||||
|
||||
## Example Response
|
||||
All times are always in UTC. For these examples the current date is 25 December, 2019.
|
||||
|
||||
### /day?start=2019-02-15
|
||||
Will give you as many objects as there are recorded values for the 15th of Febuary, 2019. Ordered from earliest to latest.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"time": 1550226776,
|
||||
"watt": 1456,
|
||||
"kwh": 14.53
|
||||
},
|
||||
{
|
||||
"time": 1550226834,
|
||||
"watt": 1456,
|
||||
"kwh": 14.54
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### /month?start=2019-06&stop=2019-06
|
||||
Will give you a total kilowatt hours generated for the month July, 2019. The time component is the first day of the month at 00:00 (midnight) UTC. For multiple months the result is ordered earliest to latest.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"time": 1559347200,
|
||||
"watt": 0,
|
||||
"kwh": 451.64
|
||||
}
|
||||
]
|
||||
22
docs/SOLAR_LOGGER.md
Normal file
22
docs/SOLAR_LOGGER.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# About
|
||||
This software targets linux and has been tested on both x86_64 and armv7 hardware.
|
||||
|
||||
## solar-logger
|
||||
Simple data collecting program for ZeverSolar's Zeverlution Sxxxx "smart" inverters with a network interface (referred to as "combox" by the manufacturer). It collects all the exposed data of the Zeverlution, including current power generation (watts) and today's cummulative power production (kilowatt/hours).
|
||||
|
||||
### Strange output
|
||||
Zeverlution Smart Inverters currently have a bug that causes leading zeroes in a decimal number to be ignored. Concretely this means that the value 0.01 becomes 0.10, 3.09 becomes 3.9, etcetera. This is causing strange peaks in the logged data where sequences go from 2.80 to 2.90 to 2.10.
|
||||
|
||||
Another bug is the inverter turning itself off and back on again throughout the day. This happens if the yield get too low. This will cause the cumulative power to reset to zero (kilowatt per hour).
|
||||
|
||||
# Building
|
||||
## Dependencies
|
||||
### solar-logger
|
||||
Dependencies for this program are:
|
||||
- `libcurl` which can be installed using your package manager. It has been tested with the `openSSL` flavour, libcurl version 4.
|
||||
- `sqlite3` which is sometimes also referred to as `libsqlite3-dev`.
|
||||
|
||||
## Building
|
||||
The logger program can be build by running the makefile (`make all`) in the project root. It will create a `bin` folder where it puts the newly created binaries. Move these to wherever you keep your binaries or use the `install` make target.
|
||||
|
||||
Refer to the `--help` switch to find out more about supported launch parameters.
|
||||
2086
include/cxxopts.hpp
Normal file
2086
include/cxxopts.hpp
Normal file
File diff suppressed because it is too large
Load Diff
15
include/electricity/logger/database.hpp
Normal file
15
include/electricity/logger/database.hpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <electricity/logger/dsmr.hpp>
|
||||
#include <sqlite3.h>
|
||||
#include <string>
|
||||
|
||||
class Database {
|
||||
private:
|
||||
sqlite3 * connectionPtr;
|
||||
|
||||
public:
|
||||
void Insert(DSMR::Data const & data, time_t const time);
|
||||
|
||||
Database(std::string const & databaseFilePath);
|
||||
~Database();
|
||||
};
|
||||
73
include/electricity/logger/dsmr.hpp
Normal file
73
include/electricity/logger/dsmr.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
//#include <iostream> // debug
|
||||
|
||||
namespace DSMR
|
||||
{
|
||||
enum class LineTag
|
||||
{
|
||||
Unknown = -1,
|
||||
DSMRversion,
|
||||
DateTimeStamp,
|
||||
SerialNo,
|
||||
TotalPowerConsumedTariff1,
|
||||
TotalPowerConsumedTariff2,
|
||||
TotalReturnedPowerTariff1,
|
||||
TotalReturnedPowerTariff2,
|
||||
CurrentTarif,
|
||||
CurrentPowerConsumption,
|
||||
CurrentPowerReturn,
|
||||
PowerFailureCount,
|
||||
PowerFailureLongCount,
|
||||
PowerFailureEventLog,
|
||||
VoltageL1SagCount,
|
||||
VoltageL2SagCount,
|
||||
VoltageL3SagCount,
|
||||
VoltageL1SwellCount,
|
||||
VoltageL2SwellCount,
|
||||
VoltageL3SwellCount,
|
||||
TextMessageMaxChar,
|
||||
InstantL1VoltageResolution,
|
||||
InstantL2VoltageResolution,
|
||||
InstantL3VoltageResolution,
|
||||
InstantL1CurrentResolution,
|
||||
InstantL2CurrentResolution,
|
||||
InstantL3CurrentResolution,
|
||||
InstantL1ActivePowerResolution,
|
||||
InstantL2ActivePowerResolution,
|
||||
InstantL3ActivePowerResolution,
|
||||
InstantL1ActivePowerResolutionA,
|
||||
InstantL2ActivePowerResolutionA,
|
||||
InstantL3ActivePowerResolutionA,
|
||||
DeviceType,
|
||||
GasDeviceIdentifier,
|
||||
GasTotalConsumptionLog
|
||||
};
|
||||
|
||||
// DSMR output parser for Landis Gyr E350, we only extract things that interest us.
|
||||
class Data {
|
||||
private:
|
||||
static const std::unordered_map<std::string, LineTag> & GetMap();
|
||||
|
||||
static void RemoveUnit(std::string & value);
|
||||
|
||||
// Single argument lines only
|
||||
static std::pair<std::string, std::string> GetKeyValuePair(const std::string & line);
|
||||
|
||||
public:
|
||||
double currentPowerUsageKw = 0.0, currentPowerReturnKw = 0.0;
|
||||
double totalPowerConsumptionDayKwh = 0.0, totalPowerConsumptionNightKwh = 0.0;
|
||||
double totalPowerReturnedDayKwh = 0.0, totalPowerReturnedNightKwh = 0.0;
|
||||
bool usingDayTarif = true;
|
||||
std::string gasTimestamp = "";
|
||||
double gasConsumptionCubicMeters = 0.0; // m^3
|
||||
|
||||
void ParseLine(const std::string & line);
|
||||
void ParseLines(const std::vector<std::string> & lines);
|
||||
|
||||
std::string GetFormattedString(char const separator) const;
|
||||
};
|
||||
}
|
||||
24
include/electricity/logger/serialport.hpp
Normal file
24
include/electricity/logger/serialport.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <termios.h>
|
||||
|
||||
class SerialPort {
|
||||
private:
|
||||
const int device;
|
||||
termios configuration, oldConfiguration;
|
||||
|
||||
void SetAttributes(const speed_t baudrate = B115200);
|
||||
|
||||
protected:
|
||||
public:
|
||||
std::string ReadLine();
|
||||
|
||||
SerialPort(const int fd);
|
||||
|
||||
~SerialPort();
|
||||
|
||||
SerialPort(const SerialPort &) = delete;
|
||||
SerialPort(SerialPort &&) = delete;
|
||||
SerialPort & operator=(const SerialPort &) = delete;
|
||||
SerialPort & operator=(SerialPort &&) = delete;
|
||||
};
|
||||
10
include/electricity/server/api.hpp
Normal file
10
include/electricity/server/api.hpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <pistache/http.h>
|
||||
#include <pistache/router.h>
|
||||
|
||||
namespace Server::Api
|
||||
{
|
||||
void GetDay(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite);
|
||||
|
||||
void SetupRouting(Pistache::Rest::Router & router);
|
||||
}
|
||||
33
include/electricity/server/configuration.hpp
Normal file
33
include/electricity/server/configuration.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace Server
|
||||
{
|
||||
class Configuration {
|
||||
private:
|
||||
std::string logDirectory;
|
||||
std::string serverDomain;
|
||||
std::string lastExternalIp;
|
||||
std::chrono::time_point<std::chrono::steady_clock> lastIpCheckTimePoint;
|
||||
std::mutex externalIpRefreshMutex;
|
||||
|
||||
Configuration();
|
||||
Configuration(Configuration & other) = delete;
|
||||
Configuration(Configuration && other) = delete;
|
||||
Configuration & operator=(Configuration & other) = delete;
|
||||
Configuration & operator=(Configuration && other) = delete;
|
||||
|
||||
void RefreshExternalIp();
|
||||
bool ExternalIpRequiresRefresh() const;
|
||||
|
||||
public:
|
||||
void Setup(std::string & electricityLogDirectory, std::string const & serverDomain);
|
||||
|
||||
std::string const & GetLogDirectory() const;
|
||||
std::string const & GetExternalServerIp();
|
||||
|
||||
static Configuration & Get();
|
||||
};
|
||||
}
|
||||
8
include/electricity/server/database.hpp
Normal file
8
include/electricity/server/database.hpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include <string>
|
||||
#include <util/date.hpp>
|
||||
|
||||
namespace Server::Database
|
||||
{
|
||||
std::string GetDetailedJsonOf(Util::Date const & date);
|
||||
std::string GetSummaryJsonOf(Util::Date const & startDate, Util::Date const & stopDate);
|
||||
}
|
||||
9
include/migrator/migrations.hpp
Normal file
9
include/migrator/migrations.hpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace Migrations
|
||||
{
|
||||
int EpochToDateTime(std::string const & sourceDatabase, std::string const & destinationDatabase);
|
||||
|
||||
int UpdateSummaryTable(std::string const & sourceDatabase, std::string const & destinationDatabase);
|
||||
}
|
||||
24
include/migrator/transaction.hpp
Normal file
24
include/migrator/transaction.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <sqlite3.h>
|
||||
#include <sstream>
|
||||
|
||||
class Transaction {
|
||||
private:
|
||||
sqlite3 * const destination;
|
||||
unsigned queryCount;
|
||||
std::stringstream queryStream;
|
||||
|
||||
void Reset();
|
||||
|
||||
public:
|
||||
void AddStatement(std::string const & statement);
|
||||
|
||||
unsigned StatementCount() const;
|
||||
|
||||
// Runs the statements inserted as a single transaction
|
||||
// Returns a SQLite status code (0 = OK) and then resets
|
||||
// to a new transaction
|
||||
int Execute();
|
||||
|
||||
Transaction(sqlite3 * const databaseToInsertIn);
|
||||
};
|
||||
25
include/solar/logger/database.hpp
Normal file
25
include/solar/logger/database.hpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <sqlite3.h>
|
||||
#include <string>
|
||||
|
||||
struct Row
|
||||
{
|
||||
time_t epochTime;
|
||||
std::int32_t watt;
|
||||
double kilowattPerHour;
|
||||
};
|
||||
|
||||
class Database {
|
||||
private:
|
||||
sqlite3 * connectionPtr;
|
||||
|
||||
std::string ToSqlInsertStatement(Row const & row) const;
|
||||
std::string ToSqlUpsertStatement(Row const & row) const;
|
||||
|
||||
public:
|
||||
bool Insert(Row & row);
|
||||
|
||||
Database(std::string const & databasePath);
|
||||
~Database();
|
||||
};
|
||||
33
include/solar/logger/zeverdata.hpp
Normal file
33
include/solar/logger/zeverdata.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <curl/curl.h> // tested with libcurl4-openSSH
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
/*
|
||||
This class holds all the data retrieved from the Zeverlution combox /
|
||||
Zeverlution Sxxxx smart inverters / ZeverSolar box.
|
||||
|
||||
For output formatting it uses a timestamp to log the datetime alongside the
|
||||
watts and kilowatts.
|
||||
*/
|
||||
|
||||
struct ZeverData
|
||||
{
|
||||
int number0, number1;
|
||||
std::string registeryID, registeryKey, hardwareVersion, appVersion, wifiVersion;
|
||||
std::string timeValue, dateValue, zeverCloudStatus;
|
||||
int number3;
|
||||
std::string inverterSN;
|
||||
int watt;
|
||||
double kilowattPerHour;
|
||||
std::string OKmsg, ERRORmsg;
|
||||
|
||||
// Parses the data string coming from the zeverlution home.cgi webpage
|
||||
bool ParseString(const std::string & str);
|
||||
|
||||
bool FetchDataFromURL(const std::string & url, const unsigned int timeout);
|
||||
|
||||
ZeverData();
|
||||
};
|
||||
10
include/solar/server/api.hpp
Normal file
10
include/solar/server/api.hpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <pistache/router.h>
|
||||
|
||||
namespace Api
|
||||
{
|
||||
void GetDay(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite);
|
||||
void GetMonth(Pistache::Http::Request const & request, Pistache::Http::ResponseWriter responseWrite);
|
||||
|
||||
void SetupRouting(Pistache::Rest::Router & router);
|
||||
}
|
||||
18
include/solar/server/configuration.hpp
Normal file
18
include/solar/server/configuration.hpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <solar/server/database/database.hpp>
|
||||
#include <string>
|
||||
|
||||
class Configuration {
|
||||
private:
|
||||
Database::Database database;
|
||||
|
||||
Configuration();
|
||||
|
||||
friend int main(int, char **);
|
||||
Configuration & SetupDatabase(std::string const & filePath);
|
||||
|
||||
public:
|
||||
Database::Connection GetDatabaseConnection() const;
|
||||
|
||||
static Configuration & Get();
|
||||
};
|
||||
30
include/solar/server/database/connection.hpp
Normal file
30
include/solar/server/database/connection.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
#include <ctime>
|
||||
#include <sqlite3.h>
|
||||
#include <string>
|
||||
#include <util/date.hpp>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Connection {
|
||||
private:
|
||||
sqlite3 * const connectionPtr;
|
||||
|
||||
public:
|
||||
Connection(sqlite3 * const databaseConnectionPtr);
|
||||
|
||||
// Date should be in format yyyy-mm-dd
|
||||
// Returns a JSON array
|
||||
std::string GetEntireDay(Util::Date const & date);
|
||||
|
||||
// Dates should be in format yyyy-mm-dd
|
||||
// Returns a JSON array
|
||||
// startDate and endDate are inclusive
|
||||
std::string GetSummarizedPerDayRecords(Util::Date const & startDate, Util::Date const & endDate);
|
||||
|
||||
// Dates should be in format yyyy-mm-dd
|
||||
// Returns a JSON array
|
||||
// startDate and endDate are inclusive
|
||||
std::string GetSummarizedPerMonthRecords(Util::Date const & startDate, Util::Date const & endDate);
|
||||
};
|
||||
}
|
||||
20
include/solar/server/database/database.hpp
Normal file
20
include/solar/server/database/database.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <solar/server/database/connection.hpp>
|
||||
#include <sqlite3.h>
|
||||
#include <string>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Database {
|
||||
private:
|
||||
sqlite3 * connectionPtr;
|
||||
|
||||
public:
|
||||
bool Connect(std::string const & path);
|
||||
|
||||
Connection GetConnection() const;
|
||||
|
||||
Database();
|
||||
~Database();
|
||||
};
|
||||
}
|
||||
151
include/util/date.hpp
Normal file
151
include/util/date.hpp
Normal file
@@ -0,0 +1,151 @@
|
||||
#pragma once
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
namespace Util
|
||||
{
|
||||
inline bool IsValidYear(int const year) { return year > 1900 && year < 9999; }
|
||||
|
||||
inline bool IsValidMonth(int const month) { return month > 0 && month < 13; }
|
||||
|
||||
inline bool IsValidDay(int const day) { return day > 0 && day < 32; }
|
||||
|
||||
inline long GetEpoch(long year, unsigned month, unsigned day)
|
||||
{
|
||||
year -= month <= 2;
|
||||
const long era = (year >= 0 ? year : year - 399) / 400;
|
||||
const unsigned yoe = static_cast<unsigned>(year - era * 400); // [0, 399]
|
||||
const unsigned doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; // [0, 365]
|
||||
const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
|
||||
|
||||
// Multiply by the day length in seconds
|
||||
return (era * 146097 + doe - 719468) * (24l * 60l * 60l);
|
||||
}
|
||||
|
||||
inline long ToEpoch(std::string const & date)
|
||||
{
|
||||
int year, month, day;
|
||||
if(std::sscanf(date.c_str(), "%d-%d-%d", &year, &month, &day) != 3)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return GetEpoch(year, month, day);
|
||||
}
|
||||
|
||||
inline unsigned ToSeconds(unsigned hours, unsigned minutes, unsigned seconds)
|
||||
{
|
||||
return (hours * 60 * 60) + (minutes * 60) + seconds;
|
||||
}
|
||||
|
||||
inline int ToSecondsFromTimeString(char const * string)
|
||||
{
|
||||
int hour, minute, second;
|
||||
if(std::sscanf(string, "%d:%d:%d", &hour, &minute, &second) != 3)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return ToSeconds(hour, minute, second);
|
||||
}
|
||||
|
||||
inline std::string GetSqliteDate(time_t const epochTime)
|
||||
{
|
||||
auto const dateTime = *std::gmtime(&epochTime);
|
||||
|
||||
char dateStringBuffer[64];
|
||||
std::sprintf(dateStringBuffer, "%i-%02i-%02i", dateTime.tm_year + 1900, dateTime.tm_mon + 1, dateTime.tm_mday);
|
||||
|
||||
return std::string(dateStringBuffer);
|
||||
}
|
||||
|
||||
inline std::string GetSqliteUtcTime(time_t const epochTime)
|
||||
{
|
||||
auto const dateTime = *std::gmtime(&epochTime);
|
||||
|
||||
char timeStringBuffer[64];
|
||||
std::sprintf(timeStringBuffer, "%02i:%02i:%02i", dateTime.tm_hour, dateTime.tm_min, dateTime.tm_sec);
|
||||
|
||||
return std::string(timeStringBuffer);
|
||||
}
|
||||
|
||||
class Date {
|
||||
private:
|
||||
unsigned year;
|
||||
unsigned month;
|
||||
unsigned day;
|
||||
|
||||
public:
|
||||
unsigned Year() const { return year; }
|
||||
unsigned Month() const { return month; }
|
||||
unsigned Day() const { return day; }
|
||||
|
||||
void AddMonth()
|
||||
{
|
||||
++month;
|
||||
if(month > 12)
|
||||
{
|
||||
month = 1;
|
||||
++year;
|
||||
}
|
||||
}
|
||||
|
||||
void SetDayToEndOfMonth() { day = 31; }
|
||||
|
||||
bool IsValid() const { return IsValidYear(year) && IsValidMonth(month) && IsValidDay(day); }
|
||||
|
||||
long ToEpoch() const { return GetEpoch(year, month, day); }
|
||||
|
||||
bool IsBefore(unsigned const otherYear, unsigned const otherMonth, unsigned const otherDay) const
|
||||
{
|
||||
return (year < otherYear) || (year == otherYear && month < otherMonth)
|
||||
|| (year == otherYear && month == otherMonth && day < otherDay);
|
||||
}
|
||||
|
||||
bool IsBefore(Date const & other) const { return IsBefore(other.year, other.month, other.day); }
|
||||
|
||||
bool IsAfter(unsigned const otherYear, unsigned const otherMonth, unsigned const otherDay) const
|
||||
{
|
||||
return (year > otherYear) || (year == otherYear && month > otherMonth)
|
||||
|| (year == otherYear && month == otherMonth && day > otherDay);
|
||||
}
|
||||
|
||||
std::string ToISOString() const
|
||||
{
|
||||
char buffer[11];
|
||||
std::sprintf(buffer, "%04i-%02i-%02i", year, month, day);
|
||||
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
bool TryParse(std::string const & date)
|
||||
{
|
||||
Date result;
|
||||
|
||||
if(sscanf(date.c_str(), "%d-%d-%d", &year, &month, &day) != 3)
|
||||
{
|
||||
result.year = 0;
|
||||
result.month = 0;
|
||||
result.day = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Date() : year(0), month(0), day(0) { }
|
||||
|
||||
Date(std::string const & date) : year(0), month(0), day(0) { TryParse(date); }
|
||||
|
||||
Date(long const epoch)
|
||||
{
|
||||
std::tm dateTime = *std::gmtime(&epoch);
|
||||
year = 1900 + dateTime.tm_year;
|
||||
month = 1 + dateTime.tm_mon;
|
||||
day = dateTime.tm_mday;
|
||||
}
|
||||
|
||||
static Date UtcNow() { return Date(std::time(nullptr)); }
|
||||
};
|
||||
}
|
||||
109
makefile
Normal file
109
makefile
Normal file
@@ -0,0 +1,109 @@
|
||||
CC = g++
|
||||
CFLAGS = -DSPDLOG_FMT_EXTERNAL -Wall -Wextra -std=c++20 -O2 -Iinclude/ -MMD
|
||||
ELECTRICITY_LOGGER_LFLAGS = -lsqlite3 -lspdlog -lfmt
|
||||
SOLAR_LOGGER_LFLAGS = -lsqlite3 -lcurl
|
||||
SERVER_LFLAGS = -lpistache -lsqlite3 -lstdc++fs -lspdlog -lfmt
|
||||
MIGRATOR_LFLAGS = -lsqlite3
|
||||
|
||||
SOLAR_LOGGER_CPPS = $(shell find src/solar-logger/ -name *.cpp)
|
||||
SOLAR_LOGGER_OBJS = $(patsubst src/%.cpp, build/%.o, ${SOLAR_LOGGER_CPPS})
|
||||
SOLAR_LOGGER_DEPS = $(patsubst src/%.cpp, build/%.d, ${SOLAR_LOGGER_CPPS})
|
||||
|
||||
SOLAR_SRV_CPPS = $(shell find src/solar-server/ -name *.cpp)
|
||||
SOLAR_SRV_OBJS = $(patsubst src/%.cpp, build/%.o, ${SOLAR_SRV_CPPS})
|
||||
SOLAR_SRV_DEPS = $(patsubst src/%.cpp, build/%.d, ${SOLAR_SRV_CPPS})
|
||||
|
||||
MIGRATOR_CPPS = $(shell find src/migrator/ -name *.cpp)
|
||||
MIGRATOR_OBJS = $(patsubst src/%.cpp, build/%.o, ${MIGRATOR_CPPS})
|
||||
MIGRATOR_DEPS = $(patsubst src/%.cpp, build/%.d, ${MIGRATOR_CPPS})
|
||||
|
||||
ELECT_LOGGER_CPPS = $(shell find src/electricity-logger/ -name *.cpp)
|
||||
ELECT_LOGGER_OBJS = $(patsubst src/%.cpp, build/%.o, ${ELECT_LOGGER_CPPS})
|
||||
ELECT_LOGGER_DEPS = $(patsubst src/%.cpp, build/%.d, ${ELECT_LOGGER_CPPS})
|
||||
|
||||
ELECT_SRV_CPPS = $(shell find src/electricity-server/ -name *.cpp)
|
||||
ELECT_SRV_OBJS = $(patsubst src/%.cpp, build/%.o, ${ELECT_SRV_CPPS})
|
||||
ELECT_SRV_DEPS = $(patsubst src/%.cpp, build/%.d, ${ELECT_SRV_CPPS})
|
||||
|
||||
SOLAR_LOGGER_BINARY_PATH = bin/solar-logger
|
||||
SOLAR_SRV_BINARY_PATH = bin/solar-server
|
||||
MIGRATOR_BINARY_PATH = bin/migrator
|
||||
ELECT_LOGGER_BINARY_PATH = bin/electricity-logger
|
||||
ELECT_SRV_BINARY_PATH = bin/electricity-server
|
||||
|
||||
INSTALL_PATH = /usr/local/bin/
|
||||
|
||||
# Test variables
|
||||
SOLAR_TEST_DATABASE = solarpaneloutput.db
|
||||
|
||||
all: ${SOLAR_LOGGER_BINARY_PATH} ${SOLAR_SRV_BINARY_PATH} ${MIGRATOR_BINARY_PATH} ${ELECT_LOGGER_BINARY_PATH} ${ELECT_SRV_BINARY_PATH}
|
||||
|
||||
clean:
|
||||
-rm -rf bin/* build/*
|
||||
|
||||
check-solar-logger: ${SOLAR_LOGGER_BINARY_PATH} ${SOLAR_TEST_DATABASE}
|
||||
./${SOLAR_LOGGER_BINARY_PATH} -d ${SOLAR_TEST_DATABASE} -u "http://192.168.2.26/home.cgi"
|
||||
|
||||
check-solar-server: ${SOLAR_SRV_BINARY_PATH} ${SOLAR_TEST_DATABASE}
|
||||
./${SOLAR_SRV_BINARY_PATH} -d ${SOLAR_TEST_DATABASE} -c public/ -p 8080
|
||||
|
||||
check-electricity-logger: ${ELECT_LOGGER_BINARY_PATH}
|
||||
./${ELECT_LOGGER_BINARY_PATH} -d /dev/ttyUSB0 -c additional_column_value
|
||||
|
||||
check-electricity-server: ${ELECT_SRV_BINARY_PATH}
|
||||
./${ELECT_SRV_BINARY_PATH} -d /var/data0/electricity -p 8081 -s valkendaal.duckdns.org
|
||||
|
||||
install: install-loggers install-servers
|
||||
|
||||
install-loggers: ${SOLAR_LOGGER_BINARY_PATH} ${ELECT_LOGGER_BINARY_PATH}
|
||||
cp -uv ${SOLAR_LOGGER_BINARY_PATH} ${INSTALL_PATH}
|
||||
cp -uv ${ELECT_LOGGER_BINARY_PATH} ${INSTALL_PATH}
|
||||
|
||||
install-solar-server: ${SOLAR_SRV_BINARY_PATH}
|
||||
cp -uv ${SOLAR_SRV_BINARY_PATH} ${INSTALL_PATH}
|
||||
|
||||
install-electricity-server: ${ELECT_SRV_BINARY_PATH}
|
||||
cp -uv ${ELECT_SRV_BINARY_PATH} ${INSTALL_PATH}
|
||||
|
||||
install-servers: install-solar-server install-electricity-server
|
||||
|
||||
.PHONY: all \
|
||||
clean \
|
||||
check-solar-logger check-solar-server \
|
||||
install \
|
||||
install-loggers \
|
||||
install-solar-server install-electricity-server \
|
||||
install-servers
|
||||
|
||||
${SOLAR_TEST_DATABASE}:
|
||||
./script/createdb.sh ${SOLAR_TEST_DATABASE}
|
||||
|
||||
${SOLAR_LOGGER_BINARY_PATH}: ${SOLAR_LOGGER_OBJS}
|
||||
mkdir -p ${@D}
|
||||
${CC} $^ ${SOLAR_LOGGER_LFLAGS} -o $@
|
||||
|
||||
${SOLAR_SRV_BINARY_PATH}: ${SOLAR_SRV_OBJS}
|
||||
mkdir -p ${@D}
|
||||
${CC} $^ ${SERVER_LFLAGS} -o $@
|
||||
|
||||
${MIGRATOR_BINARY_PATH}: ${MIGRATOR_OBJS}
|
||||
mkdir -p ${@D}
|
||||
${CC} $^ ${MIGRATOR_LFLAGS} -o $@
|
||||
|
||||
${ELECT_LOGGER_BINARY_PATH}: ${ELECT_LOGGER_OBJS}
|
||||
mkdir -p ${@D}
|
||||
${CC} $^ ${ELECTRICITY_LOGGER_LFLAGS} -o $@
|
||||
|
||||
${ELECT_SRV_BINARY_PATH}: ${ELECT_SRV_OBJS}
|
||||
mkdir -p ${@D}
|
||||
${CC} $^ ${SERVER_LFLAGS} -o $@
|
||||
|
||||
build/%.o: src/%.cpp
|
||||
mkdir -p ${@D}
|
||||
${CC} ${CFLAGS} -c $< -o $@
|
||||
|
||||
-include ${SOLAR_SRV_DEPS}
|
||||
-include ${SOLAR_LOGGER_DEPS}
|
||||
-include ${MIGRATOR_DEPS}
|
||||
-include ${ELECT_LOGGER_DEPS}
|
||||
-include ${ELECT_SRV_DEPS}
|
||||
32
script/electricity/createdb.sh
Executable file
32
script/electricity/createdb.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: $0 [/path/to/db/database].db";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "Creating database file $1...";
|
||||
touch "$1";
|
||||
if ! [ -f "$1" ]; then
|
||||
echo "Cannot open or create database file $1, aborting.";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
TABLE_NAME="ElectricityLog";
|
||||
|
||||
echo "Creating table $TABLE_NAME...";
|
||||
sqlite3 $1 "CREATE TABLE IF NOT EXISTS $TABLE_NAME (\
|
||||
Date TEXT NOT NULL,\
|
||||
TimeUtc TEXT NOT NULL,\
|
||||
CurrentPowerUsage REAL NOT NULL,\
|
||||
TotalPowerConsumptionDay REAL NOT NULL,\
|
||||
TotalPowerConsumptionNight REAL NOT NULL,\
|
||||
CurrentPowerReturn REAL NOT NULL,\
|
||||
TotalPowerReturnDay REAL NOT NULL,\
|
||||
TotalPowerReturnNight REAL NOT NULL,\
|
||||
DayTarifEnabled INTEGER NOT NULL,\
|
||||
GasConsumptionInCubicMeters REAL NOT NULL);";
|
||||
|
||||
echo "Creating indexes on table $TABLE_NAME...";
|
||||
sqlite3 $1 "CREATE INDEX IF NOT EXISTS idx_Date ON $TABLE_NAME (Date);"
|
||||
sqlite3 $1 "CREATE INDEX IF NOT EXISTS idx_TimeUtc ON $TABLE_NAME (TimeUtc);"
|
||||
BIN
script/electricity/test.db
Normal file
BIN
script/electricity/test.db
Normal file
Binary file not shown.
12
script/solar/README.md
Normal file
12
script/solar/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Scripts
|
||||
|
||||
This directory contains the following 2 scripts:
|
||||
- `createdb.sh` is used to create the SQL database file used by the logger
|
||||
- `migratedb.sh` is used to fill the newly created SQL database with the old file based logging content
|
||||
|
||||
The created database exists out of 3 rows, ignoring the `RowId` column:
|
||||
- `DateTimeUtc` is an `INTEGER` and represents the UTC date and time in the Unix epoch format
|
||||
- `Watts` is an `INTEGER` and represents the power output at the time of logging
|
||||
- `KilowattHour` is a `REAL` and represents the cumulative power generated that day
|
||||
|
||||
Try running each script without any arguments for more help or read their source code.
|
||||
30
script/solar/createdb.sh
Executable file
30
script/solar/createdb.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: $0 [/path/to/db/database].db";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "Creating database file $1...";
|
||||
touch "$1";
|
||||
if ! [ -f "$1" ]; then
|
||||
echo "Cannot open or create database file $1, aborting.";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
TABLE_NAME="SolarPanelOutput";
|
||||
|
||||
echo "Creating table $TABLE_NAME...";
|
||||
sqlite3 $1 "CREATE TABLE IF NOT EXISTS $TABLE_NAME (Date TEXT NOT NULL, TimeUtc TEXT NOT NULL, Watts INTEGER NOT NULL, KilowattHour REAL NOT NULL);";
|
||||
|
||||
echo "Creating indexes on table $TABLE_NAME...";
|
||||
sqlite3 $1 "CREATE INDEX IF NOT EXISTS idx_Date ON $TABLE_NAME (Date);"
|
||||
sqlite3 $1 "CREATE INDEX IF NOT EXISTS idx_TimeUtc ON $TABLE_NAME (TimeUtc);"
|
||||
|
||||
TABLE2_NAME="SolarPanelSummary";
|
||||
|
||||
echo "Creating table $TABLE2_NAME...";
|
||||
sqlite3 $1 "CREATE TABLE IF NOT EXISTS $TABLE2_NAME (Date TEXT NOT NULL UNIQUE, Kilowatthour REAL NOT NULL);"
|
||||
|
||||
echo "Creating indexes on table $TABLE2_NAME";
|
||||
sqlite3 $1 "CREATE UNIQUE INDEX IF NOT EXISTS idx_Date_$TABLE2_NAME on $TABLE2_NAME (Date);"
|
||||
41
script/solar/migratedb.sh
Executable file
41
script/solar/migratedb.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
TABLE_NAME="SolarPanelOutput";
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 [/log/file/directory/] [/path/to/db/database].db";
|
||||
fi
|
||||
|
||||
if ! [ -d "$1" ]; then
|
||||
echo "Error opening log file directory $1, aborting.";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
if ! [ -f "$2" ]; then
|
||||
echo "Error opening database file $2, aborting.";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
if [ "${$1}" != */ ]; then
|
||||
$1 = "$1/";
|
||||
fi
|
||||
|
||||
for DIRECTORY in $1*/; do
|
||||
if [ -d "$DIRECTORY" ]; then
|
||||
echo "Checking out directory $DIRECTORY...";
|
||||
for FILE in $DIRECTORY*; do
|
||||
if [ -f "$FILE" ]; then
|
||||
echo "Processing file $FILE...";
|
||||
while read line; do
|
||||
PROCESSED_LINE="$(echo "$line" | tr -s ' ')";
|
||||
ISODATE="$(echo "$PROCESSED_LINE" | cut -d ' ' -f1)";
|
||||
WATT="$(echo "$PROCESSED_LINE" | cut -d ' ' -f2)";
|
||||
KILOWATTHR="$(echo "$PROCESSED_LINE" | cut -d ' ' -f3)";
|
||||
|
||||
EPOCHDATE="$(date -d"$ISODATE" +%s)"
|
||||
sqlite3 $2 "INSERT INTO $TABLE_NAME VALUES($EPOCHDATE, $WATT, $KILOWATTHR);";
|
||||
done < "$FILE"
|
||||
fi
|
||||
done;
|
||||
fi
|
||||
done
|
||||
60
src/electricity-logger/database.cpp
Normal file
60
src/electricity-logger/database.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
174
src/electricity-logger/dsmr.cpp
Normal file
174
src/electricity-logger/dsmr.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
117
src/electricity-logger/main.cpp
Normal file
117
src/electricity-logger/main.cpp
Normal 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;
|
||||
}
|
||||
55
src/electricity-logger/serialport.cpp
Normal file
55
src/electricity-logger/serialport.cpp
Normal 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); }
|
||||
51
src/electricity-server/main.cpp
Normal file
51
src/electricity-server/main.cpp
Normal 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();
|
||||
}
|
||||
96
src/electricity-server/server/api.cpp
Normal file
96
src/electricity-server/server/api.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
98
src/electricity-server/server/configuration.cpp
Normal file
98
src/electricity-server/server/configuration.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
211
src/electricity-server/server/database.cpp
Normal file
211
src/electricity-server/server/database.cpp
Normal 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
32
src/migrator/main.cpp
Normal 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;
|
||||
}
|
||||
91
src/migrator/migrations/epochtodatetime.cpp
Normal file
91
src/migrator/migrations/epochtodatetime.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
68
src/migrator/migrations/updatesummarytable.cpp
Normal file
68
src/migrator/migrations/updatesummarytable.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
35
src/migrator/transaction.cpp
Normal file
35
src/migrator/transaction.cpp
Normal 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();
|
||||
}
|
||||
53
src/solar-logger/database.cpp
Normal file
53
src/solar-logger/database.cpp
Normal 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
60
src/solar-logger/main.cpp
Normal 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;
|
||||
}
|
||||
88
src/solar-logger/zeverdata.cpp
Normal file
88
src/solar-logger/zeverdata.cpp
Normal 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
93
src/solar-server/api.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
24
src/solar-server/configuration.cpp
Normal file
24
src/solar-server/configuration.cpp
Normal 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;
|
||||
}
|
||||
241
src/solar-server/database/connection.cpp
Normal file
241
src/solar-server/database/connection.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
33
src/solar-server/database/database.cpp
Normal file
33
src/solar-server/database/database.cpp
Normal 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
42
src/solar-server/main.cpp
Normal 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();
|
||||
}
|
||||
13
systemd/electricity-server.service
Normal file
13
systemd/electricity-server.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=A pistache based HTTP server serving the Electricity API
|
||||
Requires=network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/electricity-server -d /mnt/data0/log/electricity -p 3002 -s electricity.valkendaal.duckdns.org
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
13
systemd/solar-server.service
Normal file
13
systemd/solar-server.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=A pistache based HTTP server serving the Solar API
|
||||
Requires=network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/solar-server -d /mnt/data0/log/solarpaneloutput.db -p 3001
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user