Skip to content

Commit

Permalink
Merge pull request #1 from infertux/mqtt
Browse files Browse the repository at this point in the history
Refactor to add MQTT integration
  • Loading branch information
infertux authored Mar 26, 2024
2 parents a89f607 + 179cd96 commit 6261b84
Show file tree
Hide file tree
Showing 18 changed files with 3,727 additions and 352 deletions.
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

0 comments on commit 6261b84

Please sign in to comment.