Skip to content
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Topics include the hostname to support multiple PCs:
| Topic | Type | Description |
|-------|------|-------------|
| `pc-control/<hostname>/sleep` | Command | Put PC to sleep (send `1`, `true`, `on`, or `yes`) |
| `pc-control/<hostname>/monitor-off` | Command | Turn off monitor (send `1`, `true`, `on`, or `yes`) |
| `pc-control/<hostname>/monitor-off` | Command | Turn off monitor (empty payload is accepted; `1`, `true`, `on`, or `yes` are also accepted) |
| `pc-control/<hostname>/status` | Status | `online` / `offline` (retained) |
| `pc-control/<hostname>/version` | Info | Version string, e.g. `1.0.0` (retained) |

Expand Down Expand Up @@ -155,6 +155,9 @@ mosquitto_pub -h 192.168.1.100 -u myuser -P mypassword -t "pc-control/desktop-pc
# Turn off monitor
mosquitto_pub -h 192.168.1.100 -u myuser -P mypassword -t "pc-control/desktop-pc/monitor-off" -m "1"

# Turn off monitor with an empty trigger payload
mosquitto_pub -h 192.168.1.100 -u myuser -P mypassword -t "pc-control/desktop-pc/monitor-off" -n

# Check status (subscribe)
mosquitto_sub -h 192.168.1.100 -u myuser -P mypassword -t "pc-control/+/status" -v
```
Expand Down Expand Up @@ -194,6 +197,7 @@ The project uses GitHub Actions to build and release automatically.

| Version | Date | Notes |
|---------|------------|---------------------------------------------------------------------------------------------------------------|
| 1.1.1 | 09.06.2026 | Accept empty MQTT payload as a trigger for `monitor-off` and improve diagnostic logging. |
| 1.1.0 | 02.03.2026 | Add new, separate "invisible" executable and statically link paho-mqtt, so only 1 file is required for distribution |
| 1.0.1 | 02.02.2026 | Add logging to file, improved Home Assistant support, status topic (`online`/`offline`) via LWT, monitor off command. |
| 1.0.0 | 01.02.2026 | Initial release |
154 changes: 149 additions & 5 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <stdarg.h>
#include <windows.h>
#include <powrprof.h>
#include <MQTTClient.h>
Expand Down Expand Up @@ -71,6 +72,17 @@ static void log_action(const char *action) {
}
}

static void log_actionf(const char *fmt, ...) {
char msg[1024];
va_list args;

va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);

log_action(msg);
}

static void log_windows_error(const char *action, DWORD error_code) {
char msg[256];

Expand All @@ -90,6 +102,40 @@ static void log_startup(void) {
log_action(msg);
}

static void get_user_object_name(HANDLE handle, char *buf, size_t buf_size) {
DWORD needed = 0;

if (buf_size == 0) {
return;
}

if (handle == NULL ||
!GetUserObjectInformationA(handle, UOI_NAME, buf, (DWORD)buf_size, &needed)) {
snprintf(buf, buf_size, "<unknown>");
}
}

static void log_runtime_config(const char *address, const char *hostname) {
DWORD pid = GetCurrentProcessId();
DWORD session_id = 0;
char window_station[128];
char desktop[128];

if (!ProcessIdToSessionId(pid, &session_id)) {
session_id = (DWORD)-1;
}

get_user_object_name(GetProcessWindowStation(), window_station, sizeof(window_station));
get_user_object_name(GetThreadDesktop(GetCurrentThreadId()), desktop, sizeof(desktop));

log_actionf("Process context: pid=%lu, session_id=%lu, window_station=%s, desktop=%s",
(unsigned long)pid, (unsigned long)session_id, window_station, desktop);
log_actionf("Runtime config: address=%s, hostname=%s, client_id=%s",
address, hostname, client_id);
log_actionf("Topics: sleep=%s, monitor_off=%s, status=%s, version=%s",
topic_sleep, topic_monitor_off, topic_status, topic_version);
}

static void sanitize_hostname(char *dest, const char *src, size_t dest_size) {
size_t j = 0;
for (size_t i = 0; src[i] && j < dest_size - 1; i++) {
Expand All @@ -116,6 +162,8 @@ static void do_sleep(void) {
log_action("SLEEP command received - entering sleep mode");
if (!SetSuspendState(FALSE, FALSE, FALSE)) {
log_windows_error("SetSuspendState", GetLastError());
} else {
log_action("SetSuspendState returned success");
}
}

Expand All @@ -127,6 +175,9 @@ static void do_monitor_off(void) {
if (SendMessageTimeout(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)2,
SMTO_ABORTIFHUNG, MONITOR_POWER_TIMEOUT_MS, &result) == 0) {
log_windows_error("SendMessageTimeout(SC_MONITORPOWER)", GetLastError());
} else {
log_actionf("SendMessageTimeout(SC_MONITORPOWER) returned success, result=%lu",
(unsigned long)result);
}
}

Expand Down Expand Up @@ -184,19 +235,96 @@ static int is_valid_command_payload(const MQTTClient_message *msg) {
payload_token_equals(payload + start, end - start, "yes");
}

static int should_accept_command_message(const MQTTClient_message *msg) {
return msg != NULL && !msg->retained && is_valid_command_payload(msg);
static int is_empty_payload(const MQTTClient_message *msg) {
return msg == NULL || msg->payload == NULL || msg->payloadlen <= 0;
}

static void format_payload_preview(const MQTTClient_message *msg, char *buf, size_t buf_size) {
const unsigned char *payload;
size_t len;
size_t out = 0;

if (buf_size == 0) {
return;
}

if (msg == NULL || msg->payload == NULL || msg->payloadlen <= 0) {
snprintf(buf, buf_size, "<empty>");
return;
}

payload = (const unsigned char *)msg->payload;
len = (size_t)msg->payloadlen;

for (size_t i = 0; i < len && out + 1 < buf_size; i++) {
unsigned char c = payload[i];

if (c == '\r' || c == '\n' || c == '\t') {
if (out + 2 >= buf_size) {
break;
}
buf[out++] = ' ';
} else if (isprint(c)) {
buf[out++] = (char)c;
} else {
buf[out++] = '.';
}
}

if (len > out && out + 4 < buf_size) {
buf[out++] = '.';
buf[out++] = '.';
buf[out++] = '.';
}

buf[out] = '\0';
}

static int message_arrived(void *context, char *topic, int topic_len, MQTTClient_message *msg) {
(void)context;
(void)topic_len;

if (should_accept_command_message(msg)) {
char payload_preview[128];
int retained = msg != NULL ? msg->retained : 0;
int qos = msg != NULL ? msg->qos : 0;
int dup = msg != NULL ? msg->dup : 0;
int payloadlen = msg != NULL ? msg->payloadlen : 0;

format_payload_preview(msg, payload_preview, sizeof(payload_preview));
log_actionf("MQTT message arrived: topic=%s, payload_len=%d, qos=%d, retained=%d, dup=%d, payload=\"%s\"",
topic ? topic : "<null>", payloadlen, qos, retained, dup, payload_preview);

if (topic == NULL) {
log_action("MQTT message ignored: topic is null");
} else if (msg == NULL) {
log_action("MQTT message ignored: message is null");
} else if (msg->retained) {
log_action("MQTT command ignored: retained message");
} else {
if (strcmp(topic, topic_sleep) == 0) {
if (!is_valid_command_payload(msg)) {
log_action("MQTT sleep command ignored: invalid payload");
MQTTClient_freeMessage(&msg);
MQTTClient_free(topic);
return 1;
}
log_action("MQTT command accepted: sleep queued");
queue_command(COMMAND_SLEEP);
} else if (strcmp(topic, topic_monitor_off) == 0) {
if (is_empty_payload(msg)) {
log_action("MQTT monitor-off command accepted: empty payload trigger");
} else if (is_valid_command_payload(msg)) {
log_action("MQTT monitor-off command accepted: valid payload trigger");
} else {
log_action("MQTT monitor-off command ignored: invalid payload");
MQTTClient_freeMessage(&msg);
MQTTClient_free(topic);
return 1;
}
log_action("MQTT command accepted: monitor-off queued");
queue_command(COMMAND_MONITOR_OFF);
} else {
log_actionf("MQTT command ignored: topic does not match subscribed commands (topic=%s)", topic);
}
}

Expand All @@ -211,7 +339,7 @@ static void connection_lost(void *context, char *cause) {
#ifndef HIDDEN_BUILD
fprintf(stderr, "Connection lost: %s\n", cause ? cause : "unknown");
#endif
log_action("MQTT connection lost");
log_actionf("MQTT connection lost: cause=%s", cause ? cause : "unknown");
}

static BOOL WINAPI console_handler(DWORD signal) {
Expand Down Expand Up @@ -252,32 +380,47 @@ static int publish_retained(MQTTClient client, const char *topic, const char *pa
static int subscribe_topics(MQTTClient client) {
int rc;
if ((rc = MQTTClient_subscribe(client, topic_sleep, QOS)) != MQTTCLIENT_SUCCESS) {
log_actionf("MQTT subscribe failed: topic=%s, rc=%d", topic_sleep, rc);
fprintf(stderr, "Failed to subscribe to %s: %d\n", topic_sleep, rc);
return rc;
}
log_actionf("MQTT subscribe ok: topic=%s, qos=%d", topic_sleep, QOS);
if ((rc = MQTTClient_subscribe(client, topic_monitor_off, QOS)) != MQTTCLIENT_SUCCESS) {
log_actionf("MQTT subscribe failed: topic=%s, rc=%d", topic_monitor_off, rc);
fprintf(stderr, "Failed to subscribe to %s: %d\n", topic_monitor_off, rc);
return rc;
}
log_actionf("MQTT subscribe ok: topic=%s, qos=%d", topic_monitor_off, QOS);
return MQTTCLIENT_SUCCESS;
}

static int publish_birth_messages(MQTTClient client) {
int rc;
if ((rc = publish_retained(client, topic_status, STATUS_ONLINE)) != MQTTCLIENT_SUCCESS) {
log_actionf("MQTT publish failed: topic=%s, payload=%s, retained=1, rc=%d",
topic_status, STATUS_ONLINE, rc);
fprintf(stderr, "Failed to publish status: %d\n", rc);
return rc;
}
log_actionf("MQTT publish ok: topic=%s, payload=%s, retained=1",
topic_status, STATUS_ONLINE);
if ((rc = publish_retained(client, topic_version, VERSION)) != MQTTCLIENT_SUCCESS) {
log_actionf("MQTT publish failed: topic=%s, payload=%s, retained=1, rc=%d",
topic_version, VERSION, rc);
fprintf(stderr, "Failed to publish version: %d\n", rc);
return rc;
}
log_actionf("MQTT publish ok: topic=%s, payload=%s, retained=1",
topic_version, VERSION);
return MQTTCLIENT_SUCCESS;
}

static int try_connect(MQTTClient client, MQTTClient_connectOptions *conn_opts, const char *address) {
log_actionf("MQTT connect attempt: address=%s, client_id=%s, clean_session=%d, keep_alive=%d",
address, client_id, conn_opts->cleansession, conn_opts->keepAliveInterval);
int rc = MQTTClient_connect(client, conn_opts);
if (rc != MQTTCLIENT_SUCCESS) {
log_actionf("MQTT connect failed: rc=%d", rc);
return rc;
}

Expand All @@ -294,7 +437,7 @@ static int try_connect(MQTTClient client, MQTTClient_connectOptions *conn_opts,
}

set_connected(1);
log_action("Connected to MQTT broker");
log_actionf("Connected to MQTT broker: address=%s, client_id=%s", address, client_id);
printf("Connected to %s\n", address);
printf("Status: %s -> %s\n", topic_status, STATUS_ONLINE);
printf("Subscribed to:\n %s\n %s\n", topic_sleep, topic_monitor_off);
Expand Down Expand Up @@ -394,6 +537,7 @@ int main(int argc, char *argv[]) {
printf("pc-control v%s\n", VERSION);
printf("Device: %s\n", hostname);
log_startup();
log_runtime_config(address, hostname);

MQTTClient client;
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
Expand Down
4 changes: 2 additions & 2 deletions src/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

#define PC_CONTROL_VERSION_MAJOR 1
#define PC_CONTROL_VERSION_MINOR 1
#define PC_CONTROL_VERSION_PATCH 0
#define PC_CONTROL_VERSION_PATCH 1

#define VERSION "1.1.0"
#define VERSION "1.1.1"
#define PC_CONTROL_VERSION_COMMA PC_CONTROL_VERSION_MAJOR,PC_CONTROL_VERSION_MINOR,PC_CONTROL_VERSION_PATCH,0

#endif
Loading