diff --git a/.SRCINFO b/.SRCINFO
index 3ed6f9b..80a4b61 100644
--- a/.SRCINFO
+++ b/.SRCINFO
@@ -1,6 +1,6 @@
pkgbase = coolerdash
pkgdesc = Plug-in for CoolerControl that extends the LCD functionality with additional features
- pkgver = 2.1.0
+ pkgver = 3.1.1
pkgrel = 1
url = https://github.com/damachine/coolerdash
install = coolerdash.install
@@ -18,5 +18,6 @@ pkgbase = coolerdash
conflicts = coolerdash-git
replaces = coolerdash-git
backup = etc/coolercontrol/plugins/coolerdash/config.json
+ backup = etc/coolercontrol/plugins/coolerdash/credentials.json
pkgname = coolerdash
diff --git a/Makefile b/Makefile
index b296b8f..6548330 100644
--- a/Makefile
+++ b/Makefile
@@ -352,6 +352,10 @@ install: check-deps $(TARGET)
else \
$(INSTALL) -m 600 etc/coolercontrol/plugins/coolerdash/config.json "$(DESTDIR)$(PLUGINDIR)/config.json"; \
fi
+ @if [ -f "$(DESTDIR)$(PLUGINDIR)/credentials.json" ]; then \
+ chmod 600 "$(DESTDIR)$(PLUGINDIR)/credentials.json"; \
+ printf " $(GREEN)Credentials:$(RESET) Existing credentials.json preserved (chmod 600)\n"; \
+ fi
@$(INSTALL) -d "$(DESTDIR)$(PLUGINDIR)/ui"
@$(INSTALL_DATA) etc/coolercontrol/plugins/coolerdash/ui/index.html "$(DESTDIR)$(PLUGINDIR)/ui/index.html"
@$(INSTALL_DATA) images/shutdown.png "$(DESTDIR)$(PLUGINDIR)/shutdown.png"
@@ -360,6 +364,7 @@ install: check-deps $(TARGET)
@sed -i 's/{{VERSION}}/$(VERSION)/g' "$(DESTDIR)$(PLUGINDIR)/ui/index.html"
@printf " $(GREEN)Binary:$(RESET) $(DESTDIR)$(libexecdir)/coolerdash/coolerdash\n"
@printf " $(GREEN)Config JSON:$(RESET) $(DESTDIR)$(PLUGINDIR)/config.json (chmod 600)\n"
+ @printf " $(GREEN)Credentials:$(RESET) $(DESTDIR)$(PLUGINDIR)/credentials.json (chmod 600)\n"
@printf " $(GREEN)Web UI:$(RESET) $(DESTDIR)$(PLUGINDIR)/ui/index.html\n"
@printf " $(GREEN)Plugin Lib:$(RESET) Served by CoolerControl at /plugins/lib/cc-plugin-lib.js\n"
@printf " $(GREEN)Plugin:$(RESET) $(DESTDIR)$(PLUGINDIR)/manifest.toml\n"
diff --git a/PKGBUILD b/PKGBUILD
index 6dbb2ae..e521e89 100644
--- a/PKGBUILD
+++ b/PKGBUILD
@@ -15,7 +15,8 @@ license=('MIT')
depends=('cairo' 'jansson' 'libcurl-gnutls' 'ttf-roboto')
makedepends=('gcc' 'make' 'pkg-config' 'git')
optdepends=()
-backup=('etc/coolercontrol/plugins/coolerdash/config.json')
+backup=('etc/coolercontrol/plugins/coolerdash/config.json'
+ 'etc/coolercontrol/plugins/coolerdash/credentials.json')
install=coolerdash.install
source=()
sha256sums=()
diff --git a/VERSION b/VERSION
index fd2a018..94ff29c 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.1.0
+3.1.1
diff --git a/aur/PKGBUILD b/aur/PKGBUILD
index 012081f..d4c3176 100644
--- a/aur/PKGBUILD
+++ b/aur/PKGBUILD
@@ -12,7 +12,8 @@ url="https://github.com/damachine/coolerdash"
license=('MIT')
depends=('cairo' 'coolercontrol' 'jansson' 'libcurl-gnutls' 'ttf-roboto')
makedepends=('gcc' 'make' 'pkg-config' 'git')
-backup=('etc/coolercontrol/plugins/coolerdash/config.json')
+backup=('etc/coolercontrol/plugins/coolerdash/config.json'
+ 'etc/coolercontrol/plugins/coolerdash/credentials.json')
install=coolerdash.install
_commit=
source=("coolerdash-git::git+https://github.com/damachine/coolerdash.git#commit=${_commit}")
diff --git a/aur/coolerdash.install b/aur/coolerdash.install
index 7c9ce85..b6c708d 100644
--- a/aur/coolerdash.install
+++ b/aur/coolerdash.install
@@ -43,6 +43,7 @@ post_install() {
# Ensure correct permissions on config.json
chmod 600 /etc/coolercontrol/plugins/coolerdash/config.json 2>/dev/null || true
+ chmod 600 /etc/coolercontrol/plugins/coolerdash/credentials.json 2>/dev/null || true
systemctl daemon-reload
@@ -58,6 +59,8 @@ post_install() {
echo "================================================================"
echo "CoolerDash installed successfully."
+ echo "Set your Access Token in the CoolerDash Settings UI."
+ echo "The token is auto-persisted to credentials.json on startup."
echo "================================================================"
}
@@ -71,6 +74,7 @@ post_upgrade() {
# Ensure correct permissions on config.json
chmod 600 /etc/coolercontrol/plugins/coolerdash/config.json 2>/dev/null || true
+ chmod 600 /etc/coolercontrol/plugins/coolerdash/credentials.json 2>/dev/null || true
systemctl daemon-reload
@@ -83,6 +87,7 @@ post_upgrade() {
echo "CoolerDash upgraded successfully."
echo "Note: config.json is preserved. If a new template was shipped,"
echo " compare it with /etc/coolercontrol/plugins/coolerdash/config.json.pacnew"
+ echo "Note: credentials.json is never overwritten by updates."
echo "================================================================"
}
diff --git a/coolerdash.install b/coolerdash.install
index 7c9ce85..b6c708d 100644
--- a/coolerdash.install
+++ b/coolerdash.install
@@ -43,6 +43,7 @@ post_install() {
# Ensure correct permissions on config.json
chmod 600 /etc/coolercontrol/plugins/coolerdash/config.json 2>/dev/null || true
+ chmod 600 /etc/coolercontrol/plugins/coolerdash/credentials.json 2>/dev/null || true
systemctl daemon-reload
@@ -58,6 +59,8 @@ post_install() {
echo "================================================================"
echo "CoolerDash installed successfully."
+ echo "Set your Access Token in the CoolerDash Settings UI."
+ echo "The token is auto-persisted to credentials.json on startup."
echo "================================================================"
}
@@ -71,6 +74,7 @@ post_upgrade() {
# Ensure correct permissions on config.json
chmod 600 /etc/coolercontrol/plugins/coolerdash/config.json 2>/dev/null || true
+ chmod 600 /etc/coolercontrol/plugins/coolerdash/credentials.json 2>/dev/null || true
systemctl daemon-reload
@@ -83,6 +87,7 @@ post_upgrade() {
echo "CoolerDash upgraded successfully."
echo "Note: config.json is preserved. If a new template was shipped,"
echo " compare it with /etc/coolercontrol/plugins/coolerdash/config.json.pacnew"
+ echo "Note: credentials.json is never overwritten by updates."
echo "================================================================"
}
diff --git a/etc/coolercontrol/plugins/coolerdash/config.json b/etc/coolercontrol/plugins/coolerdash/config.json
index 19a480a..24defa5 100644
--- a/etc/coolercontrol/plugins/coolerdash/config.json
+++ b/etc/coolercontrol/plugins/coolerdash/config.json
@@ -2,8 +2,7 @@
"_comment": "CoolerDash Plugin Configuration - All values with defaults",
"daemon": {
- "address": "http://localhost:11987",
- "access_token": ""
+ "address": "http://localhost:11987"
},
"device_detection": {
diff --git a/etc/coolercontrol/plugins/coolerdash/manifest.toml b/etc/coolercontrol/plugins/coolerdash/manifest.toml
index d50e6fe..8828282 100644
--- a/etc/coolercontrol/plugins/coolerdash/manifest.toml
+++ b/etc/coolercontrol/plugins/coolerdash/manifest.toml
@@ -1,7 +1,7 @@
id = "coolerdash"
type = "integration"
version = "{{VERSION}}"
-description = "Monitor telemetry data on an AIO liquid cooler with an integrated LCD display"
+description = "Plug-in for CoolerControl that extends the LCD functionality with additional features"
executable = "/usr/libexec/coolerdash/coolerdash"
#args = "--verbose"
privileged = true
diff --git a/etc/coolercontrol/plugins/coolerdash/ui/index.html b/etc/coolercontrol/plugins/coolerdash/ui/index.html
index 4264cda..35e0d88 100644
--- a/etc/coolercontrol/plugins/coolerdash/ui/index.html
+++ b/etc/coolercontrol/plugins/coolerdash/ui/index.html
@@ -600,7 +600,7 @@
Authentication — CC4 (Bearer Token)
- Enter a CC4 Bearer token (cc_…). After saving, the token is stored securely in config.json (chmod 600). If a token is already set, the field shows *** — enter a new value to replace it, or leave *** unchanged.
+ Enter a CC4 Bearer token (cc_…). After saving, the token is automatically persisted to credentials.json (chmod 600) so it survives package updates. If a token is already set, the field shows *** — enter a new value to replace it, or leave *** unchanged.
diff --git a/packaging/coolerdash.spec b/packaging/coolerdash.spec
index 7f9929b..de8687a 100644
--- a/packaging/coolerdash.spec
+++ b/packaging/coolerdash.spec
@@ -78,6 +78,9 @@ fi
if [ -f /etc/coolercontrol/plugins/coolerdash/config.json ]; then
chmod 600 /etc/coolercontrol/plugins/coolerdash/config.json
fi
+if [ -f /etc/coolercontrol/plugins/coolerdash/credentials.json ]; then
+ chmod 600 /etc/coolercontrol/plugins/coolerdash/credentials.json
+fi
# Remove legacy files
rm -f /etc/systemd/system/multi-user.target.wants/coolerdash-helperd.service
rm -f /etc/systemd/system/coolerdash-helperd.service
diff --git a/packaging/debian/conffiles b/packaging/debian/conffiles
new file mode 100644
index 0000000..80e4929
--- /dev/null
+++ b/packaging/debian/conffiles
@@ -0,0 +1,2 @@
+/etc/coolercontrol/plugins/coolerdash/config.json
+/etc/coolercontrol/plugins/coolerdash/credentials.json
diff --git a/src/device/config.c b/src/device/config.c
index 144055c..763b18b 100644
--- a/src/device/config.c
+++ b/src/device/config.c
@@ -18,7 +18,6 @@
#include
#include
#include
-#include
#include
// cppcheck-suppress-end missingIncludeSystem
@@ -700,6 +699,195 @@ static void load_daemon_from_json(json_t *root, Config *config)
}
}
+/**
+ * @brief Derive credentials.json path from config.json path.
+ * @param config_json_path Path to config.json
+ * @param cred_path Output buffer for credentials.json path
+ * @param cred_path_size Size of output buffer
+ * @return 1 on success, 0 on failure
+ */
+static int get_credentials_path(const char *config_json_path,
+ char *cred_path, size_t cred_path_size)
+{
+ if (!config_json_path || !cred_path)
+ return 0;
+
+ const char *last_slash = strrchr(config_json_path, '/');
+ if (!last_slash)
+ return 0;
+
+ size_t dir_len = (size_t)(last_slash - config_json_path + 1);
+ if (dir_len + 17 > cred_path_size) /* 17 = strlen("credentials.json") + 1 */
+ return 0;
+
+ memcpy(cred_path, config_json_path, dir_len);
+ memcpy(cred_path + dir_len, "credentials.json", 17);
+ return 1;
+}
+
+/**
+ * @brief Load access_token from separate credentials.json file.
+ * @details Credentials are stored separately so package updates never
+ * overwrite the user's access_token. The credentials file is
+ * expected in the same directory as config.json.
+ * @param config_json_path Path to config.json (used to derive credentials path)
+ * @param config Config struct to populate
+ */
+static void load_credentials_file(const char *config_json_path, Config *config)
+{
+ if (!config_json_path || !config)
+ return;
+
+ char cred_path[CONFIG_MAX_PATH_LEN];
+ if (!get_credentials_path(config_json_path, cred_path, sizeof(cred_path)))
+ return;
+
+ json_error_t error;
+ json_t *root = json_load_file(cred_path, 0, &error);
+ if (!root)
+ {
+ log_message(LOG_INFO, "No credentials.json at %s", cred_path);
+ return;
+ }
+
+ json_t *token = json_object_get(root, "access_token");
+ if (token && json_is_string(token) && json_string_length(token) > 0)
+ {
+ const char *value = json_string_value(token);
+ if (value)
+ {
+ SAFE_STRCPY(config->access_token, value);
+ log_message(LOG_INFO, "Loaded access_token from credentials.json");
+ }
+ }
+
+ json_decref(root);
+}
+
+/**
+ * @brief Persist access_token to credentials.json for update safety.
+ * @details Called after config loading. Creates credentials.json if it
+ * does not exist. If a non-empty token was loaded, ensures it
+ * is always persisted so package updates that overwrite
+ * config.json do not lose the token.
+ * @param config_json_path Path to config.json (used to derive credentials path)
+ * @param config Config with the loaded access_token
+ */
+static void save_credentials_file(const char *config_json_path, const Config *config)
+{
+ if (!config_json_path || !config)
+ return;
+
+ char cred_path[CONFIG_MAX_PATH_LEN];
+ if (!get_credentials_path(config_json_path, cred_path, sizeof(cred_path)))
+ return;
+
+ const char *token = config->access_token[0] != '\0'
+ ? config->access_token
+ : "";
+
+ /* Check if credentials.json already has the same token */
+ json_t *existing = json_load_file(cred_path, 0, NULL);
+ if (existing)
+ {
+ json_t *cur = json_object_get(existing, "access_token");
+ if (cur && json_is_string(cur))
+ {
+ const char *val = json_string_value(cur);
+ if (val && strcmp(val, token) == 0)
+ {
+ json_decref(existing);
+ return; /* Already up-to-date */
+ }
+ }
+ json_decref(existing);
+ }
+
+ /* Write credentials.json */
+ json_t *root = json_object();
+ if (!root)
+ return;
+
+ if (json_object_set_new(root, "access_token",
+ json_string(token)) != 0)
+ {
+ json_decref(root);
+ return;
+ }
+
+ if (json_dump_file(root, cred_path, JSON_INDENT(2)) != 0)
+ {
+ log_message(LOG_WARNING, "Failed to write credentials.json");
+ json_decref(root);
+ return;
+ }
+
+ json_decref(root);
+
+ if (token[0] != '\0')
+ log_message(LOG_STATUS, "Access token persisted to credentials.json");
+ else
+ log_message(LOG_STATUS, "Created credentials.json");
+}
+
+/**
+ * @brief Re-format config.json with proper indentation if needed.
+ * @details CoolerControl writes config.json as compact single-line JSON
+ * without trailing newline. This normalizes it to pretty-printed
+ * format so the file stays human-readable and reduces noise in
+ * .pacnew diffs during package updates.
+ * @param json_path Path to config.json
+ * @param root Parsed JSON root (must still be valid, not yet freed)
+ */
+static void normalize_config_json(const char *json_path, json_t *root)
+{
+ if (!json_path || !root)
+ return;
+
+ /* Read existing file to check if it's already formatted */
+ FILE *fp = fopen(json_path, "r");
+ if (!fp)
+ return;
+
+ /* Check first 64 bytes — pretty JSON starts with "{\n " */
+ char head[64];
+ size_t n = fread(head, 1, sizeof(head) - 1, fp);
+ fclose(fp);
+
+ if (n < 4)
+ return;
+
+ head[n] = '\0';
+
+ /* If it already starts with "{\n " it's properly formatted */
+ if (head[0] == '{' && head[1] == '\n' && head[2] == ' ' && head[3] == ' ')
+ return;
+
+ /* Remove access_token before writing (belongs in credentials.json) */
+ json_t *daemon = json_object_get(root, "daemon");
+ if (daemon && json_is_object(daemon))
+ {
+ json_object_del(daemon, "access_token");
+ }
+
+ if (json_dump_file(root, json_path,
+ JSON_INDENT(2) | JSON_PRESERVE_ORDER) != 0)
+ {
+ log_message(LOG_WARNING, "Failed to normalize config.json format");
+ return;
+ }
+
+ /* Append trailing newline (json_dump_file doesn't add one) */
+ fp = fopen(json_path, "a");
+ if (fp)
+ {
+ fputc('\n', fp);
+ fclose(fp);
+ }
+
+ log_message(LOG_INFO, "Normalized config.json formatting");
+}
+
/**
* @brief Load paths from JSON.
*/
@@ -1275,9 +1463,6 @@ int load_plugin_config(Config *config, const char *config_path)
if (json_path)
{
- /* Ensure config.json stays protected (contains access_token) */
- chmod(json_path, 0600);
-
log_message(LOG_INFO, "Loading plugin config from: %s", json_path);
json_error_t error;
@@ -1295,6 +1480,9 @@ int load_plugin_config(Config *config, const char *config_path)
load_sensors_from_json(root, config);
load_positioning_from_json(root, config);
+ /* Re-format config.json if CoolerControl wrote it compact */
+ normalize_config_json(json_path, root);
+
json_decref(root);
loaded_from_json = 1;
log_message(LOG_STATUS, "Plugin configuration loaded from JSON");
@@ -1305,6 +1493,9 @@ int load_plugin_config(Config *config, const char *config_path)
json_path, error.text, error.line);
log_message(LOG_STATUS, "Using hardcoded defaults");
}
+
+ /* Load credentials.json (overrides access_token from config.json) */
+ load_credentials_file(json_path, config);
}
else
{
@@ -1314,5 +1505,11 @@ int load_plugin_config(Config *config, const char *config_path)
// Apply defaults for any missing fields
apply_system_defaults(config);
+ /* Auto-persist token to credentials.json (survives config.json updates) */
+ if (json_path)
+ {
+ save_credentials_file(json_path, config);
+ }
+
return loaded_from_json;
}
diff --git a/src/srv/cc_main.c b/src/srv/cc_main.c
index e28cab8..dfa5c37 100644
--- a/src/srv/cc_main.c
+++ b/src/srv/cc_main.c
@@ -22,7 +22,6 @@
#include
#include
#include
-#include
#include
// cppcheck-suppress-end missingIncludeSystem
@@ -271,173 +270,12 @@ const char *get_session_access_token(void)
}
/**
- * @brief Add a string field to multipart form with error checking.
- * @details Helper function to add a named string field to curl_mime form.
+ * @brief Reset CURL options after a request to prepare for the next one.
+ * @details Clears all request-specific CURL options (URL, body, headers, etc.).
*/
-static int add_mime_field(curl_mime *form, const char *field_name,
- const char *field_value)
+static void reset_curl_request_options(void)
{
- curl_mimepart *field = curl_mime_addpart(form);
- if (!field)
- {
- log_message(LOG_ERROR, "Failed to create %s field", field_name);
- return 0;
- }
-
- CURLcode result = curl_mime_name(field, field_name);
- if (result != CURLE_OK)
- {
- log_message(LOG_ERROR, "Failed to set %s field name: %s", field_name,
- curl_easy_strerror(result));
- return 0;
- }
-
- result = curl_mime_data(field, field_value, CURL_ZERO_TERMINATED);
- if (result != CURLE_OK)
- {
- log_message(LOG_ERROR, "Failed to set %s field data: %s", field_name,
- curl_easy_strerror(result));
- return 0;
- }
-
- return 1;
-}
-
-/**
- * @brief Add image file to multipart form with error checking.
- * @details Adds the image file field with proper MIME type and validation.
- */
-static int add_image_file_field(curl_mime *form, const char *image_path)
-{
- curl_mimepart *field = curl_mime_addpart(form);
- if (!field)
- {
- log_message(LOG_ERROR, "Failed to create image field");
- return 0;
- }
-
- CURLcode result = curl_mime_name(field, "images[]");
- if (result != CURLE_OK)
- {
- log_message(LOG_ERROR, "Failed to set image field name: %s",
- curl_easy_strerror(result));
- return 0;
- }
-
- result = curl_mime_filedata(field, image_path);
- if (result != CURLE_OK)
- {
- struct stat file_stat;
- if (stat(image_path, &file_stat) == 0)
- {
- log_message(LOG_ERROR, "Failed to set image file data: %s",
- curl_easy_strerror(result));
- }
- return 0;
- }
-
- result = curl_mime_type(field, "image/png");
- if (result != CURLE_OK)
- {
- log_message(LOG_ERROR, "Failed to set image MIME type: %s",
- curl_easy_strerror(result));
- return 0;
- }
-
- return 1;
-}
-
-/**
- * @brief Build multipart form for LCD image upload.
- * @details Constructs the complete multipart form with mode, brightness,
- * orientation and image fields.
- */
-static curl_mime *build_lcd_upload_form(const Config *config,
- const char *image_path)
-{
- curl_mime *form = curl_mime_init(cc_session.curl_handle);
- if (!form)
- {
- log_message(LOG_ERROR, "Failed to initialize multipart form");
- return NULL;
- }
-
- // Add mode field
- if (!add_mime_field(form, "mode", "image"))
- {
- curl_mime_free(form);
- return NULL;
- }
-
- // Add brightness field
- char brightness_str[8];
- snprintf(brightness_str, sizeof(brightness_str), "%d",
- config->lcd_brightness);
- if (!add_mime_field(form, "brightness", brightness_str))
- {
- curl_mime_free(form);
- return NULL;
- }
-
- // Add orientation field
- char orientation_str[8];
- snprintf(orientation_str, sizeof(orientation_str), "%d",
- config->lcd_orientation);
- if (!add_mime_field(form, "orientation", orientation_str))
- {
- curl_mime_free(form);
- return NULL;
- }
-
- // Add image file
- if (!add_image_file_field(form, image_path))
- {
- curl_mime_free(form);
- return NULL;
- }
-
- return form;
-}
-
-/**
- * @brief Configure CURL for LCD image upload request.
- * @details Sets up URL, form data, SSL and response handling.
- */
-static struct curl_slist *configure_lcd_upload_curl(const Config *config,
- const char *upload_url,
- curl_mime *form,
- http_response *response)
-{
- (void)config;
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_URL, upload_url);
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_MIMEPOST, form);
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_CUSTOMREQUEST, "PUT");
-
- // Set write callback
- curl_easy_setopt(
- cc_session.curl_handle, CURLOPT_WRITEFUNCTION,
- (size_t (*)(const void *, size_t, size_t, void *))write_callback);
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_WRITEDATA, response);
-
- // Add headers
- struct curl_slist *headers = NULL;
- headers = curl_slist_append(headers, "User-Agent: CoolerDash/1.0");
- headers = curl_slist_append(headers, "Accept: application/json");
-
- headers = curl_slist_append(headers, cc_session.access_token);
-
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_HTTPHEADER, headers);
-
- return headers;
-}
-
-/**
- * @brief Cleanup CURL options after LCD upload.
- * @details Resets all CURL options set during the upload request.
- */
-static void cleanup_lcd_upload_curl(void)
-{
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_MIMEPOST, NULL);
+ curl_easy_setopt(cc_session.curl_handle, CURLOPT_POSTFIELDS, NULL);
curl_easy_setopt(cc_session.curl_handle, CURLOPT_CUSTOMREQUEST, NULL);
curl_easy_setopt(cc_session.curl_handle, CURLOPT_WRITEFUNCTION, NULL);
curl_easy_setopt(cc_session.curl_handle, CURLOPT_WRITEDATA, NULL);
@@ -560,20 +398,14 @@ int send_image_to_lcd(const Config *config, const char *image_path,
if (headers)
curl_slist_free_all(headers);
free(json_str);
-
- // Reset CURL options
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_POSTFIELDS, NULL);
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_CUSTOMREQUEST, NULL);
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_WRITEFUNCTION, NULL);
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_WRITEDATA, NULL);
- curl_easy_setopt(cc_session.curl_handle, CURLOPT_HTTPHEADER, NULL);
+ reset_curl_request_options();
return success;
}
/**
* @brief Register shutdown image with CC4's persistent LCD shutdown endpoint.
- * @details Called once at daemon startup. Uploads shutdown.png to
+ * @details Called once at daemon startup. Sends the shutdown image path to
* PUT /devices/{uid}/settings/lcd/lcd/shutdown-image so CoolerControl
* displays it automatically whenever the CC daemon itself stops.
* Gracefully skips if the endpoint is unavailable.
@@ -588,7 +420,7 @@ int register_shutdown_image_with_cc(const Config *config,
if (!image_path || !image_path[0])
return 1;
- /* Verify the file exists before attempting to upload */
+ /* Verify the file exists before attempting to register */
FILE *f = fopen(image_path, "r");
if (!f)
{
@@ -603,22 +435,52 @@ int register_shutdown_image_with_cc(const Config *config,
int written = snprintf(upload_url, sizeof(upload_url),
"%s/devices/%s/settings/lcd/lcd/shutdown-image",
config->daemon_address, device_uid);
- if (written < 0 || (size_t)written >= sizeof(upload_url))
+ if (!validate_snprintf(written, sizeof(upload_url), upload_url))
+ return 0;
+
+ /* Build JSON body matching LcdSettings schema */
+ json_t *body = json_object();
+ if (!body)
+ {
+ log_message(LOG_ERROR, "Failed to create JSON object for shutdown image");
return 0;
+ }
- curl_mime *form = build_lcd_upload_form(config, image_path);
- if (!form)
+ json_object_set_new(body, "mode", json_string("image"));
+ json_object_set_new(body, "image_file_processed", json_string(image_path));
+ json_object_set_new(body, "brightness", json_integer(config->lcd_brightness));
+ json_object_set_new(body, "orientation", json_integer(config->lcd_orientation));
+ json_object_set_new(body, "colors", json_array());
+
+ char *json_str = json_dumps(body, JSON_COMPACT);
+ json_decref(body);
+ if (!json_str)
+ {
+ log_message(LOG_ERROR, "Failed to serialize shutdown image JSON");
return 0;
+ }
http_response response = {0};
if (!cc_init_response_buffer(&response, 4096))
{
- curl_mime_free(form);
+ free(json_str);
return 0;
}
- struct curl_slist *headers =
- configure_lcd_upload_curl(config, upload_url, form, &response);
+ curl_easy_setopt(cc_session.curl_handle, CURLOPT_URL, upload_url);
+ curl_easy_setopt(cc_session.curl_handle, CURLOPT_CUSTOMREQUEST, "PUT");
+ curl_easy_setopt(cc_session.curl_handle, CURLOPT_POSTFIELDS, json_str);
+ curl_easy_setopt(
+ cc_session.curl_handle, CURLOPT_WRITEFUNCTION,
+ (size_t (*)(const void *, size_t, size_t, void *))write_callback);
+ curl_easy_setopt(cc_session.curl_handle, CURLOPT_WRITEDATA, &response);
+
+ struct curl_slist *headers = NULL;
+ headers = curl_slist_append(headers, "Content-Type: application/json");
+ headers = curl_slist_append(headers, "User-Agent: CoolerDash/1.0");
+ headers = curl_slist_append(headers, "Accept: application/json");
+ headers = curl_slist_append(headers, cc_session.access_token);
+ curl_easy_setopt(cc_session.curl_handle, CURLOPT_HTTPHEADER, headers);
CURLcode res = curl_easy_perform(cc_session.curl_handle);
long http_code = -1;
@@ -647,8 +509,8 @@ int register_shutdown_image_with_cc(const Config *config,
cc_cleanup_response_buffer(&response);
if (headers)
curl_slist_free_all(headers);
- curl_mime_free(form);
- cleanup_lcd_upload_curl();
+ free(json_str);
+ reset_curl_request_options();
return success;
}
diff --git a/src/srv/cc_sensor.c b/src/srv/cc_sensor.c
index 21883aa..d36a5a2 100644
--- a/src/srv/cc_sensor.c
+++ b/src/srv/cc_sensor.c
@@ -401,31 +401,40 @@ int is_legacy_sensor_slot(const char *slot_value)
strcmp(slot_value, "none") == 0);
}
+/**
+ * @brief Map legacy slot name to device type string.
+ * @details Returns "CPU", "GPU", "Liquidctl" or NULL for unknown slots.
+ */
+static const char *get_legacy_slot_device_type(const char *slot_value)
+{
+ if (strcmp(slot_value, "cpu") == 0)
+ return "CPU";
+ if (strcmp(slot_value, "gpu") == 0)
+ return "GPU";
+ if (strcmp(slot_value, "liquid") == 0)
+ return "Liquidctl";
+ return NULL;
+}
+
/**
* @brief Resolve a legacy slot value to matching sensor entry.
- * @details Maps "cpu"→first CPU temp, "gpu"→first GPU temp,
- * "liquid"→first Liquidctl temp.
+ * @details Maps "cpu"→first CPU sensor, "gpu"→first GPU sensor,
+ * "liquid"→first Liquidctl sensor matching the given category.
*/
static const sensor_entry_t *resolve_legacy_slot(
- const monitor_sensor_data_t *data, const char *slot_value)
+ const monitor_sensor_data_t *data, const char *slot_value,
+ sensor_category_t category)
{
if (!data || !slot_value)
return NULL;
- const char *target_type = NULL;
- if (strcmp(slot_value, "cpu") == 0)
- target_type = "CPU";
- else if (strcmp(slot_value, "gpu") == 0)
- target_type = "GPU";
- else if (strcmp(slot_value, "liquid") == 0)
- target_type = "Liquidctl";
- else
+ const char *target_type = get_legacy_slot_device_type(slot_value);
+ if (!target_type)
return NULL;
- /* Find first temperature sensor matching the device type */
for (int i = 0; i < data->sensor_count; i++)
{
- if (data->sensors[i].category == SENSOR_CATEGORY_TEMP &&
+ if (data->sensors[i].category == category &&
strcmp(data->sensors[i].device_type, target_type) == 0)
{
return &data->sensors[i];
@@ -438,7 +447,7 @@ static const sensor_entry_t *resolve_legacy_slot(
/**
* @brief Find sensor entry matching a slot value.
* @details Handles both legacy ("cpu","gpu","liquid") and dynamic
- * ("uid:sensor_name") slot resolution.
+ * ("uid:sensor_name") slot resolution. Always matches temperature sensors.
*/
const sensor_entry_t *find_sensor_for_slot(const monitor_sensor_data_t *data,
const char *slot_value)
@@ -448,7 +457,7 @@ const sensor_entry_t *find_sensor_for_slot(const monitor_sensor_data_t *data,
/* Legacy slot resolution */
if (is_legacy_sensor_slot(slot_value))
- return resolve_legacy_slot(data, slot_value);
+ return resolve_legacy_slot(data, slot_value, SENSOR_CATEGORY_TEMP);
/* Dynamic slot: "device_uid:sensor_name" */
const char *separator = strchr(slot_value, ':');
@@ -485,27 +494,7 @@ const sensor_entry_t *find_channel_sensor_for_slot(
/* Legacy slot resolution with custom category */
if (is_legacy_sensor_slot(slot_value))
- {
- const char *target_type = NULL;
- if (strcmp(slot_value, "cpu") == 0)
- target_type = "CPU";
- else if (strcmp(slot_value, "gpu") == 0)
- target_type = "GPU";
- else if (strcmp(slot_value, "liquid") == 0)
- target_type = "Liquidctl";
- else
- return NULL;
-
- for (int i = 0; i < data->sensor_count; i++)
- {
- if (data->sensors[i].category == category &&
- strcmp(data->sensors[i].device_type, target_type) == 0)
- {
- return &data->sensors[i];
- }
- }
- return NULL;
- }
+ return resolve_legacy_slot(data, slot_value, category);
/* Dynamic slot: "device_uid:sensor_name" — match by uid + category */
const char *separator = strchr(slot_value, ':');