Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to add MQTT integration #1

Merged
merged 17 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .clang-format
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
BasedOnStyle: LLVM
ColumnLimit: 120
ColumnLimit: 140
2,822 changes: 2,822 additions & 0 deletions .doxygen

Large diffs are not rendered by default.

33 changes: 25 additions & 8 deletions .github/workflows/make.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,32 @@ on:
pull_request:
branches: [ "master" ]

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
continue-on-error: true # FIXME: remove when gcc build is passing
strategy:
fail-fast: false
matrix:
cc: [clang, gcc]
steps:
- uses: actions/checkout@v4
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: ${{ matrix.cc }} libbsd-dev libconfig-dev libmodbus-dev libmosquitto-dev
- name: make with ${{ matrix.cc }}
run: env CC=${{ matrix.cc }} make
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: clang libbsd-dev libmodbus-dev
- name: make
run: make
- name: make lint
run: make lint
- uses: actions/checkout@v4
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: clang libbsd-dev libconfig-dev libmodbus-dev libmosquitto-dev mosquitto-clients
- name: make lint
run: env CC=clang make lint
- name: make test
run: env CC=clang make test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/deploy.sh
/growatt
/html/
/tests/mock-server
30 changes: 20 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
CC=clang
RM=rm -f
CFLAGS=$(shell pkg-config --cflags libbsd libmodbus)
LIBS=$(shell pkg-config --libs libbsd libmodbus) -pthread

SRCS=src/growatt.c src/*.h
CC?=clang
#CC?=gcc
RM=rm -fv
CFLAGS=$(shell pkg-config --cflags libbsd libconfig libmodbus libmosquitto)
LIBS=$(shell pkg-config --libs libbsd libconfig libmodbus libmosquitto) -pthread
SRCS=src/*
TESTS=tests/*.c

all: growatt

doc: $(SRCS)
doxygen .doxygen

growatt: $(SRCS)
$(CC) $(CFLAGS) $(LIBS) -Wall -Werror -pedantic -O3 -o growatt src/growatt.c
$(CC) -v $(CFLAGS) $(LIBS) -Wall -Werror -O3 -o growatt src/*.c

lint:
clang-format --verbose --Werror -i --style=file src/*
clang-tidy --checks='*,-altera-id-dependent-backward-branch,-altera-unroll-loops,-cert-err33-c,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-llvm-header-guard,-llvmlibc-restrict-system-libc-headers,-readability-function-cognitive-complexity' --format-style=llvm src/* -- $(CFLAGS)
clang-format --verbose --Werror -i --style=file $(SRCS) $(TESTS)
clang-tidy --checks='*,-altera-id-dependent-backward-branch,-altera-unroll-loops,-bugprone-assignment-in-if-condition,-cert-err33-c,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-cppcoreguidelines-avoid-magic-numbers,-llvm-header-guard,-llvmlibc-restrict-system-libc-headers,-readability-function-cognitive-complexity' --format-style=llvm $(SRCS) $(TESTS) -- $(CFLAGS)
.PHONY: lint

test: growatt $(TESTS)
$(CC) -v $(shell pkg-config --libs --cflags libbsd libmodbus) -Wall -Werror -o tests/mock-server $(TESTS)
timeout 30 mosquitto_sub -h test.mosquitto.org -p 1884 -u rw -P readwrite -t homeassistant/sensor/growatt/state -d &
./tests/mock-server &
./growatt config-example.conf || true

clean:
$(RM) growatt
$(RM) growatt tests/mock-server
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This allows to monitor PV production, battery status, etc. on a nice Grafana int
## Build

```bash
apt install clang libbsd-dev libmodbus-dev
apt install clang libbsd-dev libconfig-dev libmodbus-dev libmosquitto-dev mosquitto-clients
make
```

Expand All @@ -19,6 +19,12 @@ The "Growatt OffGrid SPF5000 Modbus RS485 RTU Protocol" PDF document has been a

Would like to monitor Epever/Epsolar Tracer solar charge controllers instead? Here is a sister repository for that: https://github.com/infertux/epever_exporter

## Other approaches

- Blog post: https://www.splitbrain.org/blog/2023-11/03-growatt_and_home_assistant
- Reading Modbus registers via Home Assistant: https://github.com/home-assistant/core/issues/94149
- https://github.com/rspring/Esphome-Growatt

## License

AGPLv3+
5 changes: 5 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- add program option --verbose instead of compile-time LOG_VERBOSE define
- test program with either --mqtt XOR --prometheus but not both
- go through commented code
- fix all lint warnings
- use condition variables to exit all threads cleanly?
16 changes: 16 additions & 0 deletions config-example.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Block device path or host:port to connect to (required)
# device_or_uri = "/dev/ttyUSB0"
device_or_uri = "127.0.0.1:1502"

// Prometheus config (optional block)
prometheus = {
port = 1234
}

// MQTT config (optional block)
mqtt = {
host = "test.mosquitto.org"
port = 1884
username = "wo"
password = "writeonly"
}
4 changes: 4 additions & 0 deletions config-minimal.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
device_or_uri = "127.0.0.1:1502"
prometheus = {
port = 1234
}
34 changes: 17 additions & 17 deletions docker-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,28 @@ set -euxo pipefail

cd "$(dirname "$0")"

target="${1:-growatt}"
interactive="${2:-}"
container=${target}-builder
channel="${1:-stable}" # can be overriden with "bullseye" for example
target=growatt
container=${target}-builder-${channel}
volume=/root/HOST
channel=stable
cc=clang-13

if [ -z "${FAST:-}" ]; then
docker pull debian:${channel}
docker pull "debian:${channel}"

[ "$(docker ps -qaf "name=${container}")" ] || docker run --name $container -d -t -v "${PWD}:${volume}" debian:${channel}
[ "$(docker ps -qaf "name=${container}")" ] || docker run --name "$container" --detach --tty --volume "${PWD}:${volume}" --network host "debian:${channel}"

docker start $container
docker start "$container"

docker exec $container dpkg --configure -a
docker exec $container bash -c "echo \"deb http://ftp.sg.debian.org/debian ${channel} main\" | tee /etc/apt/sources.list"
docker exec $container apt-get update
docker exec $container apt-get upgrade -y
docker exec $container apt-get install -y make clang pkg-config
docker exec $container apt-get install -y libbsd-dev libmodbus-dev
docker exec $container apt-get autoremove -y --purge
docker exec "$container" dpkg --configure -a
docker exec "$container" apt-get update
docker exec "$container" apt-get upgrade -y
docker exec "$container" apt-get install -y $cc
docker exec "$container" apt-get install -y make pkg-config
docker exec "$container" apt-get install -y libbsd-dev libconfig-dev libmodbus-dev libmosquitto-dev
docker exec "$container" apt-get autoremove -y --purge
fi

docker exec --workdir "${volume}" $container rm -fv $target
docker exec --workdir "${volume}" $container make $target
docker exec --workdir "${volume}" $container ls -l $target
docker exec --workdir "${volume}" "$container" rm -fv $target
docker exec --workdir "${volume}" "$container" env CC=$cc make $target
docker exec --workdir "${volume}" "$container" ls -l $target
150 changes: 131 additions & 19 deletions src/growatt.c
Original file line number Diff line number Diff line change
@@ -1,40 +1,152 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

#include <assert.h>
#include <libconfig.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

#include "http.h"
#include "log.h"

#define MAX_PORT_NUMBER USHRT_MAX
#include "mqtt.h"
#include "prometheus.h"

enum {
MAX_DEVICES = 8,
RADIX_DECIMAL = 10,
STDC_VERSION_MIN = 201710L,
};

int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(LOG_ERROR, "Usage: %s <device_id[,device2_id]> <port>\n", argv[0]);
fprintf(LOG_ERROR, "Example: %s 1 1234\n", argv[0]);
fprintf(LOG_ERROR, "Example: %s 1,2 1234\n", argv[0]);
typedef struct __attribute__((aligned(64))) {
const char *device_or_uri;
prometheus_config prometheus_config;
mqtt_config mqtt_config;
} config;

static int usage(char const program[static 1]) {
fprintf(stderr, "Usage: %s <config_file>\n", program);
fprintf(stderr, "Example: %s /etc/growatt-exporter.conf\n", program);
return EXIT_FAILURE;
}

static int join_thread(thrd_t const *thread, char const label[static 1]) {
int value = 0;
int code = thrd_join(*thread, &value);

if (code != thrd_success || value != EXIT_SUCCESS) {
LOG(LOG_ERROR, "Thread %s failed with value %d (code = %d)", label, value, code);

keep_running = 0;

if (!strcmp(label, "MDBS")) {
stop_prometheus_thread();
// kill(SIGTERM, 0);
}
} else {
LOG(LOG_INFO, "Thread %s exited successfully", label);
}

return value;
}

/*static void sig_handler(int signal)
{
LOG(LOG_INFO, "Got signal %d", signal);
keep_running = 0;
}*/

int parse_config(config *config, config_t *parser, char const *filename) {
if (!config_read_file(parser, filename)) {
LOG(LOG_ERROR, "%s:%d - %s\n", config_error_file(parser), config_error_line(parser), config_error_text(parser));
return EXIT_FAILURE;
}

uint8_t device_ids[MAX_DEVICES] = {0};
uint8_t current_device_id = 0;
char *device_id = NULL;
char *rest = argv[1];
while ((device_id = strtok_r(rest, ",", &rest))) {
device_ids[current_device_id++] = strtol(device_id, NULL, RADIX_DECIMAL);
if (CONFIG_TRUE != config_lookup_string(parser, "device_or_uri", &config->device_or_uri)) {
LOG(LOG_ERROR, "No 'device_or_uri' setting in configuration file");
return EXIT_FAILURE;
}

if (CONFIG_TRUE != config_lookup_int(parser, "prometheus.port", &config->prometheus_config.port)) {
config->prometheus_config.port = 0;
}

if (CONFIG_TRUE != config_lookup_int(parser, "mqtt.port", &config->mqtt_config.port)) {
config->mqtt_config.port = 0;
}

const uint16_t port = strtol(argv[2], NULL, RADIX_DECIMAL);
if (port < 1 || port > MAX_PORT_NUMBER) {
fprintf(LOG_ERROR, "Invalid port number: %d\n", port);
config_lookup_string(parser, "mqtt.host", &config->mqtt_config.host);
config_lookup_string(parser, "mqtt.username", &config->mqtt_config.username);
config_lookup_string(parser, "mqtt.password", &config->mqtt_config.password);

return EXIT_SUCCESS;
}

int main(int argc, char *argv[argc + 1]) {
static_assert(__STDC_VERSION__ >= STDC_VERSION_MIN, "C17+ required");

// disable log buffering
setbuf(stdout, NULL);
setbuf(stderr, NULL);

// signal(SIGINT, sig_handler);

if (argc < 2) {
return usage(argv[0]);
}

config config;
config_t parser_config;
config_init(&parser_config);
if (parse_config(&config, &parser_config, argv[1])) {
config_destroy(&parser_config);
return EXIT_FAILURE;
}

thrd_t prometheus_thread = 0;
thrd_t mqtt_thread = 0;
thrd_t modbus_thread = 0;

if (config.prometheus_config.port) {
int status = thrd_create(&prometheus_thread, (thrd_start_t)start_prometheus_thread, &config.prometheus_config);
if (status != thrd_success) {
PERROR("thrd_create() failed");
config_destroy(&parser_config);
return EXIT_FAILURE;
}
}

if (config.mqtt_config.port) {
int status = thrd_create(&mqtt_thread, (thrd_start_t)start_mqtt_thread, &config.mqtt_config);
if (status != thrd_success) {
PERROR("thrd_create() failed");
config_destroy(&parser_config);
return EXIT_FAILURE;
}
}

if (!prometheus_thread && !mqtt_thread) {
LOG(LOG_ERROR, "You must configure at least Prometheus or MQTT (or both)");
config_destroy(&parser_config);
return EXIT_FAILURE;
}

int status = thrd_create(&modbus_thread, (thrd_start_t)start_modbus_thread, (void *)config.device_or_uri);
if (status != thrd_success) {
PERROR("thrd_create() failed");
config_destroy(&parser_config);
return EXIT_FAILURE;
}

return http(port, device_ids);
// FIXME: catch MQTT thread termination somehow
int value = join_thread(&modbus_thread, "MDBS");

if (prometheus_thread) {
value += join_thread(&prometheus_thread, "PRMT");
}
if (mqtt_thread) {
value += join_thread(&mqtt_thread, "MQTT");
}

config_destroy(&parser_config);

LOG(LOG_INFO, "Bye");
exit(value); // will terminate any remaining threads
}
Loading
Loading