Files
LocalAI/addons/pikvm/internal/tools/read.go
T
Ettore Di Giacinto bab8a605cc feat(addons): add the PiKVM add-on
Chats and agents can check a PiKVM (kvmd) device and, when a person
confirms each call, press the power buttons of the machine it controls
and use its keyboard.

Read tools: get-info (model, versions, no serial or host name),
get-power-state (ATX LEDs) and get-keyboard-state. Write tools:
power-action (on, off, reset; off_hard, reset_hard and long_press only
with a per-device switch), type-text (bounded, no control characters,
refuses key, token and password-list shapes) and send-key (a short
allowlist, no Windows or Meta chords). All three declare approval
always. The preview reads the power state; apply reads it again and
rejects a stale operation. The add-on journals before it sends, never
retries and reports unknown when the connection drops.

One connection is one device: the address is the single resource of the
connection and the one sealed credential holds user:password, split at
the first colon. A one-time code is not supported. The add-on speaks
only to that address, refuses redirects, uses no proxy and accepts a
self-signed certificate only through a #sha256= pin in the address.
Typing, the text length and hard power actions are per-device switches
that an administrator sets on the add-on page; all start off. Snapshots,
mass storage, GPIO and the mouse are left out.

The add-on ships a skill, a connection page with a Test button and
approval cards, the package files, the library entry and the docs, and
joins the package workflow and the case inventory.

PiKVM API reference: https://docs.pikvm.org/api/

Assisted-by: Claude:claude-sonnet-5-5 go node
2026-11-04 17:20:57 +00:00

98 lines
2.6 KiB
Go

// SPDX-License-Identifier: MIT
package tools
import "net/url"
// atxState is GET /api/atx.
type atxState struct {
Enabled bool `json:"enabled"`
Busy bool `json:"busy"`
Leds struct {
Power bool `json:"power"`
HDD bool `json:"hdd"`
} `json:"leds"`
}
func (s atxState) power() string {
if s.Leds.Power {
return "on"
}
return "off"
}
func (s atxState) view() map[string]any {
return map[string]any{"enabled": s.Enabled, "busy": s.Busy, "power": s.power(), "diskActivity": s.Leds.HDD}
}
func (c *call) atx() (atxState, error) {
var s atxState
return s, c.r.API.GetJSON(c.ctx, c.dev, c.cred, []string{"api", "atx"}, nil, &s)
}
// hidState is GET /api/hid.
type hidState struct {
Enabled bool `json:"enabled"`
Online bool `json:"online"`
Busy bool `json:"busy"`
Keyboard struct {
Online bool `json:"online"`
Leds struct {
Caps bool `json:"caps"`
Num bool `json:"num"`
Scroll bool `json:"scroll"`
} `json:"leds"`
} `json:"keyboard"`
}
func (s hidState) view() map[string]any {
return map[string]any{"enabled": s.Enabled, "online": s.Online, "busy": s.Busy, "keyboardOnline": s.Keyboard.Online,
"leds": map[string]bool{"caps": s.Keyboard.Leds.Caps, "num": s.Keyboard.Leds.Num, "scroll": s.Keyboard.Leds.Scroll}}
}
func (c *call) hid() (hidState, error) {
var s hidState
return s, c.r.API.GetJSON(c.ctx, c.dev, c.cred, []string{"api", "hid"}, nil, &s)
}
func str(m map[string]any, path ...string) string {
var cur any = m
for _, p := range path {
mm, ok := cur.(map[string]any)
if !ok {
return ""
}
cur = mm[p]
}
s, _ := cur.(string)
return short(s, 60)
}
// info returns what identifies the device: model, platform and versions. The
// serial number, the host name and the network are left out.
func (c *call) info() (map[string]any, error) {
var raw map[string]any
if err := c.r.API.GetJSON(c.ctx, c.dev, c.cred, []string{"api", "info"}, url.Values{"fields": {"hw,system"}}, &raw); err != nil {
return nil, err
}
out := map[string]any{
"model": str(raw, "hw", "platform", "model"),
"base": str(raw, "hw", "platform", "base"),
"type": str(raw, "hw", "platform", "type"),
"board": str(raw, "hw", "platform", "board"),
"video": str(raw, "hw", "platform", "video"),
"version": str(raw, "system", "kvmd", "version"),
"streamer": str(raw, "system", "streamer", "version"),
"kernel": str(raw, "system", "kernel", "release"),
}
if hw, ok := raw["hw"].(map[string]any); ok {
if h, ok := hw["health"].(map[string]any); ok {
if t, ok := h["temp"].(map[string]any); ok {
if cpu, ok := t["cpu"].(float64); ok {
out["cpuTempC"] = cpu
}
}
}
}
return out, nil
}