Skip to content

Commit

Permalink
Add state class for Home Assistant energy dashboard integration
Browse files Browse the repository at this point in the history
  • Loading branch information
infertux committed Jan 12, 2024
1 parent 2f77b3e commit c7a1b01
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 63 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
84 changes: 44 additions & 40 deletions src/growatt.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,67 +22,71 @@ typedef struct __attribute__((aligned(MAX_METRIC_LENGTH * 2))) {
uint8_t address;
char human_name[MAX_METRIC_LENGTH];
char metric_name[MAX_METRIC_LENGTH];
// https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes
char device_class[MAX_METRIC_LENGTH];
// unit of measurement e.g. "V" for voltage
char unit[MAX_METRIC_LENGTH];
// https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes
char state_class[MAX_METRIC_LENGTH];
enum { REGISTER_SINGLE, REGISTER_DOUBLE } register_size;
double scale;
} REGISTER;

const REGISTER holding_registers[] = {
// NOLINTBEGIN(readability-magic-numbers)
{34, "max charging current", "settings_max_charging_amps", "current", "A", REGISTER_SINGLE, 1},
{35, "bulk charging voltage", "settings_bulk_charging_volts", "voltage", "V", REGISTER_SINGLE, 0.1},
{36, "float charging voltage", "settings_float_charging_volts", "voltage", "V", REGISTER_SINGLE, 0.1},
{37, "battery voltage switch to utility", "settings_switch_to_utility_volts", "voltage", "V", REGISTER_SINGLE, 0.1},
// {76, "rated active power", "rated_active_power_watts", REGISTER_DOUBLE, 0.1}, // XXX: not needed
// {78, "rated apparant power", "rated_apparant_power_va", REGISTER_DOUBLE, 0.1}, // XXX: not needed
{34, "max charging current", "settings_max_charging_amps", "current", "A", "measurement", REGISTER_SINGLE, 1},
{35, "bulk charging voltage", "settings_bulk_charging_volts", "voltage", "V", "measurement", REGISTER_SINGLE, 0.1},
{36, "float charging voltage", "settings_float_charging_volts", "voltage", "V", "measurement", REGISTER_SINGLE, 0.1},
{37, "battery voltage switch to utility", "settings_switch_to_utility_volts", "voltage", "V", "measurement", REGISTER_SINGLE, 0.1},
// {76, "rated active power", "rated_active_power_watts"}, // XXX: not needed
// {78, "rated apparant power", "rated_apparant_power_va"}, // XXX: not needed
// NOLINTEND(readability-magic-numbers)
};

const REGISTER input_registers[] = {
// NOLINTBEGIN(readability-magic-numbers)
{0, "system status", "system_status", "None", "", REGISTER_SINGLE, 1},
{1, "PV1 voltage", "pv1_volts", "voltage", "V", REGISTER_SINGLE, 0.1},
{3, "PV1 power", "pv1_watts", "power", "W", REGISTER_DOUBLE, 0.1},
{7, "buck1 current", "buck1_amps", "current", "A", REGISTER_SINGLE, 0.1},
// {8, "buck2 current", "buck2_amps", "current", "A", REGISTER_SINGLE, 0.1}, // XXX: always zero
{9, "inverter active power", "inverter_active_power_watts", "power", "W", REGISTER_DOUBLE, 0.1},
{11, "inverter apparant power", "inverter_apparant_power_va", "apparent_power", "VA", REGISTER_DOUBLE, 0.1},
{13, "grid charging power", "grid_charging_watts", "power", "W", REGISTER_DOUBLE, 0.1},
{17, "battery voltage", "battery_volts", "voltage", "V", REGISTER_SINGLE, 0.01},
{18, "battery SOC", "battery_soc", "battery", "%", REGISTER_SINGLE, 1},
// {19, "bus voltage", "bus_volts", REGISTER_SINGLE, 0.1}, // irrelevant
{20, "grid voltage", "grid_volts", "voltage", "V", REGISTER_SINGLE, 0.1},
{21, "grid frequency", "grid_hz", "frequency", "Hz", REGISTER_SINGLE, 0.01},
// {24, "output DC voltage", "output_dc_volts", REGISTER_SINGLE, 0.1}, // XXX: always zero
{25, "inverter temperature", "temperature_inverter_celsius", "temperature", "°C", REGISTER_SINGLE, 0.1},
{26, "DC-DC temperature", "temperature_dcdc_celsius", "temperature", "°C", REGISTER_SINGLE, 0.1},
{27, "inverter load percent", "inverter_load_percent", "None", "%", REGISTER_SINGLE, 0.1},
{0, "system status", "system_status", "None", "", "measurement", REGISTER_SINGLE, 1},
{1, "PV1 voltage", "pv1_volts", "voltage", "V", "measurement", REGISTER_SINGLE, 0.1},
{3, "PV1 power", "pv1_watts", "power", "W", "measurement", REGISTER_DOUBLE, 0.1},
{7, "buck1 current", "buck1_amps", "current", "A", "measurement", REGISTER_SINGLE, 0.1},
// {8, "buck2 current", "buck2_amps", "current", "A", "measurement", REGISTER_SINGLE, 0.1}, // XXX: always zero
{9, "inverter active power", "inverter_active_power_watts", "power", "W", "measurement", REGISTER_DOUBLE, 0.1},
{11, "inverter apparant power", "inverter_apparant_power_va", "apparent_power", "VA", "measurement", REGISTER_DOUBLE, 0.1},
{13, "grid charging power", "grid_charging_watts", "power", "W", "measurement", REGISTER_DOUBLE, 0.1},
{17, "battery voltage", "battery_volts", "voltage", "V", "measurement", REGISTER_SINGLE, 0.01},
{18, "battery SOC", "battery_soc", "battery", "%", "measurement", REGISTER_SINGLE, 1},
// {19, "bus voltage", "bus_volts"}, // irrelevant
{20, "grid voltage", "grid_volts", "voltage", "V", "measurement", REGISTER_SINGLE, 0.1},
{21, "grid frequency", "grid_hz", "frequency", "Hz", "measurement", REGISTER_SINGLE, 0.01},
// {24, "output DC voltage", "output_dc_volts", "voltage", "V", "measurement", REGISTER_SINGLE, 0.1}, // XXX: always zero
{25, "inverter temperature", "temperature_inverter_celsius", "temperature", "°C", "measurement", REGISTER_SINGLE, 0.1},
{26, "DC-DC temperature", "temperature_dcdc_celsius", "temperature", "°C", "measurement", REGISTER_SINGLE, 0.1},
{27, "inverter load percent", "inverter_load_percent", "None", "%", "measurement", REGISTER_SINGLE, 0.1},
// {30, "work time total", "work_time_total_seconds", REGISTER_DOUBLE, 0.5}, // XXX: always zero
{32, "buck1 temperature", "temperature_buck1_celsius", "temperature", "°C", REGISTER_SINGLE, 0.1},
{32, "buck1 temperature", "temperature_buck1_celsius", "temperature", "°C", "measurement", REGISTER_SINGLE, 0.1},
// {33, "buck2 temperature", "temperature_buck2_celsius", REGISTER_SINGLE, 0.1}, // irrelevant
{34, "output current", "output_amps", "current", "A", REGISTER_SINGLE, 0.1},
{35, "inverter current", "inverter_amps", "current", "A", REGISTER_SINGLE, 0.1},
{40, "fault bit", "fault_bit", "None", "", REGISTER_SINGLE, 1},
{41, "warning bit", "warning_bit", "None", "", REGISTER_SINGLE, 1},
{34, "output current", "output_amps", "current", "A", "measurement", REGISTER_SINGLE, 0.1},
{35, "inverter current", "inverter_amps", "current", "A", "measurement", REGISTER_SINGLE, 0.1},
{40, "fault bit", "fault_bit", "None", "", "measurement", REGISTER_SINGLE, 1},
{41, "warning bit", "warning_bit", "None", "", "measurement", REGISTER_SINGLE, 1},
// {42, "fault value", "fault_value", REGISTER_SINGLE, 1}, // XXX: always zero
// {43, "warning value", "warning_value", REGISTER_SINGLE, 1}, // XXX: always zero
// {45, "product check step", "product_check_step", REGISTER_SINGLE, 1}, // irrelevant
// {46, "production line mode", "production_line_mode", REGISTER_SINGLE, 1}, // XXX: always zero
// {47, "constant power OK flag", "constant_power_ok_flag", REGISTER_SINGLE, 1}, // XXX: always zero
{48, "PV energy today", "energy_pv_today_kwh", "energy", "kWh", REGISTER_DOUBLE, 0.1},
{50, "PV energy total", "energy_pv_total_kwh", "energy", "kWh", REGISTER_DOUBLE, 0.1},
{56, "grid energy today", "energy_grid_today_kwh", "energy", "kWh", REGISTER_DOUBLE, 0.1},
{58, "grid energy total", "energy_grid_total_kwh", "energy", "kWh", REGISTER_DOUBLE, 0.1},
{60, "battery discharging energy today", "battery_discharging_today_kwh", "energy", "kWh", REGISTER_DOUBLE, 0.1},
{64, "grid discharging energy today", "grid_discharging_today_kwh", "energy", "kWh", REGISTER_DOUBLE, 0.1},
{66, "grid discharging energy total", "grid_discharging_total_kwh", "energy", "kWh", REGISTER_DOUBLE, 0.1},
{68, "grid charging current", "grid_charging_amps", "current", "A", REGISTER_SINGLE, 0.1},
{69, "inverter discharging power", "inverter_discharging_watts", "power", "W", REGISTER_DOUBLE, 0.1},
{73, "battery discharging power", "battery_discharging_watts", "power", "W", REGISTER_DOUBLE, 0.1},
{77, "battery net power", "battery_net_watts", "power", "W", REGISTER_DOUBLE, -0.1}, // XXX: signed value
{48, "PV energy today", "energy_pv_today_kwh", "energy", "kWh", "total_increasing", REGISTER_DOUBLE, 0.1},
{50, "PV energy total", "energy_pv_total_kwh", "energy", "kWh", "total_increasing", REGISTER_DOUBLE, 0.1},
{56, "grid energy today", "energy_grid_today_kwh", "energy", "kWh", "total_increasing", REGISTER_DOUBLE, 0.1},
{58, "grid energy total", "energy_grid_total_kwh", "energy", "kWh", "total_increasing", REGISTER_DOUBLE, 0.1},
{60, "battery discharging energy today", "battery_discharging_today_kwh", "energy", "kWh", "total_increasing", REGISTER_DOUBLE, 0.1},
{64, "grid discharging energy today", "grid_discharging_today_kwh", "energy", "kWh", "total_increasing", REGISTER_DOUBLE, 0.1},
{66, "grid discharging energy total", "grid_discharging_total_kwh", "energy", "kWh", "total_increasing", REGISTER_DOUBLE, 0.1},
{68, "grid charging current", "grid_charging_amps", "current", "A", "measurement", REGISTER_SINGLE, 0.1},
{69, "inverter discharging power", "inverter_discharging_watts", "power", "W", "measurement", REGISTER_DOUBLE, 0.1},
{73, "battery discharging power", "battery_discharging_watts", "power", "W", "measurement", REGISTER_DOUBLE, 0.1},
{77, "battery net power", "battery_net_watts", "power", "W", "measurement", REGISTER_DOUBLE, -0.1}, // XXX: signed value
// {81, "fan speed MPPT", "fan_speed_mppt", REGISTER_SINGLE, 1}, // XXX: always zero
{82, "fan speed inverter", "fan_speed_inverter", "wind_speed", "%", REGISTER_SINGLE, 1},
{82, "fan speed inverter", "fan_speed_inverter", "wind_speed", "%", "measurement", REGISTER_SINGLE, 1},
// {180, "solar charger status", "solar_status", REGISTER_SINGLE, 1}, // XXX: always zero
// NOLINTEND(readability-magic-numbers)
};
Expand Down
9 changes: 4 additions & 5 deletions src/log.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ enum {
LOG_ERROR,
};

#define PERROR_HELPER(fmt, ...) \
#define PERROR_HELPER(fmt, ...) \
fprintf(stderr, "\x1b[37;41m" fmt ": %s%s\n\x1b[0m", __VA_ARGS__, strerror(errno)) // NOLINT(concurrency-mt-unsafe)
#define PERROR(...) PERROR_HELPER(__VA_ARGS__, "")

Expand All @@ -33,9 +33,8 @@ const char *log_prefix(const char filename[static 1]) {
return "31m[????]";
}

#define LOG(level, string, ...) \
if (LOG_VERBOSE || level >= LOG_INFO) \
fprintf((level == LOG_ERROR ? stderr : stdout), "\x1b[%s " string "\x1b[0m\n", \
log_prefix(__FILE__) __VA_OPT__(, ) __VA_ARGS__)
#define LOG(level, string, ...) \
if (LOG_VERBOSE || level >= LOG_INFO) \
fprintf((level == LOG_ERROR ? stderr : stdout), "\x1b[%s " string "\x1b[0m\n", log_prefix(__FILE__) __VA_OPT__(, ) __VA_ARGS__)

#endif /* GROWATT_LOG_H */
7 changes: 1 addition & 6 deletions src/modbus.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,7 @@ void clock_write(modbus_t *ctx) {
gmtime_r(&now, &now_tm);

const uint16_t clock[REGISTER_CLOCK_SIZE] = {
now_tm.tm_year - REGISTER_CLOCK_YEAR_OFFSET,
now_tm.tm_mon + 1,
now_tm.tm_mday,
now_tm.tm_hour,
now_tm.tm_min,
now_tm.tm_sec,
now_tm.tm_year - REGISTER_CLOCK_YEAR_OFFSET, now_tm.tm_mon + 1, now_tm.tm_mday, now_tm.tm_hour, now_tm.tm_min, now_tm.tm_sec,
};

char hex[REGISTER_CLOCK_SIZE * HEX_SIZE] = {0};
Expand Down
7 changes: 3 additions & 4 deletions src/mqtt.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ int start_mqtt_thread(void *config_ptr) {
return EXIT_FAILURE;
}

if (mosquitto_loop_start(client) !=
MOSQ_ERR_SUCCESS) { // without this statement, the callback is not called upon connection
if (mosquitto_loop_start(client) != MOSQ_ERR_SUCCESS) { // without this statement, the callback is not called upon connection
PERROR("Unable to start loop");
return EXIT_FAILURE;
}
Expand All @@ -93,10 +92,10 @@ int start_mqtt_thread(void *config_ptr) {

sprintf(unique_id, "growatt_%s", reg.metric_name);
sprintf(payload,
"{\"device_class\":\"%s\",\"state_topic\":\"%s\",\"unit_of_measurement\":\"%s\","
"{\"device_class\":\"%s\",\"state_class\":\"%s\",\"state_topic\":\"%s\",\"unit_of_measurement\":\"%s\","
"\"value_template\":\"{{value_json.%s}}\",\"name\":\"%s\",\"unique_id\":\"%s\","
"\"device\":{\"identifiers\":[\"1\"],\"name\":\"Growatt\",\"manufacturer\":\"Growatt\"}}",
reg.device_class, TOPIC_STATE, reg.unit, reg.metric_name, reg.human_name, unique_id);
reg.device_class, reg.state_class, TOPIC_STATE, reg.unit, reg.metric_name, reg.human_name, unique_id);

sprintf(topic, "homeassistant/sensor/%s/config", unique_id);
mosquitto_publish(client, NULL, topic, (int)strlen(payload), payload, 0, true);
Expand Down
6 changes: 2 additions & 4 deletions src/prometheus.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ int set_response(char *response) {
METRIC metric = device_metrics.metrics[i];
// LOG(LOG_DEBUG, "%s = %lf\n", metric.name, metric.value);

snprintf(buffer, sizeof(buffer), "# TYPE growatt_%s gauge\ngrowatt_%s %lf\n", metric.name, metric.name,
metric.value);
snprintf(buffer, sizeof(buffer), "# TYPE growatt_%s gauge\ngrowatt_%s %lf\n", metric.name, metric.name, metric.value);
strlcat(metrics, buffer, RESPONSE_BUFFER_SIZE);
}
mtx_unlock(&device_metrics.mutex);
Expand Down Expand Up @@ -122,8 +121,7 @@ int start_prometheus_thread(void *config_ptr) {
0 // protocol 0
);

const struct sockaddr_in6 address = {
.sin6_family = AF_INET6, .sin6_port = htons(config->port), .sin6_addr = in6addr_any};
const struct sockaddr_in6 address = {.sin6_family = AF_INET6, .sin6_port = htons(config->port), .sin6_addr = in6addr_any};

// prevent "bind failed: Address already in use" when restarting the program too quickly
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int))) {
Expand Down
5 changes: 2 additions & 3 deletions tests/mock-server.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ int main(void) {
// assert there is nothing relevant after the clock register
assert(holding_registers[COUNT(holding_registers) - 1].address < REGISTER_CLOCK_ADDRESS + REGISTER_CLOCK_SIZE);

modbus_mapping_t *mapping =
modbus_mapping_new_start_address(0, 0, 0, 0, 0, REGISTER_CLOCK_ADDRESS + REGISTER_CLOCK_SIZE + 1, 0,
input_registers[COUNT(input_registers) - 1].address + 1);
modbus_mapping_t *mapping = modbus_mapping_new_start_address(0, 0, 0, 0, 0, REGISTER_CLOCK_ADDRESS + REGISTER_CLOCK_SIZE + 1, 0,
input_registers[COUNT(input_registers) - 1].address + 1);

// NOLINTBEGIN(readability-magic-numbers)
mapping->tab_registers[34] = 80; // settings_max_charging_amps
Expand Down

0 comments on commit c7a1b01

Please sign in to comment.