Rewrite electricity-logger to use an sqlite3 database

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

107
.clang-format Normal file
View 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
View File

@@ -1,3 +1,5 @@
build/
# ---> C++
# Prerequisites
*.d

19
.vscode/c_cpp_properties.json vendored Normal file
View 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

File diff suppressed because one or more lines are too long

55
.vscode/dryrun.log vendored Normal file
View 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
View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"llvm-vs-code-extensions.vscode-clangd",
]
}

79
.vscode/settings.json vendored Normal file
View 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
View 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

View File

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

View File

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

Binary file not shown.

6
compile_flags.txt Normal file
View File

@@ -0,0 +1,6 @@
-xc++
-Wall
-Wextra
-DSPDLOG_FMT_EXTERNAL
-Iinclude/
-std=c++17

75
docs/BENCHMARK.md Normal file
View 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
View 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
},
...
]
```

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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();
};

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

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

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

View 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();
};
}

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

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

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

View 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();
};

View 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();
};

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

View 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();
};

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

View 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
View 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
View 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
View 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

Binary file not shown.

12
script/solar/README.md Normal file
View 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
View 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
View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

View 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

View 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