refactor: improve robustness and consistency across all scripts
- kb-blackout: optimize trim(), fix log units, add jit fallback - ac-power: extract error handling helper, add capture_stderr, validate timeout, add security note, rename CONFIG to config - touch-gestures: add min_brightness config, optimize brightnessctl to single call, improve wpctl regex, add shutdown handler
This commit is contained in:
@@ -7,10 +7,14 @@
|
||||
* Survives flaky networks with capped exponential backoff, optional HUD error messages, and redraws of the last known value after OSD clears.
|
||||
|
||||
Configuration:
|
||||
Edit the CONFIG table below to match your Home Assistant deployment; the script ignores script-opts files.
|
||||
Edit the config table below to match your Home Assistant deployment; the script ignores script-opts files.
|
||||
|
||||
Security:
|
||||
The ha_token is a long-lived access token. Avoid committing it to version control.
|
||||
Consider storing this script outside of shared repositories or using a separate config file.
|
||||
]]
|
||||
|
||||
local CONFIG = {
|
||||
local config = {
|
||||
sensor_id = "sensor.ac_power", -- Home Assistant entity id
|
||||
ha_base_url = "http://homeassistant.local:8123",
|
||||
ha_token = "REPLACE_WITH_LONG_LIVED_TOKEN",
|
||||
@@ -60,58 +64,60 @@ end
|
||||
local function build_url()
|
||||
return string.format(
|
||||
"%s/api/states/%s",
|
||||
trim_trailing_slash(CONFIG.ha_base_url),
|
||||
CONFIG.sensor_id
|
||||
trim_trailing_slash(config.ha_base_url),
|
||||
config.sensor_id
|
||||
)
|
||||
end
|
||||
|
||||
local function build_request()
|
||||
local args = { CONFIG.curl_binary, "-sS", "-m", tostring(CONFIG.http_timeout) }
|
||||
if CONFIG.insecure_ssl then
|
||||
local timeout = tonumber(config.http_timeout) or 2
|
||||
local args = { config.curl_binary, "-sS", "-m", tostring(timeout) }
|
||||
if config.insecure_ssl then
|
||||
table.insert(args, "-k")
|
||||
end
|
||||
table.insert(args, "-H")
|
||||
table.insert(args, "Authorization: Bearer " .. CONFIG.ha_token)
|
||||
table.insert(args, "Authorization: Bearer " .. config.ha_token)
|
||||
table.insert(args, build_url())
|
||||
return {
|
||||
name = "subprocess",
|
||||
playback_only = false,
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
args = args,
|
||||
}
|
||||
end
|
||||
|
||||
local function format_value(value)
|
||||
local fmt = "%0." .. tostring(CONFIG.decimal_places) .. "f"
|
||||
local fmt = "%0." .. tostring(config.decimal_places) .. "f"
|
||||
return string.format(fmt, value)
|
||||
end
|
||||
|
||||
local function sensor_units(payload)
|
||||
if CONFIG.use_sensor_unit and payload and payload.attributes then
|
||||
if config.use_sensor_unit and payload and payload.attributes then
|
||||
local unit = payload.attributes.unit_of_measurement
|
||||
if unit and unit ~= "" then
|
||||
return unit
|
||||
end
|
||||
end
|
||||
return CONFIG.units_label
|
||||
return config.units_label
|
||||
end
|
||||
|
||||
local function show_message(text, duration)
|
||||
local timeout = duration
|
||||
if timeout == nil then
|
||||
timeout = CONFIG.display_timeout
|
||||
timeout = config.display_timeout
|
||||
end
|
||||
mp.osd_message(text, timeout)
|
||||
last_display_ts = mp.get_time()
|
||||
end
|
||||
|
||||
local function show_value(value_text, unit)
|
||||
show_message(string.format("%s: %s %s", CONFIG.display_label, value_text, unit or ""))
|
||||
show_message(string.format("%s: %s %s", config.display_label, value_text, unit or ""))
|
||||
end
|
||||
|
||||
local function show_error(err)
|
||||
if CONFIG.show_errors then
|
||||
show_message(string.format("%s: -- (%s)", CONFIG.display_label, err))
|
||||
if config.show_errors then
|
||||
show_message(string.format("%s: -- (%s)", config.display_label, err))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -125,6 +131,20 @@ local function ensure_poll_timer(delay, fn)
|
||||
end)
|
||||
end
|
||||
|
||||
local poll_sensor
|
||||
|
||||
local function schedule_retry()
|
||||
backoff_failures = backoff_failures + 1
|
||||
local next_delay = math.min(config.max_backoff, config.poll_interval * (2 ^ backoff_failures))
|
||||
ensure_poll_timer(next_delay, poll_sensor)
|
||||
end
|
||||
|
||||
local function handle_poll_error(message)
|
||||
log("error", message)
|
||||
show_error(message)
|
||||
schedule_retry()
|
||||
end
|
||||
|
||||
local function meets_delta_threshold(value)
|
||||
if not value then
|
||||
return false
|
||||
@@ -134,11 +154,11 @@ local function meets_delta_threshold(value)
|
||||
end
|
||||
|
||||
local delta = math.abs(value - last_watts)
|
||||
if delta >= CONFIG.min_delta_watts then
|
||||
if delta >= config.min_delta_watts then
|
||||
return true
|
||||
end
|
||||
|
||||
if CONFIG.min_delta_percent > 0 then
|
||||
if config.min_delta_percent > 0 then
|
||||
local baseline = math.abs(last_watts)
|
||||
if baseline < 1e-6 then
|
||||
baseline = math.abs(value)
|
||||
@@ -147,7 +167,7 @@ local function meets_delta_threshold(value)
|
||||
baseline = 1
|
||||
end
|
||||
local percent_delta = (delta / baseline) * 100
|
||||
if percent_delta >= CONFIG.min_delta_percent then
|
||||
if percent_delta >= config.min_delta_percent then
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -156,7 +176,7 @@ local function meets_delta_threshold(value)
|
||||
end
|
||||
|
||||
local function thresholds_enabled()
|
||||
return (CONFIG.min_delta_watts or 0) > 0 or (CONFIG.min_delta_percent or 0) > 0
|
||||
return (config.min_delta_watts or 0) > 0 or (config.min_delta_percent or 0) > 0
|
||||
end
|
||||
|
||||
local function handle_success(value, payload)
|
||||
@@ -164,14 +184,14 @@ local function handle_success(value, payload)
|
||||
local now = mp.get_time()
|
||||
local printable = value and format_value(value) or "--"
|
||||
local unit = sensor_units(payload)
|
||||
local force_due = CONFIG.force_refresh_interval > 0
|
||||
and (now - last_display_ts) >= CONFIG.force_refresh_interval
|
||||
local force_due = config.force_refresh_interval > 0
|
||||
and (now - last_display_ts) >= config.force_refresh_interval
|
||||
local significant_change = meets_delta_threshold(value)
|
||||
|
||||
local should_render = significant_change or last_watts == nil
|
||||
if not should_render and force_due then
|
||||
local allow_force = true
|
||||
if thresholds_enabled() and CONFIG.force_refresh_obeys_threshold then
|
||||
if thresholds_enabled() and config.force_refresh_obeys_threshold then
|
||||
allow_force = false
|
||||
end
|
||||
if allow_force then
|
||||
@@ -191,15 +211,15 @@ local function redraw_last_value()
|
||||
show_value(format_value(last_watts), last_unit)
|
||||
return
|
||||
end
|
||||
if CONFIG.initial_message and CONFIG.initial_message ~= "" then
|
||||
if config.initial_message and config.initial_message ~= "" then
|
||||
show_message(
|
||||
string.format("%s: %s", CONFIG.display_label, CONFIG.initial_message),
|
||||
CONFIG.initial_message_duration
|
||||
string.format("%s: %s", config.display_label, config.initial_message),
|
||||
config.initial_message_duration
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local function poll_sensor()
|
||||
poll_sensor = function()
|
||||
if request_inflight then
|
||||
return
|
||||
end
|
||||
@@ -209,42 +229,25 @@ local function poll_sensor()
|
||||
request_inflight = false
|
||||
|
||||
if not success or not result then
|
||||
backoff_failures = backoff_failures + 1
|
||||
local message = err or "request failed"
|
||||
log("error", message)
|
||||
show_error(message)
|
||||
local next_delay = math.min(CONFIG.max_backoff, CONFIG.poll_interval * (2 ^ backoff_failures))
|
||||
ensure_poll_timer(next_delay, poll_sensor)
|
||||
handle_poll_error(err or "request failed")
|
||||
return
|
||||
end
|
||||
|
||||
if result.status ~= 0 then
|
||||
backoff_failures = backoff_failures + 1
|
||||
local stderr = (result.stderr and result.stderr ~= "" and result.stderr)
|
||||
or ("curl exit " .. tostring(result.status))
|
||||
log("error", stderr)
|
||||
show_error(stderr)
|
||||
local next_delay = math.min(CONFIG.max_backoff, CONFIG.poll_interval * (2 ^ backoff_failures))
|
||||
ensure_poll_timer(next_delay, poll_sensor)
|
||||
handle_poll_error(stderr)
|
||||
return
|
||||
end
|
||||
|
||||
if not result.stdout or result.stdout == "" then
|
||||
backoff_failures = backoff_failures + 1
|
||||
log("error", "empty response from Home Assistant")
|
||||
show_error("empty response")
|
||||
local next_delay = math.min(CONFIG.max_backoff, CONFIG.poll_interval * (2 ^ backoff_failures))
|
||||
ensure_poll_timer(next_delay, poll_sensor)
|
||||
handle_poll_error("empty response from Home Assistant")
|
||||
return
|
||||
end
|
||||
|
||||
local payload = utils.parse_json(result.stdout)
|
||||
if not payload then
|
||||
backoff_failures = backoff_failures + 1
|
||||
log("error", "invalid JSON payload")
|
||||
show_error("invalid JSON")
|
||||
local next_delay = math.min(CONFIG.max_backoff, CONFIG.poll_interval * (2 ^ backoff_failures))
|
||||
ensure_poll_timer(next_delay, poll_sensor)
|
||||
handle_poll_error("invalid JSON payload")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -257,22 +260,22 @@ local function poll_sensor()
|
||||
end
|
||||
|
||||
backoff_failures = 0
|
||||
ensure_poll_timer(CONFIG.poll_interval, poll_sensor)
|
||||
ensure_poll_timer(config.poll_interval, poll_sensor)
|
||||
end)
|
||||
end
|
||||
|
||||
local function validate_config()
|
||||
if CONFIG.ha_base_url == "" then
|
||||
if config.ha_base_url == "" then
|
||||
log("error", "ha_base_url is required")
|
||||
show_error("missing base url")
|
||||
return false
|
||||
end
|
||||
if CONFIG.sensor_id == "" then
|
||||
if config.sensor_id == "" then
|
||||
log("error", "sensor_id is required")
|
||||
show_error("missing sensor id")
|
||||
return false
|
||||
end
|
||||
if CONFIG.ha_token == "" or CONFIG.ha_token == "REPLACE_WITH_LONG_LIVED_TOKEN" then
|
||||
if config.ha_token == "" or config.ha_token == "REPLACE_WITH_LONG_LIVED_TOKEN" then
|
||||
log("error", "ha_token must be set to a Home Assistant long-lived token")
|
||||
show_error("missing HA token")
|
||||
return false
|
||||
@@ -284,10 +287,10 @@ local function start()
|
||||
if not validate_config() then
|
||||
return
|
||||
end
|
||||
if CONFIG.initial_message and CONFIG.initial_message ~= "" then
|
||||
if config.initial_message and config.initial_message ~= "" then
|
||||
show_message(
|
||||
string.format("%s: %s", CONFIG.display_label, CONFIG.initial_message),
|
||||
CONFIG.initial_message_duration
|
||||
string.format("%s: %s", config.display_label, config.initial_message),
|
||||
config.initial_message_duration
|
||||
)
|
||||
end
|
||||
ensure_poll_timer(0.1, poll_sensor)
|
||||
|
||||
Reference in New Issue
Block a user