Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -880,12 +880,16 @@ func rpcSetATXPowerAction(action string) error {
type ATXState struct {
Power bool `json:"power"`
HDD bool `json:"hdd"`
// Responsive reports whether the ATX extension board has sent a status
// line recently, so the UI can flag an unpowered or disconnected board.
Responsive bool `json:"responsive"`
}

func rpcGetATXState() (ATXState, error) {
state := ATXState{
Power: ledPWRState.Load(),
HDD: ledHDDState.Load(),
Power: ledPWRState.Load(),
HDD: ledHDDState.Load(),
Responsive: atxBoardResponsive(),
}
return state, nil
}
Expand Down
5 changes: 3 additions & 2 deletions mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,9 @@ func (m *MQTTManager) onConnect(client mqtt.Client) {
// the current state of all switches and sensors right away.
if config.ActiveExtension == "atx-power" {
m.publishATXState(ATXState{
Power: ledPWRState.Load(),
HDD: ledHDDState.Load(),
Power: ledPWRState.Load(),
HDD: ledHDDState.Load(),
Responsive: atxBoardResponsive(),
})
}
if config.ActiveExtension == "dc-power" {
Expand Down
5 changes: 3 additions & 2 deletions mqtt_publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,8 +563,9 @@ func (m *MQTTManager) startPeriodicStatusUpdates(interval time.Duration) {
// Publish current ATX state only if ATX extension is active
if config.ActiveExtension == "atx-power" {
m.publishATXState(ATXState{
Power: ledPWRState.Load(),
HDD: ledHDDState.Load(),
Power: ledPWRState.Load(),
HDD: ledHDDState.Load(),
Responsive: atxBoardResponsive(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish ATX responsiveness changes through MQTT debounce

With the default MQTT debounce_ms of 500, this snapshot still goes through publishATXState, whose debouncer only republishes on Power/HDD changes or the first frame and otherwise returns early for unchanged LED states. When the board stops sending lines but the last Power/HDD values are unchanged, the periodic snapshot computes Responsive: false here but no retained atx/state update is emitted, so MQTT/Home Assistant keeps reporting the board as responsive until an LED transition or MQTT manager restart. Publish responsiveness-only changes outside the HDD debounce path or include Responsive in the debounce change detection.

Useful? React with 👍 / 👎.

})
}

Expand Down
25 changes: 23 additions & 2 deletions serial.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var consoleBroker *ConsoleBroker

func mountATXControl() error {
_ = port.SetMode(defaultMode)
atxLastLineTime.Store(0)
go runATXControl()

return nil
Expand All @@ -38,8 +39,24 @@ var (
ledPWRState atomic.Bool
btnRSTState bool
btnPWRState bool

// atxLastLineTime holds the unix time (in seconds) of the last status
// line received from the ATX extension board. Zero means no line has
// been received since the extension was mounted.
atxLastLineTime atomic.Int64
)

// atxResponsiveTimeout is how long after the last received status line the
// ATX extension board is still considered responsive.
const atxResponsiveTimeout = 10 * time.Second

// atxBoardResponsive reports whether the ATX extension board has sent a
// status line within atxResponsiveTimeout.
func atxBoardResponsive() bool {
lastLine := atxLastLineTime.Load()
return lastLine != 0 && time.Since(time.Unix(lastLine, 0)) < atxResponsiveTimeout
}

func runATXControl() {
scopedLogger := serialLogger.With().Str("service", "atx_control").Logger()

Expand All @@ -51,6 +68,9 @@ func runATXControl() {
return
}

// The board is talking to us, even if the line turns out invalid.
atxLastLineTime.Store(time.Now().Unix())

// Each line should be 4 binary digits + newline
if len(line) != 5 {
scopedLogger.Warn().Int("length", len(line)).Msg("Invalid line length")
Expand All @@ -64,8 +84,9 @@ func runATXControl() {
newBtnPWRState := line[3] == '1'

atxState := ATXState{
Power: newLedPWRState,
HDD: newLedHDDState,
Power: newLedPWRState,
HDD: newLedHDDState,
Responsive: true, // a status line was just received
}

if currentSession != nil {
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/cy.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"appearance_theme_system": "System",
"appearance_title": "Ymddangosiad",
"attach": "Atodi",
"atx_power_control_board_not_responding": "Dim ymateb gan y bwrdd estyniad ATX. Gwiriwch y cebl RJ-12 a phŵer y bwrdd.",
"atx_power_control_get_state_error": "Methwyd cael cyflwr pŵer ATX: {error}",
"atx_power_control_hdd_led": "LED HDD",
"atx_power_control_hold_hint": "Dal am 3 eiliad i orfodi diffodd",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "System",
"appearance_title": "Udseende",
"attach": "Vedhæft",
"atx_power_control_board_not_responding": "Intet svar fra ATX-udvidelseskortet. Kontrollér RJ-12-kablet og kortets strømforsyning.",
"atx_power_control_get_state_error": "Kunne ikke hente ATX-strømtilstand: {error}",
"atx_power_control_hdd_led": "HDD-LED",
"atx_power_control_hold_hint": "Hold nede i 3 sek. for at tvinge slukning",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "System",
"appearance_title": "Darstellung",
"attach": "Anhängen",
"atx_power_control_board_not_responding": "Keine Antwort von der ATX-Erweiterungsplatine. Prüfen Sie das RJ-12-Kabel und die Stromversorgung der Platine.",
"atx_power_control_get_state_error": "ATX-Stromversorgungsstatus konnte nicht abgerufen werden: {error}",
"atx_power_control_hdd_led": "Festplatten-LED",
"atx_power_control_hold_hint": "3 Sekunden halten zum Ausschalten erzwingen",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "System",
"appearance_title": "Appearance",
"attach": "Attach",
"atx_power_control_board_not_responding": "No response from the ATX extension board. Check the RJ-12 cable and board power.",
"atx_power_control_get_state_error": "Failed to get ATX power state: {error}",
"atx_power_control_hdd_led": "HDD LED",
"atx_power_control_hold_hint": "Hold for 3s to force off",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "Sistema",
"appearance_title": "Apariencia",
"attach": "Adjuntar",
"atx_power_control_board_not_responding": "No hay respuesta de la placa de extensión ATX. Compruebe el cable RJ-12 y la alimentación de la placa.",
"atx_power_control_get_state_error": "No se pudo obtener el estado de energía ATX: {error}",
"atx_power_control_hdd_led": "LED del disco duro",
"atx_power_control_hold_hint": "Mantenga presionado 3 s para forzar el apagado",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "Système",
"appearance_title": "Apparence",
"attach": "Attacher",
"atx_power_control_board_not_responding": "Aucune réponse de la carte d'extension ATX. Vérifiez le câble RJ-12 et l'alimentation de la carte.",
"atx_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation ATX : {error}",
"atx_power_control_hdd_led": "Voyant du disque dur",
"atx_power_control_hold_hint": "Maintenir 3 s pour forcer l'arrêt",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "Sistema",
"appearance_title": "Aspetto",
"attach": "Allega",
"atx_power_control_board_not_responding": "Nessuna risposta dalla scheda di estensione ATX. Controlla il cavo RJ-12 e l'alimentazione della scheda.",
"atx_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione ATX: {error}",
"atx_power_control_hdd_led": "LED dell'HDD",
"atx_power_control_hold_hint": "Tenere premuto 3 s per lo spegnimento forzato",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "システム",
"appearance_title": "外観",
"attach": "接続",
"atx_power_control_board_not_responding": "ATX拡張ボードから応答がありません。RJ-12ケーブルとボードの電源を確認してください。",
"atx_power_control_get_state_error": "ATX電源状態の取得に失敗しました: {error}",
"atx_power_control_hdd_led": "HDD LED",
"atx_power_control_hold_hint": "3秒長押しで強制オフ",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "System",
"appearance_title": "Utseende",
"attach": "Legg ved",
"atx_power_control_board_not_responding": "Ingen respons fra ATX-utvidelseskortet. Kontroller RJ-12-kabelen og strømforsyningen til kortet.",
"atx_power_control_get_state_error": "Klarte ikke å hente ATX-strømstatus: {error}",
"atx_power_control_hdd_led": "HDD-LED",
"atx_power_control_hold_hint": "Hold inne 3 s for å tvinge avslåing",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "Sistema",
"appearance_title": "Aparência",
"attach": "Anexar",
"atx_power_control_board_not_responding": "Sem resposta da placa de extensão ATX. Verifique o cabo RJ-12 e a alimentação da placa.",
"atx_power_control_get_state_error": "Falha ao obter estado de energia ATX: {error}",
"atx_power_control_hdd_led": "LED do HDD",
"atx_power_control_hold_hint": "Manter premido 3 s para forçar o desligamento",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "Системная",
"appearance_title": "Внешний вид",
"attach": "Подключить",
"atx_power_control_board_not_responding": "Нет ответа от платы расширения ATX. Проверьте кабель RJ-12 и питание платы.",
"atx_power_control_get_state_error": "Не удалось получить состояние ATX-питания: {error}",
"atx_power_control_hdd_led": "Индикатор HDD",
"atx_power_control_hold_hint": "Удерживайте 3 с для принудительного выключения",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "System",
"appearance_title": "Utseende",
"attach": "Bifoga",
"atx_power_control_board_not_responding": "Inget svar från ATX-expansionskortet. Kontrollera RJ-12-kabeln och kortets strömförsörjning.",
"atx_power_control_get_state_error": "Misslyckades med att hämta ATX-strömstatus: {error}",
"atx_power_control_hdd_led": "Hårddisk-LED",
"atx_power_control_hold_hint": "Håll in 3 s för att tvinga avstängning",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "系統",
"appearance_title": "外觀",
"attach": "放至下方",
"atx_power_control_board_not_responding": "ATX 擴充板沒有回應。請檢查 RJ-12 線材與擴充板的電源。",
"atx_power_control_get_state_error": "取得 ATX 電源狀態失敗:{error}",
"atx_power_control_hdd_led": "HDD 指示燈",
"atx_power_control_hold_hint": "按住 3 秒強制關機",
Expand Down
1 change: 1 addition & 0 deletions ui/localization/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"appearance_theme_system": "跟随系统",
"appearance_title": "外观",
"attach": "挂载",
"atx_power_control_board_not_responding": "ATX 扩展板无响应。请检查 RJ-12 线缆和扩展板的电源。",
"atx_power_control_get_state_error": "获取 ATX 电源状态失败:{error}",
"atx_power_control_hdd_led": "硬盘指示灯",
"atx_power_control_hold_hint": "按住 3 秒强制关机",
Expand Down
44 changes: 32 additions & 12 deletions ui/src/components/extensions/ATXPowerControl.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
import { LuHardDrive, LuPower, LuRotateCcw, LuTriangleAlert } from "react-icons/lu";

import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
Expand All @@ -10,10 +10,12 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
import notifications from "@/notifications";

const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
const STATE_POLL_INTERVAL = 5000; // Poll board responsiveness every 5 seconds

interface ATXState {
power: boolean;
hdd: boolean;
responsive?: boolean;
}

export function ATXPowerControl() {
Expand All @@ -29,17 +31,26 @@ export function ATXPowerControl() {
}
});

// Request initial state
// Request initial state and keep polling while the control is open,
// so an unresponsive extension board is detected and surfaced.
useEffect(() => {
send("getATXState", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(
m.atx_power_control_get_state_error({ error: resp.error.data || m.unknown_error() }),
);
return;
}
setAtxState(resp.result as ATXState);
});
const requestState = (notifyOnError: boolean) => {
send("getATXState", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
if (notifyOnError) {
notifications.error(
m.atx_power_control_get_state_error({ error: resp.error.data || m.unknown_error() }),
);
}
return;
}
setAtxState(resp.result as ATXState);
});
};

requestState(true);
const pollTimer = setInterval(() => requestState(false), STATE_POLL_INTERVAL);
return () => clearInterval(pollTimer);
}, [send]);

const handlePowerPress = (pressed: boolean) => {
Expand Down Expand Up @@ -115,6 +126,13 @@ export function ATXPowerControl() {
) : (
<Card className="min-h-[120px] animate-fadeIn opacity-0">
<div className="space-y-4 p-3">
{/* Board responsiveness warning */}
{atxState.responsive === false && (
<div className="flex items-start gap-x-2 text-xs text-amber-600 dark:text-amber-500">
<LuTriangleAlert className="mt-0.5 h-4 w-4 shrink-0" />
<span>{m.atx_power_control_board_not_responding()}</span>
</div>
)}
{/* Control Buttons */}
<div className="flex items-center space-x-2">
<Button
Expand Down Expand Up @@ -147,7 +165,9 @@ export function ATXPowerControl() {
}}
/>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400">{m.atx_power_control_hold_hint()}</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
{m.atx_power_control_hold_hint()}
</p>

<hr className="border-slate-700/30 dark:border-slate-600/30" />
{/* Status Indicators */}
Expand Down
Loading