Chipi-API adds an optional REST/JSON layer on top of the Chipi home-automation framework. It lets you read and update item values via HTTP; every request is authorised with scope-based API keys.
Licence: Apache 2.0 (see root‐level
LICENSE).
- SBCL ≥ 2.x (or any other supported Common Lisp implementation)
- Quicklisp (or OCICL)
- ASDF systems
chipiandchipi-api
;; Quicklisp
(ql:quickload :chipi-api)
;; OCICL
(asdf:load-system :chipi-api)(hab:defconfig "chipi"
;; 1 – initialises runtime, actor system, timers …
;; 2 – Chipi-API specific environment
(api-env:init
:apikey-store (apikey-store:make-simple-file-backend) ; or *memory-backend* for testing
:apikey-lifetime (ltd:duration :day 100))
;; 3 – HTTP server on port 8765
(api:start))For a complete, runnable example have a look at example-web.lisp in the project root.
(defitem 'lamp "Living-room lamp" 'boolean :initial-value 'item:false)
(defitem 'temp "Temperature" 'float :initial-value 21.5)Items can be marked as read-only for external systems by setting the :ext-readonly tag:
(defitem 'sensor "Temperature sensor" 'float
:initial-value 20.0
:tags '((:ext-readonly . t)))Read-only items can still be read via the REST API but cannot be updated through external calls. The tag value should be the Lisp boolean t.
Note: This is a convention that should be respected by the UI and external systems. The server does not enforce read-only restrictions - it's up to client implementations to check for the :ext-readonly tag and prevent updates accordingly.
(defparameter *my-key*
(apikey-store:create-apikey :access-rights '(:read :update)))(api-env:init
:apikey-store (apikey-store:make-simple-file-backend))The file backend stores keys in runtime/apikeys.
A full OpenAPI 3.0 specification lives in chipi-api.yaml.
Besides individual items the API also exposes itemgroups, logical containers that hold multiple items.
| Endpoint | Method | Required scope |
|---|---|---|
/items |
GET | read |
/items/{itemName} |
GET | read |
/items/{itemName} |
POST | update |
/itemgroups |
GET | read |
/itemgroups/{groupName} |
GET | read |
/events/items |
GET | read |
Add header X-Api-Key: <your-key> to every call.
# List all items
curl -H "X-Api-Key: $MY_KEY" http://localhost:8765/items
# List all itemgroups
curl -H "X-Api-Key: $MY_KEY" http://localhost:8765/itemgroups
# Get single item
curl -H "X-Api-Key: $MY_KEY" http://localhost:8765/items/lamp
# Get single itemgroup
curl -H "X-Api-Key: $MY_KEY" http://localhost:8765/itemgroups/living
# Update item
curl -X POST -H "X-Api-Key: $MY_KEY" -H "Content-Type: application/json" \
-d '{"value": true}' http://localhost:8765/items/lampThe API provides real-time item updates via Server-Sent Events:
# Connect to item events stream
curl -H "Accept: text/event-stream" \
"http://localhost:8765/events/items?apikey=$MY_KEY"The SSE endpoint sends:
- Connection confirmation messages
- Real-time item change notifications with full item data
- Periodic heartbeat messages to keep the connection alive
Event format follows the SSE standard with data: fields containing JSON payloads.
Connection Event:
{"event":{"type":"connection","message":"Connected to item events"}}
Item Change Event:
{"event":{"type":"item-change","item":{"name":"lamp","label":"Living-room lamp","type-hint":"boolean","tags":{},"item-state":{"value":true,"timestamp":1703123456}}}}
Heartbeat Event:
{"event":{"type":"heartbeat","timestamp":1703123456}}
| Field | Type | Description |
|---|---|---|
event.type |
string | Always "item-change" |
event.item.name |
string | Item identifier |
event.item.label |
string | Human-readable item name |
event.item.type-hint |
string | Data type (boolean, float, integer, string) |
event.item.tags |
object | Item metadata (including :ext-readonly flag) |
event.item.item-state.value |
any | Current item value |
event.item.item-state.timestamp |
number | Common Lisp universal-time timestamp (seconds since 1900-01-01) |
Timestamps are provided as Common Lisp universal-time values (seconds since January 1st, 1900, 00:00:00 GMT). To convert to Unix timestamp (seconds since 1970-01-01), subtract 2208988800:
// Convert CL universal-time to Unix timestamp
const unixTimestamp = universalTime - 2208988800;
const date = new Date(unixTimestamp * 1000);# Convert in shell (example with timestamp 3913123456)
echo $((3913123456 - 2208988800)) # Result: 1704134656 (Unix timestamp)| Scope | Meaning |
|---|---|
read |
read-only |
update |
push new values |
delete |
reserved |
admin |
full access |
The highest requested scope must not exceed the highest scope granted to the API key.
(api:stop) ; stop only the web API
(hab:shutdown) ; stop the entire Chipi instanceChipi UI is a responsive UI, based on CLOG and Bootstrap 5 with realtime updates of values.
Example of setup:
(hab:defconfig "chipi"
;; 1 – initialises runtime, actor system, timers …
;; 2 – Chipi-API specific environment
(api-env:init
:apikey-store (apikey-store:make-simple-file-backend) ; or *memory-backend* for testing
:apikey-lifetime (ltd:duration :day 100))
;; 3 – HTTP server on port 8765
;;(api:start)
;; 4 - UI
;; API server is not needed for UI, but api-env is (eventually, because we want API key security)
;; ATM it's WIP and doesn't need that either
(ui:start :host "your-host" :port <your-port>)
)Items and itemgroups support special tags that control how they are rendered in the UI.
| Tag | Value | Description |
|---|---|---|
:ui-type |
string | Overrides the default type badge label (e.g. "Light" instead of "Switch") |
:ui-readonly |
t |
Boolean items render as plain "ON"/"OFF" text instead of an interactive toggle |
;; Custom type label
(defitem 'switch1 "Switch1" 'boolean :initial-value 'item:false
:tags '((:ui-type . "Light")))
;; Read-only boolean displayed as ON/OFF text
(defitem 'motion-sensor "Motion Sensor" 'boolean :initial-value 'item:true
:tags '((:ui-readonly . t)))| Tag | Value | Description |
|---|---|---|
:ui-link |
— | Renders the itemgroup as a clickable navigation link instead of an inline card |
;; Renders as a navigation link that opens a detail page
(defitemgroup 'lights "Lights" :tags '((:ui-link)))This is mostly WIP. Still under investigation if this is a practical or not.
