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, ':');