FM80 solar charge controller datalogger

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Home Assistant is brilliant.

I'm tidying up some error checking and mode logic for the lINUX C program that controls the advanced energy buffering, time-shifting and energy flow functions. Needed a way to easily and reliably make sure the program is actually sending data via MQTT to HA so it runs in the correct mode. The simplest way I found was to simply use the HA MQTT 'message received' trigger' to set a HA 'counter' to 1 when it receives data from the energy program in a HA Connected automation and another write HA Disconnected automation to set the counter to 0 when the HA Connected automation trigger (update time set when MQTT data is received) has not happened for X seconds.
1729471569949.png
1729471631549.png
1729471718675.png
1729472146940.png

https://www.home-assistant.io/docs/automation/templating/#available-this-data

All done with the GUI.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Latest software update for the Linux ha_energy solar hybrid control program that interfaces with Home Assistant, the Q84 FM80 interface and K42 load control system boards.
https://github.com/nsaspook/ha_energy/tree/fsm_main

energy.c with the 'main' program function
C:
/*
 * HA Energy control using MQTT JSON and HTTP format data from various energy monitor sources
 * asynchronous mode using threads in a background process
 * 
 * long life HA token: 
 *
 * This file may be freely modified, distributed, and combined with
 * other software, as long as proper attribution is given in the
 * source code.
 * Daemon example code:
 * https://github.com/pasce/daemon-skeleton-linux-c
 */
#define _DEFAULT_SOURCE
#include "ha_energy/energy.h"
#include "ha_energy/mqtt_rec.h"
#include "ha_energy/bsoc.h"

/*
 * V0.25 add Home Assistant Matter controlled utility power control switching
 * V0.26 BSOC weights for system condition for power diversion
 * V0.27 -> V0.28 GTI power ramps stability using battery current STD DEV
 * V0.29 log date-time and spam control
 * V0.30 add iammeter http data reading and processing
 * V0.31 refactor http code and a few vars
 * V0.32 AC and GTI power triggers reworked
 * V0.33 refactor system parms into energy structure energy_type E
 * V0.34 GTI and AC Inverter battery energy run down limits adjustments per energy usage and solar production
 * V0.35 more refactors and global variable consolidation
 * V0.36 more command repeat fixes for ramp up/down dumpload commands
 * V0.37 Power feedback to use PV power to GTI and AC loads
 * V0.38 signal filters to smooth large power swings in control optimization
 * V0.39 fix optimizer bugs and add AC load switching set-points in BSOC control
 * V0.40 shutdown and restart fixes
 * V0.41 fix errors and warning per cppcheck
 * V0.42 fake ac charger for dumpload using FAKE_VPV define
 * V0.43 adjust PV_BIAS per float or charging status
 * V0.44 tune for spring/summer solar conditions
 * V0.50 convert main loop code to FSM
 * V0.51 logging time additions
 * V0.52 tune GTI inverter levels for better conversion efficiency
 * V0.53 sync to HA back-end switch status
 * V0.54 data source shutdown functions
 * V0.55 off-grid inverter power tracking for HA
 * V0.56 run as Daemon in background
 * V0.62 adjust battery critical to keep making energy calculations
 * V0.63 add IP address logging
 * V0.64 Dump Load excess load mode programming
 * V.065 DL excess logic tuning and power adjustments
 * V.066 -> V.068 Various timing fixes to reduce spamming commands and logs
 * V.069 send MQTT showdown commands to HA when critical energy conditions are meet
 * V.070 process Home Assistant MQTT commands sent from automations
 * V.071 comment additions, logging improvements and code cleanups
 */

/*
 * for the publish and subscribe topic pair
 * passed as a context variable
 */
// for local device Comedi hardware I/O device control
struct ha_flag_type ha_flag_vars_pc = {
    .runner = false,
    .receivedtoken = false,
    .deliveredtoken = false,
    .rec_ok = false,
    .ha_id = P8055_ID,
    .var_update = 0,
};

// solar data from mateq84
struct ha_flag_type ha_flag_vars_ss = {
    .runner = false,
    .receivedtoken = false,
    .deliveredtoken = false,
    .rec_ok = false,
    .ha_id = FM80_ID,
    .var_update = 0,
    .energy_mode = NORM_MODE,
};

// dumpload data from mbmc_k42
struct ha_flag_type ha_flag_vars_sd = {
    .runner = false,
    .receivedtoken = false,
    .deliveredtoken = false,
    .rec_ok = false,
    .ha_id = DUMPLOAD_ID,
    .var_update = 0,
};

// data from HA
struct ha_flag_type ha_flag_vars_ha = {
    .runner = false,
    .receivedtoken = false,
    .deliveredtoken = false,
    .rec_ok = false,
    .ha_id = HA_ID,
    .var_update = 0,
};

// Comedi I/O device type
const char *board_name = "NO_BOARD", *driver_name = "NO_DRIVER";

FILE* fout; // logging stream

// energy state structure
struct energy_type E = {
    .once_gti = true,
    .once_ac = true,
    .once_gti_zero = true,
    .iammeter = false,
    .fm80 = false,
    .dumpload = false,
    .homeassistant = false,
    .ac_low_adj = 0.0f,
    .gti_low_adj = 0.0f,
    .ac_sw_on = true,
    .gti_sw_on = true,
    .im_delay = 0,
    .gti_delay = 0,
    .im_display = 0,
    .rc = 0,
    .speed_go = 0,
    .mode.pid.iMax = PV_IMAX,
    .mode.pid.iMin = 0.0f,
    .mode.pid.pGain = PV_PGAIN,
    .mode.pid.iGain = PV_IGAIN,
    .mode.mode_tmr = 0,
    .mode.mode = true,
    .mode.in_pid_control = false,
    .mode.dl_mqtt_max = PV_DL_MPTT_MAX,
    .mode.E = E_INIT,
    .mode.R = R_INIT,
    .mode.no_float = true,
    .mode.data_error = false,
    .ac_sw_status = false,
    .gti_sw_status = false,
    .solar_mode = false,
    .solar_shutdown = false,
    .mode.pv_bias = PV_BIAS_LOW,
    .sane = S_DLAST,
    .startup = true,
    .ac_mismatch = false,
    .dc_mismatch = false,
    .mode_mismatch = false,
    .link.shutdown = 0,
    .mode.bat_crit = false,
    .dl_excess = false,
    .dl_excess_adj = 0.0f,
};

static bool solar_shutdown(void);
void showIP(void);

/*
 * show all assigned networking addresses and types
 * on the current machine
 */
void showIP(void)
{
    struct ifaddrs *ifaddr, *ifa;
    int s;
    char host[NI_MAXHOST];

    if (getifaddrs(&ifaddr) == -1) {
        perror("getifaddrs");
        exit(EXIT_FAILURE);
    }


    for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
        if (ifa->ifa_addr == NULL)
            continue;

        s = getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in), host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);

        if (ifa->ifa_addr->sa_family == AF_INET) {
            if (s != 0) {
                exit(EXIT_FAILURE);
            }
            printf("\tInterface : <%s>\n", ifa->ifa_name);
            printf("\t  Address : <%s>\n", host);
        }
    }

    freeifaddrs(ifaddr);
}

/*
 * setup ha_energy program to run as a background deamon
 * disconnect and exit foreground startup process
 */
static void skeleton_daemon()
{
    pid_t pid;

    /* Fork off the parent process */
    pid = fork();

    /* An error occurred */
    if (pid < 0) {
        printf("\r\n%sDAEMON failure  LOG Version %s : MQTT Version %s\r\n", log_time(false), LOG_VERSION, MQTT_VERSION);
        exit(EXIT_FAILURE);
    }

    /* Success: Let the parent terminate */
    if (pid > 0) {
        exit(EXIT_SUCCESS);
    }

    /* On success: The child process becomes session leader */
    if (setsid() < 0) {
        exit(EXIT_FAILURE);
    }

    /* Catch, ignore and handle signals */
    /*TODO: Implement a working signal handler */
    //    signal(SIGCHLD, SIG_IGN);
    //    signal(SIGHUP, SIG_IGN);

    /* Fork off for the second time*/
    pid = fork();

    /* An error occurred */
    if (pid < 0) {
        exit(EXIT_FAILURE);
    }

    /* Success: Let the parent terminate */
    if (pid > 0) {
        exit(EXIT_SUCCESS);
    }

    /* Set new file permissions */
    umask(0);

    /* Change the working directory to the root directory */
    /* or another appropriated directory */
    chdir("/");

    /* Close all open file descriptors */
    int x;
    for (x = sysconf(_SC_OPEN_MAX); x >= 0; x--) {
        close(x);
    }

}

/*
 * check for sensor range errors for some critical data points
 * look for bad data on the high side
 */
bool sanity_check(void)
{
    if (E.mvar[V_PWA] > PWA_SANE) {
        E.sane = S_PWA;
        return false;
    }
    if (E.mvar[V_PAMPS] > PAMPS_SANE) {
        E.sane = S_PAMPS;
        return false;
    }
    if (E.mvar[V_PVOLTS] > PVOLTS_SANE) {
        E.sane = S_PVOLTS;
        return false;
    }
    if (E.mvar[V_FBAMPS] > BAMPS_SANE) {
        E.sane = S_FBAMPS;
        return false;
    }
    return true;
}

/*
 * Async processing threads
 */

/*
 * data update timer flag
 * and 10 second software time clock
 */
void timer_callback(int32_t signum)
{
    signal(signum, timer_callback);
    ha_flag_vars_ss.runner = true;
    E.ten_sec_clock++;
    E.log_spam++;
    E.log_time_reset++;
    if (E.log_spam > MAX_LOG_SPAM) {
        E.log_spam = 0;
    }
}

/*
 * MQTT Broker errors are fatal
 */
void connlost(void *context, char *cause)
{
    struct ha_flag_type *ha_flag = context;
    int32_t id_num;

    // bug-out if no context variables passed to callback
    if (context == NULL) {
        id_num = -1;
    } else {
        id_num = ha_flag->ha_id;
    }
    fprintf(fout, "\n%s Connection lost, exit ha_energy program\n", log_time(false));
    fprintf(fout, "%s     cause: %s, %d\n", log_time(false), cause, id_num);
    fprintf(fout, "%sDAEMON failure  LOG Version %s : MQTT Version %s\n", log_time(false), LOG_VERSION, MQTT_VERSION);
    fflush(fout);
    exit(EXIT_FAILURE);
}

/*
 * HA_ENERGY
 * 
 * Use MQTT/HTTP to send/receive updates to Solar hardware devices
 * and control energy in an optimized fashion to reduce electrical energy costs
 */
int main(int argc, char *argv[])
{
    struct itimerval new_timer = {
        .it_value.tv_sec = CMD_SEC,
        .it_value.tv_usec = 0,
        .it_interval.tv_sec = CMD_SEC,
        .it_interval.tv_usec = 0,
    };
    struct itimerval old_timer;
    time_t rawtime;
    MQTTClient_connectOptions conn_opts_p = MQTTClient_connectOptions_initializer,
        conn_opts_sd = MQTTClient_connectOptions_initializer,
        conn_opts_ha = MQTTClient_connectOptions_initializer;
    MQTTClient_message pubmsg = MQTTClient_message_initializer;
    MQTTClient_deliveryToken token;
    char hname[256], *hname_ptr = hname;
    size_t hname_len = 12;

    gethostname(hname, hname_len);
    hname[12] = 0;
    printf("\r\n  LOG Version %s : MQTT Version %s : Host Name %s\r\n", LOG_VERSION, MQTT_VERSION, hname);
    showIP();
    skeleton_daemon();

    while (true) {
        switch (E.mode.E) {
        case E_INIT:

#ifdef LOG_TO_FILE
            fout = fopen(LOG_TO_FILE, "a");
            if (fout == NULL) {
                fout = fopen(LOG_TO_FILE_ALT, "a");
                if (fout == NULL) {
                    fout = stdout;
                    printf("\r\n%s Unable to open LOG file %s \r\n", log_time(false), LOG_TO_FILE_ALT);
                }
            }
#else
            fout = stdout;
#endif
            fprintf(fout, "\r\n%s LOG Version %s : MQTT Version %s\r\n", log_time(false), LOG_VERSION, MQTT_VERSION);
            fflush(fout);

            if (!bsoc_init()) {
                fprintf(fout, "\r\n%s bsoc_init failure \r\n", log_time(false));
                fflush(fout);
                exit(EXIT_FAILURE);
            }
            /*
             * set the timer for MQTT publishing sample speed
             * CMD_SEC         10
             */
            setitimer(ITIMER_REAL, &new_timer, &old_timer);
            signal(SIGALRM, timer_callback);

            if (strncmp(hname, TNAME, 6) == 0) {
                MQTTClient_create(&E.client_p, LADDRESS, CLIENTID1,
                    MQTTCLIENT_PERSISTENCE_NONE, NULL);
                conn_opts_p.keepAliveInterval = 20;
                conn_opts_p.cleansession = 1;
                hname_ptr = LADDRESS;
            } else {
                MQTTClient_create(&E.client_p, ADDRESS, CLIENTID1,
                    MQTTCLIENT_PERSISTENCE_NONE, NULL);
                conn_opts_p.keepAliveInterval = 20;
                conn_opts_p.cleansession = 1;
                hname_ptr = ADDRESS;
            }

            fprintf(fout, "%s Connect MQTT server %s, %s\n", log_time(false), hname_ptr, CLIENTID1);
            fflush(fout);
            MQTTClient_setCallbacks(E.client_p, &ha_flag_vars_ss, connlost, msgarrvd, delivered);
            if ((E.rc = MQTTClient_connect(E.client_p, &conn_opts_p)) != MQTTCLIENT_SUCCESS) {
                fprintf(fout, "%s Failed to connect MQTT server, return code %d %s, %s\n", log_time(false), E.rc, hname_ptr, CLIENTID1);
                fflush(fout);
                pthread_mutex_destroy(&E.ha_lock);
                exit(EXIT_FAILURE);
            }

            if (strncmp(hname, TNAME, 6) == 0) {
                MQTTClient_create(&E.client_sd, LADDRESS, CLIENTID2,
                    MQTTCLIENT_PERSISTENCE_NONE, NULL);
                conn_opts_sd.keepAliveInterval = 20;
                conn_opts_sd.cleansession = 1;
                hname_ptr = LADDRESS;
            } else {
                MQTTClient_create(&E.client_sd, ADDRESS, CLIENTID2,
                    MQTTCLIENT_PERSISTENCE_NONE, NULL);
                conn_opts_sd.keepAliveInterval = 20;
                conn_opts_sd.cleansession = 1;
                hname_ptr = ADDRESS;
            }

            fprintf(fout, "%s Connect MQTT server %s, %s\n", log_time(false), hname_ptr, CLIENTID2);
            fflush(fout);
            MQTTClient_setCallbacks(E.client_sd, &ha_flag_vars_sd, connlost, msgarrvd, delivered);
            if ((E.rc = MQTTClient_connect(E.client_sd, &conn_opts_sd)) != MQTTCLIENT_SUCCESS) {
                fprintf(fout, "%s Failed to connect MQTT server, return code %d %s, %s\n", log_time(false), E.rc, hname_ptr, CLIENTID2);
                fflush(fout);
                pthread_mutex_destroy(&E.ha_lock);
                exit(EXIT_FAILURE);
            }

            /*
             * Home Assistant MQTT receive messages
             */
            if (strncmp(hname, TNAME, 6) == 0) {
                MQTTClient_create(&E.client_ha, LADDRESS, CLIENTID3,
                    MQTTCLIENT_PERSISTENCE_NONE, NULL);
                conn_opts_ha.keepAliveInterval = 20;
                conn_opts_ha.cleansession = 1;
                hname_ptr = LADDRESS;
            } else {
                MQTTClient_create(&E.client_ha, ADDRESS, CLIENTID3,
                    MQTTCLIENT_PERSISTENCE_NONE, NULL);
                conn_opts_ha.keepAliveInterval = 20;
                conn_opts_ha.cleansession = 1;
                hname_ptr = ADDRESS;
            }

            fprintf(fout, "%s Connect MQTT server %s, %s\n", log_time(false), hname_ptr, CLIENTID3);
            fflush(fout);
            MQTTClient_setCallbacks(E.client_ha, &ha_flag_vars_ha, connlost, msgarrvd, delivered);
            if ((E.rc = MQTTClient_connect(E.client_ha, &conn_opts_ha)) != MQTTCLIENT_SUCCESS) {
                fprintf(fout, "%s Failed to connect MQTT server, return code %d %s, %s\n", log_time(false), E.rc, hname_ptr, CLIENTID3);
                fflush(fout);
                pthread_mutex_destroy(&E.ha_lock);
                exit(EXIT_FAILURE);
            }

            /*
             * on topic received data will trigger the msgarrvd function
             */
            MQTTClient_subscribe(E.client_p, TOPIC_SS, QOS); // FM80 Q84
            MQTTClient_subscribe(E.client_sd, TOPIC_SD, QOS); // DUMPLOAD K42
            MQTTClient_subscribe(E.client_ha, TOPIC_HA, QOS); // Home Assistant Linux AMD64  and ARM64

            pubmsg.payload = "online";
            pubmsg.payloadlen = strlen("online");
            pubmsg.qos = QOS;
            pubmsg.retained = 0;
            ha_flag_vars_ss.deliveredtoken = 0;
            // notify HA we are running and controlling AC power plugs
            MQTTClient_publishMessage(E.client_p, TOPIC_PACA, &pubmsg, &token);
            MQTTClient_publishMessage(E.client_p, TOPIC_PDCA, &pubmsg, &token);

            // sync HA power switches
            mqtt_ha_switch(E.client_p, TOPIC_PDCC, false);
            mqtt_ha_switch(E.client_p, TOPIC_PACC, false);
            mqtt_ha_switch(E.client_p, TOPIC_PDCC, true);
            mqtt_ha_switch(E.client_p, TOPIC_PACC, true);
            mqtt_ha_switch(E.client_p, TOPIC_PDCC, false);
            mqtt_ha_switch(E.client_p, TOPIC_PACC, false);

            E.ac_sw_on = true; // can be switched on once
            E.gti_sw_on = true; // can be switched on once

            /*
             * use libcurl to read AC power meter HTTP data
             * iammeter connected for split single phase monitoring and one leg GTI power exporting
             */
            iammeter_read();

            /*
             * start the main energy monitoring loop
             */
            fprintf(fout, "\r\n%s Solar Energy AC power controller\r\n", log_time(false));

#ifdef FAKE_VPV
            fprintf(fout, "\r\n Faking dumpload PV voltage\r\n");
#endif
            ha_flag_vars_ss.energy_mode = NORM_MODE;
            E.mode.E = E_WAIT;
            break;
        case E_WAIT:
            if (ha_flag_vars_ss.runner || E.speed_go++ > 1500000) {
                E.speed_go = 0;
                ha_flag_vars_ss.runner = false;
                E.mode.E = E_RUN;
            }

            usleep(100);
            /*
             * main state-machine update sequence
             */
            bsoc_data_collect();
            if (!sanity_check()) {
                fprintf(fout, "\r\n%s Sanity Check error %d %s \r\n", log_time(false), E.sane, mqtt_name[E.sane]);
                fflush(fout);
            }

            /*
             * stop and restart the energy control processing
             * from inside the program or from a remote Home Assistant command
             */
            if (solar_shutdown()) {
                if (!E.startup) {
                    fprintf(fout, "%s SHUTDOWN Solar Energy Control ---> \r\n", log_time(false));
                }
                fflush(fout);
                ramp_down_gti(E.client_p, true);
                usleep(100000); // wait
                ramp_down_ac(E.client_p, true);
                usleep(100000); // wait
                ramp_down_gti(E.client_p, true);
                usleep(100000); // wait
                ramp_down_ac(E.client_p, true);
                usleep(100000); // wait
                if (!E.startup) {
                    fprintf(fout, "%s Completed SHUTDOWN, Press again to RESTART.\r\n", log_time(false));
                    fflush(fout);
                }
                fflush(fout);

                uint8_t iam_delay = 0;
                while (solar_shutdown()) {
                    mqtt_ha_shutdown(E.client_p, TOPIC_SHUTDOWN);
                    usleep(USEC_SEC); // wait
                    if ((int32_t) E.mvar[V_HACSW]) {
                        ha_ac_off();
                    }
                    if ((int32_t) E.mvar[V_HDCSW]) {
                        ha_dc_off();
                    }
                    if ((iam_delay++ > IAM_DELAY) && E.link.shutdown) {
                        E.fm80 = true;
                        E.dumpload = true;
                        E.iammeter = true;
                        E.homeassistant = true;
                    }
                }
                E.link.shutdown = 0;
                fprintf(fout, "%s RESTART Solar Energy Control\r\n", log_time(false));
                fflush(fout);
                bsoc_set_mode(E.mode.pv_bias, true, true);
                E.dl_excess = true;
                mqtt_gti_power(E.client_p, TOPIC_P, "Z#"); // zero power at startup
                E.dl_excess = false;
#ifdef AUTO_CHARGE
                mqtt_ha_switch(E.client_p, TOPIC_PDCC, true);
#endif
                usleep(100000); // wait
                E.gti_sw_status = true;
                ResetPI(&E.mode.pid);
                ha_flag_vars_ss.runner = true;
                E.fm80 = true;
                E.dumpload = true;
                E.iammeter = true;
                E.homeassistant = true;
                E.mode.in_pid_control = false; // shutdown auto energy control
                E.mode.R = R_INIT;
            }
            if (ha_flag_vars_ss.receivedtoken) {
                ha_flag_vars_ss.receivedtoken = false;
            }
            if (ha_flag_vars_sd.receivedtoken) {
                ha_flag_vars_sd.receivedtoken = false;
            }
            break;
        case E_RUN:
            usleep(100);
            switch (E.mode.R) {
            case R_INIT:
                E.once_ac = true;
                E.once_gti = true;
                E.ac_sw_on = true;
                E.gti_sw_on = true;
                E.mode.R = R_RUN;
                E.mode.no_float = true;
                break;
            case R_FLOAT:
                if (E.mode.no_float) {
                    E.once_ac = true;
                    E.once_gti = true;
                    E.ac_sw_on = true;
                    E.gti_sw_on = true;
                    E.gti_sw_status = false;
                    E.ac_sw_status = false;
                    E.mode.no_float = false;
                }
                if (!E.gti_sw_status) {
                    if (gti_test() > MIN_BAT_KW_GTI_HI) {
                        mqtt_ha_switch(E.client_p, TOPIC_PDCC, true);
                        E.gti_sw_status = true;
                        fprintf(fout, "%s R_FLOAT DC switch true \r\n", log_time(false));
                    }
                }
                usleep(100000); // wait
                if (!E.ac_sw_status) {
                    if (ac_test() > MIN_BAT_KW_AC_HI) {
                        mqtt_ha_switch(E.client_p, TOPIC_PACC, true);
                        E.ac_sw_status = true;
                        fprintf(fout, "%s R_FLOAT AC switch true \r\n", log_time(false));
                    }
                }
                E.mode.pv_bias = PV_BIAS;
                fm80_float(true);
                break;
            case R_RUN:
            default:
                E.mode.R = R_RUN;
                E.mode.no_float = true;
                break;
            }
            /*
             * main state-machine update sequence and control logic
             */
            /*
             * check for idle/data errors flags from sensors and HA
             */
            if (!E.mode.data_error) {
                bsoc_set_mode(E.mode.pv_bias, true, false);
                if (E.gti_delay++ >= GTI_DELAY) {
                    char gti_str[SBUF_SIZ];
                    int32_t error_drive;

                    /*
                     * reset the control mode from simple switched power to PID control
                     */
                    if (!E.mode.in_pid_control) {
                        mqtt_ha_switch(E.client_p, TOPIC_PDCC, true);
                        E.gti_sw_status = true;
                        usleep(100000); // wait
                        mqtt_ha_switch(E.client_p, TOPIC_PACC, true);
                        E.ac_sw_status = true;
                        E.mode.pv_bias = PV_BIAS;
                        fprintf(fout, "%s in_pid_mode AC/DC switch true \r\n", log_time(false));
                        fm80_float(true);
                    } else {
                        if (!fm80_float(true)) {
                            E.mode.pv_bias = (int32_t) E.mode.error - PV_BIAS;
                        }
                    }
                    /*
                     * use PID style set-point error correction
                     */
                    E.mode.in_pid_control = true;
                    E.gti_delay = 0;
                    /*
                     * adjust power balance if battery charging energy is low
                     */
                    if (E.mvar[V_DPBAT] > PV_DL_BIAS_RATE) {
                        error_drive = (int32_t) E.mode.error - E.mode.pv_bias; // PI feedback control signal
                    } else {
                        error_drive = (int32_t) E.mode.error - PV_BIAS_RATE;
                    }
                    /*
                     * when main battery is in float, crank-up the power draw from the solar panels
                     */
                    if (fm80_float(true)) {
                        error_drive = (int32_t) (E.mode.error + PV_BIAS);
                    }
                    /*
                     * don't drive to zero power
                     */
                    if (error_drive < 0) {
                        error_drive = PV_BIAS_LOW; // control wide power swings
                        if (!fm80_sleep()) { // check for using sleep bias
                            if ((E.mvar[V_FBEKW] > MIN_BAT_KW_BSOC_SLP) && (E.mvar[V_PWA] > PWA_SLEEP)) {
                                error_drive = PV_BIAS_SLEEP; // use higher power when we still have sun for better inverter efficiency
                            }
                        }
                    }

                    /*
                     * reduce charging/diversion power to safe PS limits
                     */
                    if (E.mode.dl_mqtt_max > PV_DL_MPTT_MAX) {
                        if (!E.dl_excess) {
                            error_drive = PV_DL_MPTT_IDLE;
                        } else {
                            if (E.mode.dl_mqtt_max > PV_DL_MPTT_EXCESS) {
                                error_drive = PV_DL_MPTT_IDLE;
                            }
                        }
                    } else {
                        if (E.dl_excess) {
                            error_drive = PV_DL_EXCESS + E.dl_excess_adj;
                        }
                    }

                    snprintf(gti_str, SBUF_SIZ - 1, "V%04dX", error_drive); // format for dumpload controller gti power commands
                    mqtt_gti_power(E.client_p, TOPIC_P, gti_str);
                }

#ifndef  FAKE_VPV
                if (fm80_float(true) || ((ac1_filter(E.mvar[V_BEN]) > BAL_MAX_ENERGY_AC) && (ac_test() > MIN_BAT_KW_AC_HI))) {
                    ramp_up_ac(E.client_p, E.ac_sw_on); // use once control
#ifdef PSW_DEBUG
                    fprintf(fout, "%s MIN_BAT_KW_AC_HI AC switch %d \r\n", log_time(false), E.ac_sw_on);
#endif
                    E.ac_sw_on = false; // once flag
                }
#endif
                if (((ac2_filter(E.mvar[V_BEN]) < BAL_MIN_ENERGY_AC) || ((ac_test() < (MIN_BAT_KW_AC_LO + E.ac_low_adj))))) {
                    if (!fm80_float(true)) {
                        ramp_down_ac(E.client_p, E.ac_sw_on);
                        if (log_timer()) {
                            fprintf(fout, "%s RAMP DOWN AC, MIN_BAT_KW_AC_LO AC switch %d \r\n", log_time(false), E.ac_sw_on);
                        }
                    }
                    E.ac_sw_on = true;
                }


                /*
                 * Dump Load Excess testing
                 * send excess power into the home power grid taking care not to export energy to the utility grid
                 */
                if (((dc1_filter(E.mvar[V_BEN]) > BAL_MAX_ENERGY_GTI) && (gti_test() > MIN_BAT_KW_GTI_HI)) || E.dl_excess) {
#ifndef  FAKE_VPV                            
#ifdef B_DLE_DEBUG
                    if (E.dl_excess) {
                        fprintf(fout, "%s DL excess ramp_up_gti, DC switch %d\r\n", log_time(false), E.gti_sw_on);
                    }
#endif
                    ramp_up_gti(E.client_p, E.gti_sw_on, E.dl_excess);
                    if (log_timer()) {
                        fprintf(fout, "%s RAMP DOWN DC, MIN_BAT_KW_GTI_HI DC switch %d \r\n", log_time(false), E.gti_sw_on);
                    }
                    E.gti_sw_on = false; // once flag
#endif                            
                } else {
                    if ((dc2_filter(E.mvar[V_BEN]) < BAL_MIN_ENERGY_GTI) || (gti_test() < (MIN_BAT_KW_GTI_LO + E.gti_low_adj))) {
                        if (!E.dl_excess) {
                            if (log_timer()) {
                                ramp_down_gti(E.client_p, true);
#ifdef PSW_DEBUG
                                fprintf(fout, "%s MIN_BAT_KW_GTI_LO DC switch %d \r\n", log_time(false), E.gti_sw_on);
#endif
                            }
                            E.gti_sw_on = true;
                        }
                    }
                }
            };

#ifdef B_ADJ_DEBUG
            fprintf(fout, "\r\n LO ADJ: AC %8.2fWh, GTI %8.2fWh\r\n", MIN_BAT_KW_AC_LO + E.ac_low_adj, MIN_BAT_KW_GTI_LO + E.gti_low_adj);
#endif
#ifdef B_DLE_DEBUG
            if (E.dl_excess) {
                fprintf(fout, "%s DL excess vars from ha_energy %d %d : Flag %d\r\n", log_time(false), E.mode.con4, E.mode.con5, E.dl_excess);
            }
#endif

            time(&rawtime);

            if (E.im_delay++ >= IM_DELAY) {
                E.im_delay = 0;
                iammeter_read();
            }
            if (E.im_display++ >= IM_DISPLAY) {
                char buffer[SYSLOG_SIZ];
                uint32_t len;

                E.im_display = 0;
                mqtt_ha_pid(E.client_p, TOPIC_PPID);
                if (!(E.fm80 && E.dumpload && E.iammeter)) {
                    if (!E.iammeter) {
                        E.link.iammeter_error++;
                    } else {
                        E.link.mqtt_error++;
                    }
                    E.link.shutdown++;
                    fprintf(fout, "\r\n%s !!!! Source data update error !!!! , check FM80 %i, DUMPLOAD %i, IAMMETER %i channels M %u,%u I %u,%u\r\n", log_time(false), E.fm80, E.dumpload, E.fm80,
                        E.link.mqtt_count, E.link.mqtt_error, E.link.iammeter_count, E.link.iammeter_error);
                    fflush(fout);
                    snprintf(buffer, SYSLOG_SIZ - 1, "\r\n%s !!!! Source data update error !!!! , check FM80 %i, DUMPLOAD %i, IAMMETER %i channels M %u,%u I %u,%u\r\n", log_time(false), E.fm80, E.dumpload, E.fm80,
                        E.link.mqtt_count, E.link.mqtt_error, E.link.iammeter_count, E.link.iammeter_error);
                    syslog(LOG_NOTICE, buffer);
                    mqtt_ha_shutdown(E.client_p, TOPIC_SHUTDOWN);
                    E.mode.data_error = true;
                } else {
                    E.mode.data_error = false;
                    E.link.shutdown = 0;
                }
                snprintf(buffer, RBUF_SIZ - 1, "%s", ctime(&rawtime));
                len = strlen(buffer);
                buffer[len - 1] = 0; // munge out the return character
                fprintf(fout, "%s ", buffer);
                fflush(fout);
                E.fm80 = false;
                E.dumpload = false;
                E.homeassistant = false;
                E.iammeter = false;
                sync_ha();
                print_im_vars();
                print_mvar_vars();
                fprintf(fout, "%s\r", ctime(&rawtime));
            }
            E.mode.E = E_WAIT;
            fflush(fout);
            if (E.mode.con6) {
                E.mode.R = R_IDLE;
            }
            if (E.mode.con7) {
                E.mode.E = E_STOP;
            }
            break;
        case E_STOP:
        default:
            fflush(fout);
            fprintf(fout, "\r\n%s HA Energy stopped and exited.\r\n", log_time(false));
            fflush(fout);
            return 0;
            break;
        }
    }
}

/*
 * send energy to the house grid
 */
void ramp_up_gti(MQTTClient client_p, bool start, bool excess)
{
    static uint32_t sequence = 0;

    if (start) {
        E.once_gti = true;
    }

    if (E.once_gti) {
        E.once_gti = false;
        sequence = 0;
        if (!excess) {
            mqtt_ha_switch(client_p, TOPIC_PDCC, true);
            E.gti_sw_status = true;
            usleep(500000); // wait for voltage to ramp
        } else {
            sequence = 1;
        }
    }

    switch (sequence) {
    case 4:
        E.once_gti_zero = true;
        break;
    case 3:
    case 2:
    case 1:
        E.once_gti_zero = true;
        if (bat_current_stable() || E.dl_excess) { // check battery current std dev, stop 'motorboating'
            sequence++;
            if (!mqtt_gti_power(client_p, TOPIC_P, "+#")) {
                sequence = 0;
            }; // +100W power
        } else {
            usleep(500000); // wait a bit more for power to be stable
            sequence = 1; // do power ramps when ready
            if (!mqtt_gti_power(client_p, TOPIC_P, "-#")) {
                sequence = 0;
            }; // - 100W power
        }
        break;
    case 0:
        sequence++;
        if (E.once_gti_zero) {
            mqtt_gti_power(client_p, TOPIC_P, "Z#"); // zero power
            E.once_gti_zero = false;
        }
        break;
    default:
        if (E.once_gti_zero) {
            mqtt_gti_power(client_p, TOPIC_P, "Z#"); // zero power
            E.once_gti_zero = false;
        }
        sequence = 0;
        break;
    }
}

/*
 * showdown energy to the house grid
 */
void ramp_down_gti(MQTTClient client_p, bool sw_off)
{
    if (sw_off) {
        mqtt_ha_switch(client_p, TOPIC_PDCC, false);
        E.once_gti_zero = true;
        E.gti_sw_status = false;
    }
    E.once_gti = true;

    if (E.once_gti_zero) {
        mqtt_gti_power(client_p, TOPIC_P, "Z#"); // zero power
        E.once_gti_zero = false;
    }
}

/*
 * control power from the off-grid AC inverter
 */
void ramp_up_ac(MQTTClient client_p, bool start)
{

    if (start) {
        E.once_ac = true;
    }

    if (E.once_ac) {
        E.once_ac = false;
        mqtt_ha_switch(client_p, TOPIC_PACC, true);
        E.ac_sw_status = true;
        usleep(500000); // wait for voltage to ramp
    }
}

void ramp_down_ac(MQTTClient client_p, bool sw_off)
{
    if (sw_off) {
        mqtt_ha_switch(client_p, TOPIC_PACC, false);
        E.ac_sw_status = false;
        usleep(500000);
    }
    E.once_ac = true;
}

void ha_ac_off(void)
{
    mqtt_ha_switch(E.client_p, TOPIC_PACC, false);
    E.ac_sw_status = false;
}

void ha_ac_on(void)
{
    mqtt_ha_switch(E.client_p, TOPIC_PACC, true);
    E.ac_sw_status = true;
}

/*
 * control power from the GTI inverters to the house grid
 */
void ha_dc_off(void)
{
    mqtt_ha_switch(E.client_p, TOPIC_PDCC, false);
    E.gti_sw_status = false;
}

void ha_dc_on(void)
{
    mqtt_ha_switch(E.client_p, TOPIC_PDCC, true);
    E.gti_sw_status = true;
}

/*
 * Battery and system protection
 */
static bool solar_shutdown(void)
{
    static bool ret = false;

    if (E.startup) {
        ret = true;
        E.startup = false;
        return ret;
    } else {
        ret = false;

        /*
         * FIXME
         * 
         */
    }

    if (E.solar_shutdown) {
        ret = true;
    } else {
        ret = false;
    }

    if ((E.mvar[V_FBEKW] < BAT_CRITICAL) && !E.startup) { // special case for low battery
        if (!E.mode.bat_crit) {
            ret = true;
#ifdef CRITIAL_SHUTDOWN_LOG
            fprintf(fout, "%s Solar BATTERY CRITICAL shutdown comms check ret = %d \r\n", log_time(false), ret);
            fflush(fout);
#endif
            E.mode.bat_crit = true;
            return ret;
        }
    } else {
        E.mode.bat_crit = false;
    }

    if (E.link.shutdown >= MAX_ERROR) {
        ret = true;
        if (E.fm80 && E.dumpload && E.iammeter) {
            ret = false;
            E.link.shutdown = 0;
        }

#ifdef DEBUG_SHUTDOWN
        fprintf(fout, "%s Solar shutdown comms check ret = %d \r\n", log_time(false), ret);
        fflush(fout);
#endif
    }
    return ret;
}

/*
 * sent the current UTC to the Dump Load controller
 */
char * log_time(bool log)
{
    static char time_log[RBUF_SIZ] = {0};
    static uint32_t len = 0, sync_time = TIME_SYNC_SEC - 1;
    time_t rawtime_log;

    tzset();
    timezone = 0;
    daylight = 0;
    time(&rawtime_log);
    if (sync_time++ > TIME_SYNC_SEC) {
        sync_time = 0;
        snprintf(time_log, RBUF_SIZ - 1, "VT%lut", rawtime_log); // format for dumpload controller gti time commands
        mqtt_gti_time(E.client_p, TOPIC_P, time_log);
    }

    sprintf(time_log, "%s", ctime(&rawtime_log));
    len = strlen(time_log);
    time_log[len - 1] = 0; // munge out the return character
    if (log) {
        fprintf(fout, "%s ", time_log);
        fflush(fout);
    }

    return time_log;
}

/*
 * try to keep this programs switch status and HA in sync
 */
bool sync_ha(void)
{
    bool sync = false;
    if (E.gti_sw_status != (bool) ((int32_t) E.mvar[V_HDCSW])) {
        fprintf(fout, "DC_MM %d %d ", (bool) E.gti_sw_status, (bool) ((int32_t) E.mvar[V_HDCSW]));
        mqtt_ha_switch(E.client_p, TOPIC_PDCC, !E.gti_sw_status);
        E.dc_mismatch = true;
        fflush(fout);
        sync = true;
    } else {
        E.dc_mismatch = false;
    }

    E.ac_sw_status = (bool) ((int32_t) E.mvar[V_HACSW]); // TEMP FIX for MISmatch errors
    if (E.ac_sw_status != (bool) ((int32_t) E.mvar[V_HACSW])) {
        fprintf(fout, "AC_MM %d %d ", (bool) E.ac_sw_status, (bool) ((int32_t) E.mvar[V_HACSW]));
        mqtt_ha_switch(E.client_p, TOPIC_PACC, !E.ac_sw_status);
        E.ac_mismatch = true;
        fflush(fout);
        sync = true;
    } else {
        E.ac_mismatch = false;
    }
    return sync;
}

/*
 * limits commands and log messages, check for proper functionality for each usage
 */
bool log_timer(void)
{
    bool itstime = false;

    if (E.log_spam < LOW_LOG_SPAM) {
        E.log_time_reset = 0;
        itstime = true;
    }
    if (E.log_time_reset > RESET_LOG_SPAM) {
        E.log_spam = 0;
        itstime = true;
    }
    return itstime;
}
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Local speech to text processing on the Home Assistant Linux server using faster-whisper (which is run locally using local resources (GPU / CPU).
For GPU, you need a CUDA enabled GPU.
Using an old slow GT-710 with 2G of VRAM on this under-powered testing machine. I have a GTX-1080 8G VRAM for the HPE DL360P gen8 production server the home Ha system will move moved to this summer to run my own LLM.
Current dual processor system, not very powerful.
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 37
model name : Intel(R) Core(TM) i3 CPU 550 @ 3.20GHz
stepping : 5
microcode : 0x7
cpu MHz : 1316.368
cache size : 4096 KB

https://github.com/SYSTRAN/faster-whisper/issues/556
For GPU, you need a CUDA enabled GPU, for some (esp. older), you will have to use int8 instead of float16. For CPU, there does not really seem to be a lower limit that we found, it just takes longer. Only limit might be memory, but faster-whisper states around 3GB of ram use for the large v2 model.

https://github.com/SYSTRAN/faster-whisper

It's not perfect but it's amazing
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
1739639824886.png
Cleaned the array. Ground level is nice if you have the room.
1739639852785.png

I now have 23W of solar power. :(
1739640025431.png
and going up. :D
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Q84 piggyback controller remote display using CANFD for data networking to share home display data to two remote displays.
1740881213737.png
1740881250155.png
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Working on the Linux interface code for some low-level 24VDC relay switching and position switch readbacks for Solar Panel tracking. Using the standard Linux Comedi DAQ sub-system with some USB DAQ modules. Testing with an old VM110N board connected to a Orange PI 3 (that runs Home Assistant) with a custom kernel for USB comedi device support.

https://github.com/Linux-Comedi
https://github.com/nsaspook/mqtt_comedi

Using netbeans to cross-compile for arm64 on the amd64 HPE DL380 workstation. Much faster this way. The C software uses asynchronous threads for background network connection and transmissions.

1742779202534.png
1742779319201.png

The C software reads the DAQ DI, DO, AI and AO data from the board VM110N via the USB connection, formats it to jSON format and then sends that data to the house MQTT server that handles the reset of the FM80 system data sharing requirements.
1742779450713.png1742779471051.png

Dummied up a quick testing trigger button inside Home Assistant. Each time the button is tapped, it sends a MQTT trigger data point to the Comedi interface program. The program then reads fresh data, formats and sends the data the MQTT server as requested.
1742779657794.png

1742779703420.png
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Installed the trash can enclosure/solar panel mount with halt of the panels to check.
Looks pretty good and is very strong.
1744250032033.png
1744250069627.png

1744250730530.png
1744250095389.png
The magic right angle adapter and extension for the screws.

1744250147640.png
WIll mount the other two a bit later once I install some 4 inch deck screws to secure the fence cross support boards (2 2x4 and one 2X6) for the upper structure to the platform.
1744250421558.png

Also need to dig some support footers for the legs to be concreted and bolted to for extra wind resistance from up drafts.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
I have 7 AC utility power monitor data point measured using IAMMETER WiFi sensors.
1744603410184.png
Two CT's (twisted pair cat 5 cable) go to the main panel for the incoming split single-phase power to the house. The third CT in measures the main solar panel inverter grid-tie power to L1

1744603709820.png
The server rack power, server GTI solar panel input power and server UPS power monitor.


1744603836563.png
Remote shed GTI power meter from another solar panel array.


1744604296301.png
and the off-grid inverter power is measured using a EM540 from Carlo Gavazzi using MODBUS.

1744604534981.png1744604573287.png
1744604684635.png
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
I also made a RS485 PIC18 Q84 box to reset all of the data and statistics on those meters. It's a simple MODBUS command sequence to clear them.
1744604914283.png
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Pushing the total house L1 power drain to under 100W.
1745371100069.png
1745371352144.png

L2 was always using power from the utility so I never pushed power into the grid, only used it internally.

1745371566264.png
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
I have the beta custom DAQ board for the Orange PI in operational testing. 8 24vdc, 2 5vdc digital inputs. 16, up to 24vdc low side switches, 7 12-bit ADC inputs and 1 8-bit DAC output. The analog and connections to the OPi3 are handled by a PIC18F47Q84 controller firmware. SPI2 is the SPI slave for the OPi3 Master controller. SPI1 handles the display and the two DIO devices that transfer data via SPI2 per Master requests for status and changes.
On the Orange PI and custom kernel driver was designed (still needs optimization to use CPU threads effectively) to interface with the Comedi DAQ API. This allows for specific hardware device functions to be abstracted into a common user API so different hardware DAQ frontends can be used with the same applications software.

https://github.com/nsaspook/mqtt_comedi/blob/main/secs_q84 v2_sch.pdf
https://github.com/nsaspook/mqtt_comedi/blob/main/secs_q84 v2_brd.pdf

Early Beta software: https://github.com/nsaspook/mqtt_comedi
Applications program: https://github.com/nsaspook/mqtt_comedi/tree/main/bmc
Q84 firmware: https://github.com/nsaspook/mqtt_comedi/tree/main/Q84/bmc_slave.X
Linux kernel protocol driver: https://github.com/nsaspook/mqtt_comedi/blob/main/daq_bmc/daq_bmc.c
Requires Linux kernel patches to add the needed device-tree and Kconfig to the new user protocol driver source into the standard kernel build process. Then add the new protocol driver spi_bmc instead of the system standard spi-dev in the Orange PI
device tree-config file.
Boot config file: https://github.com/nsaspook/mqtt_comedi/blob/main/daq_bmc/orangepiEnv.txt
Device-tree source file: https://github.com/nsaspook/mqtt_comedi/blob/main/daq_bmc/sun50i-h616-spi1-spibmc.dts

1747064741976.png
1747064764275.png1747066142398.png
BMC1 MQTT data. The ADC channel 0 and 1 are the raw voltage reading on the ADC pins for the RS232 line voltages (that have been normalized and scaled on the Q84 display to the +- 15 volts range)
1747066472069.png
DAQ testing RS232 raw ADC values displayed on the Home Assistant Shed PV voltage cards. The Sys voltages are live system values.

1747064799064.png
Second line: The first number is the error count, second ADC conversions, third master data packet requests.
Third line: ADC channel 1 raw value, ADC channel 2 raw value.
Last Line: SPI2TCNT, SPI2INTF SPI2 registers and program state flags. The number 1, on the end means, spi_comm_ss.REMOTE_LINK is true.

1747065404421.png
RS-232 Jack status. Voltage and Mark/Space or Open line condition.
Last line: Input line buffer from the SPI2 and UART links.
C:
data_in2 = SPI2RXB;
serial_buffer_ss.data[0] = data_in2;
 char_rxtmp = UART1_Read();
 serial_buffer_ss.data[1] = char_rxtmp;
Line data specs:
1747065835405.png
MOSI -> MISO ADC data transactions.
1747065875649.png
One ADC value request transaction.
1747065909429.png
Q84, all channels ADC conversion and buffered processing time.

1747065974516.png
Q84 ADC all channel update rate with the display update gap timing. We only monitoring slowly changing DC signals for this applications, so the variation in sampling rate is not important.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Reworked the DAQ_BMC board to also handle the FM80 (9-bit serial) and EM540 three-phase power monitor (MODBUS) The Q84 has hardware auto DERE RS-485 output pins on the uarts, that makes half-duplex M/S 485 driver chip handling easy.
2 65VDC ADC inputs for battery voltage, 3 to 7 ADC inputs for Hall current monitors to 200A or other 4.096VDC inputs, 16 24VDC low-side switches, 23 24VDC inputs, RS232, etc ...

https://github.com/nsaspook/mqtt_comedi/tree/fixes
https://forum.allaboutcircuits.com/threads/home-assistant-devices-and-uses.206323/post-1995121

1755541928358.png
1755541959538.png
All of the command and status data is via the SPI link to the OPi.
1755542032558.png
Raw data from the power monitor via MODBUS on the daq_bmc board.

Software instrumented with GPIO pins to trace state machine and ISR run timing with all hardware functionality at near max capacity.
1755542983032.png
1755543001669.png
1755543375445.png
1755543417421.png
Looking for nice uniform processing patterns. Looks fine.


1755543109879.png
TX MODBUS command 115200 decoding.
1755543199859.png
RX serial decoding.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
https://medium.com/@tariqjamel/mast...ontrollers-a-comprehensive-guide-a3c5d583e3d0
Mastering Configurable Logic Cells (CLCs) in PIC18F Microcontrollers: A Comprehensive Guide

Configurable Logic Cells (CLCs) are programmable hardware peripherals integrated into Microchip’s PIC microcontrollers, such as the PIC18F57Q84. They allow you to implement custom logic functions — like AND/OR gates, flip-flops, latches, and more — directly in hardware, bypassing the CPU for ultra-low latency operations. Think of CLCs as a mini-FPGA embedded within your MCU: they operate independently, reducing software overhead and enabling parallel processing.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,333
Time to remove the unused side of the old play-set (the wife still used the swing seat) for the larger panel ground mounts. Two foot rebar retainers in the ground lock the wood foundations in place.
1762888873525.png
 
Top