From 34d0876ee2878e803ae23b787d3a7f1dc359e368 Mon Sep 17 00:00:00 2001 From: pvtom Date: Sat, 23 Sep 2023 09:19:39 +0200 Subject: [PATCH] v2.0: new topics, docker --- .github/workflows/ci.yml | 55 ++++ DOCKER.md | 15 + Dockerfile | 34 +++ LICENSE | 2 +- Makefile | 2 +- README.md | 144 +++++++--- config.template | 12 + s10m.c | 607 +++++++++++++++++++++++---------------- s10m.h | 78 ++++- s10m.service | 18 ++ 10 files changed, 662 insertions(+), 305 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 DOCKER.md create mode 100644 Dockerfile create mode 100644 config.template create mode 100644 s10m.service diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cb1ff3c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: docker + +on: + push: + tags: + - "*" +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + # list of Docker images to use as base name for tags + images: | + ${{ github.repository }} + ghcr.io/${{ github.repository }} + - + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ github.actor }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Login to Github Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/386,linux/arm64,linux/arm + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..0ab1b75 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,15 @@ +## Docker + +A Docker image is available at https://hub.docker.com/r/pvtom/s10m + +### Configuration + +Create a `.config` file as described in the [Readme](README.md). + +### Start the docker container + +``` +docker run --rm -v /path/to/your/.config:/app/.config pvtom/s10m:latest +``` + +Thanks to [felix1507](https://github.com/felix1507/s10m-docker) for developing the Dockerfile! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..413a1b2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM debian:latest as build-basis + +WORKDIR /build + +# Install dependencies +RUN apt-get update && \ + apt-get install -y build-essential git automake autoconf libtool libmosquitto-dev + +FROM build-basis as build-libmodbus + +WORKDIR /libmodbus + +RUN git clone https://github.com/stephane/libmodbus.git . && \ + bash autogen.sh && \ + ./configure && \ + make install + +FROM build-libmodbus as build-s10m + +WORKDIR /build_s10m +COPY . . + +RUN make + +FROM debian:latest as runtime + +WORKDIR /app +COPY --from=build-s10m /build_s10m/s10m /usr/bin +COPY --from=build-libmodbus /usr/local/lib/* /usr/local/lib/ + +RUN apt-get update && \ + apt-get install -y libmosquitto-dev + +CMD [ "s10m" ] diff --git a/LICENSE b/LICENSE index c0df488..89287ce 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Thomas Heiny, Wolfsburg, Germany +Copyright (c) 2022,2023 Thomas Heiny, Wolfsburg, Germany Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 49e2dd2..0b2c82a 100644 --- a/Makefile +++ b/Makefile @@ -9,4 +9,4 @@ $(PRG): clean $(CC) -O3 s10m.c $(MODBUS_CFLAGS) -lmosquitto $(MODBUS_LIBS) -o $@ clean: - -rm $(PRG) + -rm -f $(PRG) diff --git a/README.md b/README.md index 8a63dd5..a891641 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,26 @@ # s10m - E3/DC S10 Modbus to MQTT connector +[![GitHub sourcecode](https://img.shields.io/badge/Source-GitHub-green)](https://github.com/pvtom/s10m/) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/pvtom/s10m)](https://github.com/pvtom/s10m/releases/latest) +[![GitHub last commit](https://img.shields.io/github/last-commit/pvtom/s10m)](https://github.com/pvtom/s10m/commits) +[![GitHub issues](https://img.shields.io/github/issues/pvtom/s10m)](https://github.com/pvtom/s10m/issues) +[![GitHub pull requests](https://img.shields.io/github/issues-pr/pvtom/s10m)](https://github.com/pvtom/s10m/pulls) +[![GitHub](https://img.shields.io/github/license/pvtom/s10m)](https://github.com/pvtom/s10m/blob/main/LICENSE) -This software module connects a E3/DC S10 home power station with a MQTT broker. It uses the Modbus interface of the S10 device. +This software module connects a home power station from E3/DC to an MQTT broker. It uses the Modbus interface of the S10 device. -Developed and tested with a Raspberry Pi. +Developed and tested with a Raspberry Pi and a Linux PC (x86_64). The tool s10m queries the data from the home power station and sends it to the MQTT broker. The following topics are supported: - s10/autarky - s10/consumption -- s10/battery/charging/limit - s10/battery/charging/lock - s10/battery/discharging/lock - s10/emergency/ready +- s10/battery/weather_regulation - s10/grid/limit +- s10/idle_period/charging/active +- s10/idle_period/discharging/active - s10/battery/soc - s10/battery/power - s10/battery/state @@ -24,18 +32,55 @@ The tool s10m queries the data from the home power station and sends it to the M - s10/grid/power/L2 - s10/grid/power/L3 - s10/home/power +- s10/addon/power - s10/manufacturer - s10/model - s10/serial_number - s10/solar/power -- s10/string_1/current -- s10/string_2/current -- s10/string_1/power -- s10/string_2/power -- s10/string_1/voltage -- s10/string_2/voltage - -Only modified values will be published. +- s10/current/string_1 +- s10/current/string_2 +- s10/power/string_1 +- s10/power/string_2 +- s10/voltage/string_1 +- s10/voltage/string_2 +- s10/pvi/apparent_power/L1 +- s10/pvi/apparent_power/L2 +- s10/pvi/apparent_power/L3 +- s10/pvi/active_power/L1 +- s10/pvi/active_power/L2 +- s10/pvi/active_power/L3 +- s10/pvi/reactive_power/L1 +- s10/pvi/reactive_power/L2 +- s10/pvi/reactive_power/L3 +- s10/pvi/voltage/L1 +- s10/pvi/voltage/L2 +- s10/pvi/voltage/L3 +- s10/pvi/current/L1 +- s10/pvi/current/L2 +- s10/pvi/current/L3 +- s10/grid/frequency +- s10/pvi/power/string_1 +- s10/pvi/power/string_2 +- s10/pvi/voltage/string_1 +- s10/pvi/voltage/string_2 +- s10/pvi/current/string_1 +- s10/pvi/current/string_2 + +If one or more E3/DC wallboxes are available: + +- s10/wallbox/[0-7]/available +- s10/wallbox/[0-7]/sun_mode +- s10/wallbox/[0-7]/ready +- s10/wallbox/[0-7]/charging +- s10/wallbox/[0-7]/1phase +- s10/wallbox/total/power +- s10/wallbox/solar/power + +The prefix of the topics can be configured by the attribute ROOT_TOPIC. By default all topics start with "s10". This can be changed to any other string that MQTT accepts as a topic. + +## Docker + +Instead of installing the package you can use the [Docker image](DOCKER.md). ## Prerequisite @@ -44,13 +89,12 @@ Only modified values will be published. - s10m needs the library libmosquitto. To install it on a Raspberry Pi enter: ``` -sudo apt-get install libmosquitto-dev +sudo apt-get install -y build-essential git automake autoconf libtool libmosquitto-dev ``` - s10m connects the S10 via the Modbus protocol, so you have to install a Modbus library: ``` git clone https://github.com/stephane/libmodbus.git cd libmodbus/ -sudo apt-get install libtool # when autogen or configure fail ./autogen.sh ./configure sudo make install @@ -63,25 +107,6 @@ sudo apt-get install git # if necessary git clone https://github.com/pvtom/s10m.git ``` -## Configuration - -Please check the configuration values in the source code file s10m.h and adjust them to your needs. -``` -// Host name of the E3/DC S10 device -MODBUS_HOST=e3dc -// Port of the E3/DC S10 device, default is 502 -MODBUS_PORT=502 -// Target MQTT broker -MQTT_HOST=localhost -// Default port is 1883 -MQTT_PORT=1883 -// MQTT parameters -MQTT_QOS=0 -MQTT_RETAIN=false -// Interval requesting the E3/DC S10 device in seconds -POLL_INTERVAL=1 -``` - ## Compilation ``` @@ -101,6 +126,47 @@ Adjust user and group (pi:pi) if you use another user. cp -a s10m /opt/s10m ``` +## Configuration + +Copy the config template file into the directory `/opt/s10m` +``` +cp config.template /opt/s10m/.config +``` + +Please change to the directory `/opt/s10m` and edit `.config` to adjust to your configuration: + +``` +cd /opt/s10m +nano .config +``` + +``` +// Host name of the E3/DC S10 device +MODBUS_HOST=e3dc +// Port of the E3/DC S10 device, default is 502 +MODBUS_PORT=502 +// Target MQTT broker +MQTT_HOST=localhost +// Default port is 1883 +MQTT_PORT=1883 +// MQTT user / password authentication necessary? Depends on the MQTT broker configuration. +MQTT_AUTH=false +// if true, then enter here +MQTT_USER= +MQTT_PASSWORD= +// MQTT parameters +MQTT_QOS=0 +MQTT_RETAIN=false +// Interval requesting the E3/DC S10 device in seconds +INTERVAL=1 +// Root topic +ROOT_TOPIC=s10 +// Force mode (publish also unchanged topics) +FORCE=false +``` + +s10m will also start wihout a `.config` file. In this case s10m will use the default values. + ## Test Start the program: @@ -169,7 +235,7 @@ Start the program in daemon mode: ./s10m -d ``` -If you like to start `s10m` during the system start, use `/etc/rc.local`. Add the following line before `exit 0`. +If you like to start s10m during the system start, use `/etc/rc.local`. Add the following line before `exit 0`. ``` (cd /opt/s10m ; /usr/bin/sudo -H -u pi /opt/s10m/s10m -d) @@ -182,6 +248,18 @@ pkill s10m ``` Be careful that the program runs only once. +Alternatively, s10m can be managed by systemd. To do this, copy the file `s10m.service` to the systemd directory: +``` +sudo cp -a s10m.service /etc/systemd/system/ +``` +Configure the service `sudo nano s10m.service` (adjust user 'User=pi'), if needed. + +Register the service and start it with: +``` +sudo systemctl start s10m +sudo systemctl enable s10m +``` + ## Used external libraries - Eclipse Mosquitto (https://github.com/eclipse/mosquitto) diff --git a/config.template b/config.template new file mode 100644 index 0000000..9085b2a --- /dev/null +++ b/config.template @@ -0,0 +1,12 @@ +MODBUS_HOST=e3dc +MODBUS_PORT=502 +MQTT_HOST=mqttbroker +MQTT_PORT=1883 +MQTT_AUTH=false +MQTT_USER= +MQTT_PASSWORD= +MQTT_RETAIN=false +MQTT_QOS=0 +INTERVAL=1 +ROOT_TOPIC=s10 +FORCE=false diff --git a/s10m.c b/s10m.c index 7f9c774..79ad058 100644 --- a/s10m.c +++ b/s10m.c @@ -11,288 +11,385 @@ #include "s10m.h" static struct mosquitto *mosq = NULL; -static int bg; - -static void publish(char *topic, char *payload) -{ - char tbuf[TOPIC_SIZE]; - sprintf(tbuf, "%s/%s", ROOT_TOPIC, topic); - if (mosq && mosquitto_publish(mosq, NULL, tbuf, strlen(payload), payload, MQTT_QOS, MQTT_RETAIN)) { - if (!bg) printf("MQTT connection lost\n"); - mosquitto_disconnect(mosq); - mosquitto_destroy(mosq); - mosq = NULL; - } - if (!bg) printf("MQTT: publish topic >%s< payload >%s<\n", tbuf, payload); +char root_topic[128]; +int mqtt_qos = 0; +int mqtt_retain = 0; +int verbose = 0; + +static void publish(char *topic, char *payload) { + char tbuf[TOPIC_SIZE]; + + sprintf(tbuf, "%s/%s", root_topic, topic); + if (mosq && mosquitto_publish(mosq, NULL, tbuf, strlen(payload), payload, mqtt_qos, mqtt_retain)) { + printf("MQTT connection lost\n"); + fflush(NULL); + mosquitto_disconnect(mosq); + mosquitto_destroy(mosq); + mosq = NULL; + } + if (verbose) printf("MQTT: publish topic >%s< payload >%s<\n", tbuf, payload); } -static void publish_if_changed(int16_t *reg, int16_t *old, int r, char *topic) -{ - char sbuf[BUFFER_SIZE]; - if (old[r] != reg[r]) { - old[r] = reg[r]; +static void publish_number_if_changed(int16_t *reg, int16_t *old, int format, int r, char *topic) { + char sbuf[BUFFER_SIZE]; + + if (old[r] != reg[r]) { + old[r] = reg[r]; + switch (format) { + case F1: { sprintf(sbuf, "%d", reg[r]); - publish(topic, sbuf); + break; + } + case F01: { + sprintf(sbuf, "%.1f", 0.1 * reg[r]); + break; + } + case F001: { + sprintf(sbuf, "%.2f", 0.01 * reg[r]); + break; + } } + publish(topic, sbuf); + } } -int main(int argc, char **argv) -{ - int j, n; - modbus_t *ctx = NULL; - int rc; - char sbuf[BUFFER_SIZE]; - char manufacturer[BUFFER_SIZE]; - char model[BUFFER_SIZE]; - char serial_number[BUFFER_SIZE]; - char firmware[BUFFER_SIZE]; - char bstate[BUFFER_SIZE]; - char gstate[BUFFER_SIZE]; - - int16_t *regs = malloc(MODBUS_NR_REGS * sizeof(int16_t)); - int16_t *olds = malloc(MODBUS_NR_REGS * sizeof(int16_t)); - for (j = 0; j < MODBUS_NR_REGS; j++) olds[j] = 65535; - - strcpy(manufacturer, ""); - strcpy(model, ""); - strcpy(serial_number, ""); - strcpy(firmware, ""); - strcpy(bstate, ""); - strcpy(gstate, ""); - - j = bg = 0; - while (j < argc) { - if (!strcmp(argv[j], "-d")) bg = 1; - j++; - } - - printf("Connecting...\n"); - printf("E3DC system %s:%s (Modbus)\n", MODBUS_HOST, MODBUS_PORT); - printf("MQTT broker %s:%i qos = %i retain = %s\n", MQTT_HOST, MQTT_PORT, MQTT_QOS, MQTT_RETAIN ? "true" : "false"); - printf("Fetching data every "); - if (POLL_INTERVAL == 1) printf("second.\n"); else printf("%i seconds.\n", POLL_INTERVAL); - printf("\n"); - - if (bg) { - printf("...working as a daemon.\n"); - pid_t pid, sid; - pid = fork(); - if (pid < 0) - exit(EXIT_FAILURE); - if (pid > 0) - exit(EXIT_SUCCESS); - umask(0); - sid = setsid(); - if (sid < 0) - exit(EXIT_FAILURE); - if ((chdir("/")) < 0) - exit(EXIT_FAILURE); - close(STDIN_FILENO); - close(STDOUT_FILENO); - close(STDERR_FILENO); - } - - mosquitto_lib_init(); - - while (1) { - // MQTT connection - if (!mosq) { - mosq = mosquitto_new(NULL, true, NULL); - if (!bg) fprintf(stderr, "Connecting to MQTT broker %s:%i\n", MQTT_HOST, MQTT_PORT); - if (mosq) { - if (!mosquitto_connect(mosq, MQTT_HOST, MQTT_PORT, 10)) { - if (!bg) fprintf(stderr, "MQTT: Connected successfully\n"); - } else { - if (!bg) fprintf(stderr, "MQTT: Connection failed\n"); - mosquitto_destroy(mosq); - mosq = NULL; - sleep(1); - continue; - } - } - } - - // modbus connection - if (ctx == NULL) { - ctx = modbus_new_tcp_pi(MODBUS_HOST, MODBUS_PORT); - - if (!ctx) { - if (!bg) fprintf(stderr, "E3DC_MODBUS: Unable to allocate libmodbus context\n"); - sleep(1); - continue; - } - if (modbus_connect(ctx) == -1) { - if (!bg) fprintf(stderr, "E3DC_MODBUS: Connection failed: %s\n",modbus_strerror(errno)); - if (ctx) modbus_free(ctx); - ctx = NULL; - sleep(1); - continue; - } - modbus_set_debug(ctx, FALSE); - modbus_set_response_timeout(ctx, 1, 0); // 1 second timeout - modbus_set_error_recovery(ctx, MODBUS_ERROR_RECOVERY_LINK|MODBUS_ERROR_RECOVERY_PROTOCOL); - - if (!bg) fprintf(stderr,"E3DC_MODBUS: Connected successfully\n"); - } +static void publish_string_if_changed(int16_t *reg, char *lst_value, int r, int length, char *topic) { + char sbuf[BUFFER_SIZE]; + int j, i = 0; + + for (j = r; j < length + r; j++) { + sbuf[i++] = MODBUS_GET_HIGH_BYTE(reg[j]); + sbuf[i++] = MODBUS_GET_LOW_BYTE(reg[j]); + } + if (strcmp(sbuf, lst_value)) { + publish(topic, sbuf); + strcpy(lst_value, sbuf); + } +} - rc = modbus_read_registers(ctx, 0, MODBUS_NR_REGS, ®s[1]); - if (rc != MODBUS_NR_REGS) { - if (!bg) fprintf(stderr,"E3DC_MODBUS: ERROR modbus_read_registers (%d/%d)\n", rc, MODBUS_NR_REGS); - if (ctx) modbus_close(ctx); - if (ctx) modbus_free(ctx); - ctx = NULL; - sleep(1); - continue; - } +static void publish_boolean(int16_t *reg, int r, int bit, int b, char *topic) { + char sbuf[BUFFER_SIZE]; - if (regs[1] != -7204) { - if (!bg) fprintf(stderr,"E3DC_MODBUS: Modbus mode is wrong (%x). Must be E3DC, not SUN_SPEC.\n", regs[1]); - sleep(POLL_INTERVAL); - continue; - } + sprintf(sbuf, "%s", ((reg[r] & b) >> bit)?"true":"false"); + publish(topic, sbuf); +} - if (olds[82] != regs[82]) { - sprintf(sbuf, "%d", MODBUS_GET_HIGH_BYTE(regs[82])+1); - publish("autarky", sbuf); - sprintf(sbuf, "%d", MODBUS_GET_LOW_BYTE(regs[82])+1); - publish("consumption", sbuf); - olds[82] = regs[82]; +int main(int argc, char **argv) { + modbus_t *ctx = NULL; + int rc, bg, i, j; + char tbuf[TOPIC_SIZE]; + char sbuf[BUFFER_SIZE]; + char manufacturer[BUFFER_SIZE]; + char model[BUFFER_SIZE]; + char serial_number[BUFFER_SIZE]; + char firmware[BUFFER_SIZE]; + char bstate[BUFFER_SIZE]; + char gstate[BUFFER_SIZE]; + char key[128], value[128], line[256]; + int reg_nr[2], reg_delta[2], reg_offset[2], wallbox; + char mqtt_host[128]; + char mqtt_user[128]; + char mqtt_password[128]; + char modbus_host[128]; + char modbus_port[128]; + int mqtt_port = 1883; + int mqtt_auth = 0; + int interval = 1; + int force = 0; + int pool_length = sizeof(pool) / sizeof(pool[0]); + + int16_t *regs = malloc((MODBUS_1_NR + MODBUS_2_NR) * sizeof(int16_t)); + int16_t *olds = malloc((MODBUS_1_NR + MODBUS_2_NR) * sizeof(int16_t)); + + for (j = 0; j < (MODBUS_1_NR + MODBUS_2_NR); j++) olds[j] = 2 ^ 15 - 1; + + reg_nr[0] = MODBUS_1_NR; + reg_delta[0] = 1; + reg_offset[0] = MODBUS_1_OFFSET; + reg_nr[1] = MODBUS_2_NR; + reg_delta[1] = MODBUS_1_NR; + reg_offset[1] = MODBUS_2_OFFSET; + + strcpy(modbus_host, "e3dc"); + strcpy(modbus_port, "502"); + strcpy(mqtt_host, "localhost"); + strcpy(root_topic, "s10"); + strcpy(mqtt_user, ""); + strcpy(mqtt_password, ""); + strcpy(manufacturer, ""); + strcpy(model, ""); + strcpy(serial_number, ""); + strcpy(firmware, ""); + strcpy(bstate, ""); + strcpy(gstate, ""); + + j = bg = 0; + while (j < argc) { + if (!strcmp(argv[j], "-d")) bg = 1; + j++; + } + + FILE *fp; + + fp = fopen(CONFIG_FILE, "r"); + if (!fp) + printf("Config file %s not found. Using default values.\n", CONFIG_FILE); + else { + while (fgets(line, sizeof(line), fp)) { + memset(key, 0, sizeof(key)); + memset(value, 0, sizeof(value)); + if (sscanf(line, "%127[^ \t=]=%127[^\n]", key, value) == 2) { + if (!strcasecmp(key, "MODBUS_HOST")) + strcpy(modbus_host, value); + else if (!strcasecmp(key, "MODBUS_PORT")) + strcpy(modbus_port, value); + else if (!strcasecmp(key, "MQTT_HOST")) + strcpy(mqtt_host, value); + else if (!strcasecmp(key, "MQTT_PORT")) + mqtt_port = atoi(value); + else if (!strcasecmp(key, "MQTT_USER")) + strcpy(mqtt_user, value); + else if (!strcasecmp(key, "MQTT_PASSWORD")) + strcpy(mqtt_password, value); + else if (!strcasecmp(key, "MQTT_AUTH") && !strcasecmp(value, "true")) + mqtt_auth = 1; + else if (!strcasecmp(key, "MQTT_RETAIN") && !strcasecmp(value, "true")) + mqtt_retain = 1; + else if (!strcasecmp(key, "MQTT_QOS")) + mqtt_qos = abs(atoi(value)); + if (mqtt_qos > 2) mqtt_qos = 0; + else if (!strcasecmp(key, "INTERVAL")) + interval = abs(atoi(value)); + else if (!strcasecmp(key, "ROOT_TOPIC")) + strncpy(root_topic, value, 24); + else if (!strcasecmp(key, "FORCE") && !strcasecmp(value, "true")) + force = 1; + } + } + fclose(fp); + } + + printf("Connecting...\n"); + printf("E3DC system %s:%s (Modbus)\n", modbus_host, modbus_port); + printf("MQTT broker %s:%i qos = %i retain = %s\n", mqtt_host, mqtt_port, mqtt_qos, mqtt_retain?"true":"false"); + printf("Fetching data every "); + if (interval == 1) printf("second.\n"); else printf("%i seconds.\n", interval); + printf("Force mode = %s\n", force?"true":"false"); + if (isatty(STDOUT_FILENO)) { + printf("Stdout to terminal\n"); + verbose = 1; + } else { + printf("Stdout to pipe/file\n"); + bg = 0; + } + printf("\n"); + fflush(NULL); + + if (bg) { + printf("...working as a daemon.\n"); + pid_t pid, sid; + pid = fork(); + if (pid < 0) + exit(EXIT_FAILURE); + if (pid > 0) + exit(EXIT_SUCCESS); + umask(0); + sid = setsid(); + if (sid < 0) + exit(EXIT_FAILURE); + if ((chdir("/")) < 0) + exit(EXIT_FAILURE); + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + } + + mosquitto_lib_init(); + + while (1) { + // MQTT connection + if (!mosq) { + mosq = mosquitto_new(NULL, true, NULL); + printf("Connecting to MQTT broker %s:%i\n", mqtt_host, mqtt_port); + fflush(NULL); + if (mosq) { + if (mqtt_auth && strcmp(mqtt_user, "") && strcmp(mqtt_password, "")) mosquitto_username_pw_set(mosq, mqtt_user, mqtt_password); + if (!mosquitto_connect(mosq, mqtt_host, mqtt_port, 10)) { + printf("MQTT: Connected successfully\n"); + fflush(NULL); + } else { + printf("MQTT: Connection failed\n"); + fflush(NULL); + mosquitto_destroy(mosq); + mosq = NULL; + sleep(1); + continue; } + } + } - if (olds[85] != regs[85]) { - sprintf(sbuf, "%d", (regs[85]&8)>>3); - publish("battery/charging/limit", sbuf); - sprintf(sbuf, "%d", regs[85]&1); - publish("battery/charging/lock", sbuf); - sprintf(sbuf, "%d", (regs[85]&2)>>1); - publish("battery/discharging/lock", sbuf); - sprintf(sbuf, "%d", (regs[85]&4)>>2); - publish("emergency/ready", sbuf); - sprintf(sbuf, "%d", (regs[85]&16)>>4); - publish("grid/limit", sbuf); - olds[85] = regs[85]; - } + // modbus connection + if (ctx == NULL) { + ctx = modbus_new_tcp_pi(modbus_host, modbus_port); + + if (!ctx) { + printf("E3DC_MODBUS: Unable to allocate libmodbus context\n"); + fflush(NULL); + sleep(1); + continue; + } + if (modbus_connect(ctx) == -1) { + printf("E3DC_MODBUS: Connection failed: %s\n", modbus_strerror(errno)); + fflush(NULL); + if (ctx) modbus_free(ctx); + ctx = NULL; + sleep(1); + continue; + } + modbus_set_debug(ctx, FALSE); + modbus_set_response_timeout(ctx, 1, 0); // 1 second timeout + modbus_set_error_recovery(ctx, MODBUS_ERROR_RECOVERY_LINK | MODBUS_ERROR_RECOVERY_PROTOCOL); + + printf("E3DC_MODBUS: Connected successfully\n"); + fflush(NULL); + } - publish_if_changed(regs, olds, 83, "battery/soc"); - - if (olds[70] != regs[70]) { - publish_if_changed(regs, olds, 70, "battery/power"); - if (regs[71] >= 0) { - if ((regs[70] == 0) && (regs[83] == 0)) { - if (strcmp(bstate, "EMPTY")) { - publish("battery/state", "EMPTY"); - strcpy(bstate, "EMPTY"); - } - } else if ((regs[70] == 0) && (regs[83] == 100)) { - if (strcmp(bstate, "FULL")) { - publish("battery/state", "FULL"); - strcpy(bstate, "FULL"); - } - } else if (regs[70] == 0) { - if (strcmp(bstate, "PENDING")) { - publish("battery/state", "PENDING"); - strcpy(bstate, "PENDING"); - } - } else { - if (strcmp(bstate, "CHARGING")) { - publish("battery/state", "CHARGING"); - strcpy(bstate, "CHARGING"); - } - } - } else { - if (strcmp(bstate, "DISCHARGING")) { - publish("battery/state", "DISCHARGING"); - strcpy(bstate, "DISCHARGING"); - } - } - } + if (force) for (j = 0; j < (MODBUS_1_NR + MODBUS_2_NR); j++) olds[j] = 2 ^ 15 - 1; + + for (i = 0; i < 2; i++) { + rc = modbus_read_registers(ctx, reg_offset[i], reg_nr[i], ®s[reg_delta[i]]); + if (rc != reg_nr[i]) { + printf("E3DC_MODBUS: ERROR modbus_read_registers at %d (%d/%d)\n", reg_delta[i], rc, reg_nr[i]); + fflush(NULL); + if (ctx) modbus_close(ctx); + if (ctx) modbus_free(ctx); + ctx = NULL; + sleep(1); + continue; + } + } - if (olds[84] != regs[84]) { - if (regs[84] == 1) { - publish("emergency/mode", "ACTIVE"); - } else if (regs[84] == 2) { - publish("emergency/mode", "INACTIVE"); - } else { - publish("emergency/mode", "N/A"); - } - olds[84] = regs[84]; - } + if (regs[1] != -7204) { + printf("E3DC_MODBUS: Modbus mode is wrong (%x). Must be E3DC, not SUN_SPEC.\n", regs[1]); + fflush(NULL); + sleep(interval); + continue; + } - n = 0; - for (j = 52; j < 16+52; j++) {sbuf[n++] = MODBUS_GET_HIGH_BYTE(regs[j]); sbuf[n++] = MODBUS_GET_LOW_BYTE(regs[j]);} - if (strcmp(sbuf, firmware)) { - publish("firmware", sbuf); - strcpy(firmware, sbuf); - } + if (olds[82] != regs[82]) { + sprintf(sbuf, "%d", MODBUS_GET_HIGH_BYTE(regs[82]) + 1); + publish("autarky", sbuf); + sprintf(sbuf, "%d", MODBUS_GET_LOW_BYTE(regs[82]) + 1); + publish("consumption", sbuf); + olds[82] = regs[82]; + } - if (olds[74] != regs[74]) { - publish_if_changed(regs, olds, 74, "grid/power"); - if (regs[75] < 0) { - if (strcmp(gstate, "IN")) { - publish("grid/state", "IN"); - strcpy(gstate, "IN"); - } - } else { - if (strcmp(gstate, "OUT")) { - publish("grid/state", "OUT"); - strcpy(gstate, "OUT"); - } - } - } + if (olds[85] != regs[85]) { + publish_boolean(regs, 85, 0, 1, "battery/charging/lock"); + publish_boolean(regs, 85, 1, 2, "battery/discharging/lock"); + publish_boolean(regs, 85, 2, 4, "emergency/ready"); + publish_boolean(regs, 85, 3, 8, "battery/weather_regulation"); + publish_boolean(regs, 85, 4, 16, "grid/limit"); + publish_boolean(regs, 85, 5, 32, "idle_period/charging/active"); + publish_boolean(regs, 85, 6, 64, "idle_period/discharging/active"); + olds[85] = regs[85]; + } - publish_if_changed(regs, olds, 106, "grid/power/L1"); - publish_if_changed(regs, olds, 107, "grid/power/L2"); - publish_if_changed(regs, olds, 108, "grid/power/L3"); + if (regs[88] & 1) { + publish_number_if_changed(regs, olds, F1, 78, "wallbox/total/power"); + publish_number_if_changed(regs, olds, F1, 80, "wallbox/solar/power"); + } - publish_if_changed(regs, olds, 72, "home/power"); + for (wallbox = 0; wallbox < 8; wallbox++) { + if ((regs[88 + wallbox] & 1) && (olds[88 + wallbox] != regs[88 + wallbox])) { + sprintf(tbuf, "wallbox/%d/available", wallbox); + publish_boolean(regs, 88 + wallbox, 0, 1, tbuf); + sprintf(tbuf, "wallbox/%d/sun_mode", wallbox); + publish_boolean(regs, 88 + wallbox, 1, 2, tbuf); + sprintf(tbuf, "wallbox/%d/ready", wallbox); + publish_boolean(regs, 88 + wallbox, 2, 4, tbuf); + sprintf(tbuf, "wallbox/%d/charging", wallbox); + publish_boolean(regs, 88 + wallbox, 3, 8, tbuf); + sprintf(tbuf, "wallbox/%d/1phase", wallbox); + publish_boolean(regs, 88 + wallbox, 12, 4096, tbuf); + olds[88 + wallbox] = regs[88 + wallbox]; + } + } - n = 0; - for (j = 4; j < 16+4; j++) {sbuf[n++] = MODBUS_GET_HIGH_BYTE(regs[j]); sbuf[n++] = MODBUS_GET_LOW_BYTE(regs[j]);} - if (strcmp(sbuf, manufacturer)) { - publish("manufacturer", sbuf); - strcpy(manufacturer, sbuf); + if (olds[70] != regs[70]) { + if (regs[71] >= 0) { + if ((regs[70] == 0) && (regs[83] == 0)) { + if (strcmp(bstate, "EMPTY")) { + publish("battery/state", "EMPTY"); + strcpy(bstate, "EMPTY"); + } + } else if ((regs[70] == 0) && (regs[83] == 100)) { + if (strcmp(bstate, "FULL")) { + publish("battery/state", "FULL"); + strcpy(bstate, "FULL"); + } + } else if (regs[70] == 0) { + if (strcmp(bstate, "PENDING")) { + publish("battery/state", "PENDING"); + strcpy(bstate, "PENDING"); + } + } else { + if (strcmp(bstate, "CHARGING")) { + publish("battery/state", "CHARGING"); + strcpy(bstate, "CHARGING"); + } } - - n = 0; - for (j = 20; j < 16+20; j++) {sbuf[n++] = MODBUS_GET_HIGH_BYTE(regs[j]); sbuf[n++] = MODBUS_GET_LOW_BYTE(regs[j]);} - if (strcmp(sbuf, model)) { - publish("model", sbuf); - strcpy(model, sbuf); + } else { + if (strcmp(bstate, "DISCHARGING")) { + publish("battery/state", "DISCHARGING"); + strcpy(bstate, "DISCHARGING"); } + } + } - n = 0; - for (j = 36; j < 16+36; j++) {sbuf[n++] = MODBUS_GET_HIGH_BYTE(regs[j]); sbuf[n++] = MODBUS_GET_LOW_BYTE(regs[j]);} - if (strcmp(sbuf, serial_number)) { - publish("serial_number", sbuf); - strcpy(serial_number, sbuf); - } + if (olds[84] != regs[84]) { + if (regs[84] == 1) + publish("emergency/mode", "ACTIVE"); + else if (regs[84] == 2) + publish("emergency/mode", "INACTIVE"); + else if (regs[84] == 4) + publish("emergency/mode", "CHECK_MOTOR_SWITCH"); + else + publish("emergency/mode", "N/A"); + olds[84] = regs[84]; + } - publish_if_changed(regs, olds, 68, "solar/power"); + publish_string_if_changed(regs, &manufacturer[0], 4, 16, "manufacturer"); + publish_string_if_changed(regs, &model[0], 20, 16, "model"); + publish_string_if_changed(regs, &serial_number[0], 36, 16, "serial_number"); + publish_string_if_changed(regs, &firmware[0], 52, 16, "firmware"); - if (olds[99] != regs[99]) { - sprintf(sbuf, "%.2f", 0.01*regs[99]); - publish("string_1/current", sbuf); - olds[99] = regs[99]; + if (olds[74] != regs[74]) { + if (regs[75] < 0) { + if (strcmp(gstate, "IN")) { + publish("grid/state", "IN"); + strcpy(gstate, "IN"); } - if (olds[100] != regs[100]) { - sprintf(sbuf, "%.2f", 0.01*regs[100]); - publish("string_2/current", sbuf); - olds[100] = regs[100]; + } else { + if (strcmp(gstate, "OUT")) { + publish("grid/state", "OUT"); + strcpy(gstate, "OUT"); } + } + } - publish_if_changed(regs, olds, 102, "string_1/power"); - publish_if_changed(regs, olds, 103, "string_2/power"); - publish_if_changed(regs, olds, 96, "string_1/voltage"); - publish_if_changed(regs, olds, 97, "string_2/voltage"); - - sleep(POLL_INTERVAL); + for (i = 0; i < pool_length; i++) { + publish_number_if_changed(regs, olds, pool[i].format, pool[i].reg, pool[i].topic); } - if (ctx) modbus_close(ctx); - if (ctx) modbus_free(ctx); - if (regs) free(regs); - if (olds) free(olds); - mosquitto_lib_cleanup(); + sleep(interval); + } + if (ctx) modbus_close(ctx); + if (ctx) modbus_free(ctx); + if (regs) free(regs); + if (olds) free(olds); + + mosquitto_lib_cleanup(); - exit(0); + exit(EXIT_SUCCESS); } diff --git a/s10m.h b/s10m.h index 62f3d4b..4a45fd8 100644 --- a/s10m.h +++ b/s10m.h @@ -1,22 +1,70 @@ #ifndef __S10M_H_ #define __S10M_H_ -/* configurations */ -#define MQTT_HOST "localhost" -#define MQTT_PORT 1883 -#define MQTT_QOS 0 -#define MQTT_RETAIN false -#define ROOT_TOPIC "s10" -#define MODBUS_HOST "e3dc" -#define MODBUS_PORT "502" -#define POLL_INTERVAL 1 - -/* constants */ -#define MODBUS_NR_REGS 120 -#define BUFFER_SIZE 32 -#define TOPIC_SIZE 128 +#define CONFIG_FILE ".config" + +#define MODBUS_1_NR 110 +#define MODBUS_1_OFFSET 0 +#define MODBUS_2_NR 34 +#define MODBUS_2_OFFSET 1000 +#define REGISTER_41000 MODBUS_1_NR + +#define BUFFER_SIZE 32 +#define TOPIC_SIZE 128 + +#define F1 0 +#define F01 1 +#define F001 2 static void publish(char *topic, char *payload); -static void publish_if_changed(int16_t *reg, int16_t *old, int r, char *topic); +static void publish_number_if(int16_t *reg, int16_t *old, int format, int r, char *topic); +static void publish_string_if_changed(int16_t *reg, char *lst_value, int r, int length, char *topic); +static void publish_boolean(int16_t *reg, int r, int bit, int b, char *topic); + +typedef struct _pool_t { + int reg; + int format; + char topic[TOPIC_SIZE]; +} pool_t; + +pool_t pool[] = { + { 83, F1, "battery/soc" }, + { 70, F1, "battery/power" }, + { 74, F1, "grid/power" }, + { 106, F1, "grid/power/L1" }, + { 107, F1, "grid/power/L2" }, + { 108, F1, "grid/power/L3" }, + { 72, F1, "home/power" }, + { 76, F1, "addon/power" }, + { 68, F1, "solar/power" }, + { 99, F001, "current/string_1" }, + { 100, F001, "current/string_2" }, + { 102, F1, "power/string_1" }, + { 103, F1, "power/string_2" }, + { 96, F1, "voltage/string_1" }, + { 97, F1, "voltage/string_2" }, + { REGISTER_41000 + 0, F1, "pvi/apparent_power/L1" }, + { REGISTER_41000 + 2, F1, "pvi/apparent_power/L2" }, + { REGISTER_41000 + 4, F1, "pvi/apparent_power/L3" }, + { REGISTER_41000 + 6, F1, "pvi/active_power/L1" }, + { REGISTER_41000 + 8, F1, "pvi/active_power/L2" }, + { REGISTER_41000 + 10, F1, "pvi/active_power/L3" }, + { REGISTER_41000 + 12, F1, "pvi/reactive_power/L1" }, + { REGISTER_41000 + 14, F1, "pvi/reactive_power/L2" }, + { REGISTER_41000 + 16, F1, "pvi/reactive_power/L3" }, + { REGISTER_41000 + 18, F01, "pvi/voltage/L1" }, + { REGISTER_41000 + 19, F01, "pvi/voltage/L2" }, + { REGISTER_41000 + 20, F01, "pvi/voltage/L3" }, + { REGISTER_41000 + 21, F001, "pvi/current/L1" }, + { REGISTER_41000 + 22, F001, "pvi/current/L2" }, + { REGISTER_41000 + 23, F001, "pvi/current/L3" }, + { REGISTER_41000 + 24, F001, "grid/frequency" }, + { REGISTER_41000 + 25, F1, "pvi/power/string_1" }, + { REGISTER_41000 + 26, F1, "pvi/power/string_2" }, + { REGISTER_41000 + 28, F01, "pvi/voltage/string_1" }, + { REGISTER_41000 + 29, F01, "pvi/voltage/string_2" }, + { REGISTER_41000 + 31, F001, "pvi/current/string_1" }, + { REGISTER_41000 + 32, F001, "pvi/current/string_2" } +}; #endif diff --git a/s10m.service b/s10m.service new file mode 100644 index 0000000..0a179d8 --- /dev/null +++ b/s10m.service @@ -0,0 +1,18 @@ +[Unit] +Description=s10m service +After=network.target +After=systemd-user-sessions.service +After=network-online.target + +[Service] +User=pi +WorkingDirectory=/opt/s10m +ExecStart=/opt/s10m/s10m +ExecStop=pkill s10m +Restart=on-failure +RestartSec=30 +StartLimitInterval=350 +StartLimitBurst=10 + +[Install] +WantedBy=multi-user.target