diff --git a/um5606_fan_state/README.md b/um5606_fan_state/README.md new file mode 100644 index 0000000..1e86fc1 --- /dev/null +++ b/um5606_fan_state/README.md @@ -0,0 +1 @@ +Set the fan state on the ZenBook S 16 UM5606 (and related laptops). Requires https://github.com/ThatOneCalculator/asus-5606-fan-state diff --git a/um5606_fan_state/fan_state_bar.luau b/um5606_fan_state/fan_state_bar.luau new file mode 100644 index 0000000..dedf6ee --- /dev/null +++ b/um5606_fan_state/fan_state_bar.luau @@ -0,0 +1,69 @@ +-- ASUS UM5606 Fan State - bar widget (thin presentation client). +-- +-- The fan control logic lives in the [[service]] (fan_state_service.luau), which +-- runs for the whole session. This widget only reflects the published "status" +-- and cycles states by writing to the "command" channel. + +local showLabel = noctalia.getConfig("show_label") + +local STATES = { + [0] = { label = "Standard", glyph = "car-fan", color = "on_surface" }, + [1] = { label = "Quiet", glyph = "car-fan-1", color = "secondary" }, + [2] = { label = "High-Performance", glyph = "car-fan-2", color = "tertiary" }, + [3] = { label = "Full", glyph = "car-fan-3", color = "primary" }, +} + +local fanState = -1 +local available = false + +local function send(action) + noctalia.state.set("command", action) +end + +local function applyState(state) + local info = STATES[state] + if not info then + barWidget.setGlyph("car-fan") + if showLabel then + barWidget.setText("Unknown") + end + barWidget.setGlyphColor("on_surface") + barWidget.setColor("on_surface") + return + end + + barWidget.setGlyph(info.glyph) + if showLabel then + barWidget.setText(info.label) + end + barWidget.setGlyphColor(info.color) + barWidget.setColor(info.color) +end + +local function applyStatus(status) + if type(status) == "table" then + fanState = tonumber(status.state) or -1 + available = status.available and true or false + end + + if not available then + barWidget.setGlyph("car-fan") + if showLabel then + barWidget.setText("Unavailable") + end + barWidget.setGlyphColor("on_surface_variant") + barWidget.setColor("on_surface_variant") + return + end + + applyState(fanState) +end + +function onClick() + if not available then return end + send("cycle") +end + +-- Sync the current status on load, then track changes. +applyStatus(noctalia.state.get("status")) +noctalia.state.watch("status", applyStatus) diff --git a/um5606_fan_state/fan_state_service.luau b/um5606_fan_state/fan_state_service.luau new file mode 100644 index 0000000..dd38313 --- /dev/null +++ b/um5606_fan_state/fan_state_service.luau @@ -0,0 +1,129 @@ +-- ASUS UM5606 Fan State - headless service. +-- Owns all fan_state shell calls and runs for the whole session. The bar widget +-- is a thin client: it reads the published "status" and drives this service by +-- writing to the "command" channel. + +local STATE_COUNT = 4 + +local fanState = -1 +local available = false +local refreshPending = false +local lastStatusKey + +local function publishStatus() + local key = tostring(fanState) .. (available and "/1" or "/0") + if key == lastStatusKey then return end + lastStatusKey = key + noctalia.state.set("status", { state = fanState, available = available }) +end + +local function setState(state) + if fanState == state then return end + fanState = state +end + +local function parseFanState(stdout) + return tonumber((stdout or ""):match("^%s*(.-)%s*$")) +end + +local function refreshFanState() + if refreshPending then return end + + refreshPending = true + local ok = noctalia.runAsync("fan_state get --int", function(result) + refreshPending = false + + if result.exitCode ~= 0 then + available = false + noctalia.log("fan_control: failed to get fan state") + publishStatus() + return + end + + local parsed = parseFanState(result.stdout) + if parsed == nil then + available = false + noctalia.log("fan_control: failed to parse fan state") + publishStatus() + return + end + + available = true + setState(parsed) + publishStatus() + end) + + if not ok then + refreshPending = false + available = false + noctalia.log("fan_control: failed to start fan state command") + publishStatus() + end +end + +local function setFanState(nextState) + if type(nextState) ~= "number" then return end + if nextState < 0 or nextState >= STATE_COUNT then return end + + available = true + setState(nextState) + publishStatus() + + local ok = noctalia.runAsync("fan_state set " .. tostring(nextState), function(result) + if result.exitCode ~= 0 then + noctalia.log("fan_control: failed to set fan state") + end + refreshFanState() + end) + + if not ok then + noctalia.log("fan_control: failed to start fan state command") + refreshFanState() + end +end + +local function cycleFanState() + if not available then + refreshFanState() + return + end + + setFanState((fanState + 1) % STATE_COUNT) +end + +local function handleCommand(command) + local action = command + local state = nil + + if type(command) == "table" then + action = command.action + state = tonumber(command.state) + end + + if action == "cycle" then + cycleFanState() + elseif action == "refresh" then + refreshFanState() + elseif action == "set" then + setFanState(state) + elseif type(action) == "string" then + local parsed = tonumber(action:match("^set%s+([0-3])$")) + if parsed ~= nil then + setFanState(parsed) + else + noctalia.log("fan_control: unknown command '" .. tostring(action) .. "'") + end + end +end + +local pollInterval = tonumber(noctalia.getConfig("poll_interval")) or 2000 +noctalia.setUpdateInterval(pollInterval) + +publishStatus() +refreshFanState() + +function update() + refreshFanState() +end + +noctalia.state.watch("command", handleCommand) diff --git a/um5606_fan_state/plugin.toml b/um5606_fan_state/plugin.toml new file mode 100644 index 0000000..46ec27d --- /dev/null +++ b/um5606_fan_state/plugin.toml @@ -0,0 +1,38 @@ +id = "thatonecalculator/um5606_fan_state" +name = "ASUS UM5606 Fan State" +version = "1.0.6" +min_noctalia = "5.0.0" +author = "ThatOneCalculator" +license = "MIT" +tags = ["hardware"] +icon = "car-fan" +description = "Set the fan state on the ZenBook S 16 UM5606." +dependencies = ["fan_state"] + +[[setting]] +key = "show_label" +type = "bool" +label_key = "settings.show_label.label" +default = false + +[[setting]] +key = "poll_interval" +type = "int" +label_key = "settings.poll_interval.label" +description_key="settings.poll_interval.description" +default = 2000 +min = 250 +max = 60000 +advanced = true + +[[service]] +id = "service" +entry = "fan_state_service.luau" + +[[widget]] +id = "fan_state" +entry = "fan_state_bar.luau" + +[[shortcut]] +id = "toggle" +entry = "shortcut.luau" diff --git a/um5606_fan_state/shortcut.luau b/um5606_fan_state/shortcut.luau new file mode 100644 index 0000000..3c4835e --- /dev/null +++ b/um5606_fan_state/shortcut.luau @@ -0,0 +1,42 @@ +-- ASUS UM5606 Fan State - control-center quick tile. +-- +-- This tile holds no fan control logic of its own. It mirrors the bar widget +-- through the plugin's shared state: +-- reads noctalia.state "status" -> { state = ..., available = ... } +-- writes noctalia.state "command" -> "cycle" + +local STATES = { + [0] = { label = "Standard", glyph = "car-fan" }, + [1] = { label = "Quiet", glyph = "car-fan-1" }, + [2] = { label = "High-Performance", glyph = "car-fan-2" }, + [3] = { label = "Full", glyph = "car-fan-3" }, +} + +local available = false + +local function applyStatus(status) + local state = type(status) == "table" and tonumber(status.state) or nil + available = type(status) == "table" and status.available and true or false + + local info = STATES[state] + if info then + shortcut.setLabel(info.label) + shortcut.setIcon(info.glyph, "car-fan") + shortcut.setActive(state ~= 0) + else + shortcut.setLabel(available and "Unknown" or "Unavailable") + shortcut.setIcon("car-fan", "car-fan") + shortcut.setActive(false) + end + + shortcut.setEnabled(available) +end + +-- Reflect whatever the service last published, then track live changes. +applyStatus(noctalia.state.get("status")) +noctalia.state.watch("status", applyStatus) + +function onClick() + if not available then return end + noctalia.state.set("command", "cycle") +end diff --git a/um5606_fan_state/thumbnail.webp b/um5606_fan_state/thumbnail.webp new file mode 100644 index 0000000..4d9c51c Binary files /dev/null and b/um5606_fan_state/thumbnail.webp differ diff --git a/um5606_fan_state/translations/en.json b/um5606_fan_state/translations/en.json new file mode 100644 index 0000000..815535d --- /dev/null +++ b/um5606_fan_state/translations/en.json @@ -0,0 +1,12 @@ +{ + "title": "ASUS UM5606 Fan State", + "settings": { + "show_label": { + "label": "Show label" + }, + "poll_interval": { + "label": "Poll interval", + "description": "Poll interval in ms" + } + } +} \ No newline at end of file