Post

Krill SDK for Kotlin Multiplatform

Develop Home Automation on any Platform with the Krill Kotlin Multiplatform SDK for Java and Kotlin.

Krill SDK for Kotlin Multiplatform

krill sdk

Krill Kotlin Multiplatform SDK

Introduction

The Krill SDK is now a stand-alone Kotlin Multiplatform library on Maven Central. Every type, interface, and helper your code needs to talk to a Krill server — discovering peers, reading nodes, deriving auth tokens, decoding beacons, planning agent actions — has been lifted out of the Krill app’s shared module and into one redistributable artifact.

If you’re building a Krill integration, an alternative client, or just want to read data out of a swarm without depending on the full Krill app, this is your starting point. The SDK targets JVM, Android, iOS (arm64 / sim arm64 / x64), and wasm-js, so the same code works on a Raspberry Pi, a phone, a desktop, or in a browser.

What’s in it

The SDK contains the durable, redistributable parts of Krill — value types, interfaces, and protocol primitives. Process-specific wiring (Koin DI, processor implementations, platform HTTP engines) stays in the app.

LayerWhat you get
Type discriminatorsKrillApp sealed class for every node type (Server, Pin, DataPoint, Trigger, Filter, Executor, Project, …); MenuCommand for editor commands.
Node modelNode data class, NodeBuilder fluent builder, NodeMetaData interface plus every concrete subtype (ServerMetaData, PinMetaData, DataPointMetaData, TaskListMetaData, …) and Node.name(), Node.details(), Node.https() extensions.
HTTPNodeHttp class for the full REST surface (/health, /nodes, /data/series, /camera/*, /backup/*, /project/*/diagram/*); TrustHost interface; BeaconCodec for multicast payloads.
AuthPinDerivation (PBKDF2/HMAC bearer + rolling beacon tokens); ClientPinStore interface for per-platform credential persistence.
DiscoveryPeerConstants (multicast group / port / TTL); BeaconSender, BeaconWireHandler, BeaconSupervisor, ServerConnector interfaces; NodeWire payload type.
EventsEvent envelope, EventPayload polymorphic interface, payloads for state-change / snapshot-update / pin-change / created.
LLM toolingChat, Message, ToolCall, Function for chat-completion plumbing; LLMResponse, LLMProposedAction, LLMNewNodeProposal for agent planning.
LifecycleAppLifecycle foreground/background flow.
HelpersDataPointRelevance (which filters/triggers fit a DataType), computeTaskListState(...), updateMetaWithError(...), Snapshot.doubleValue().

What stays in the Krill app (and you don’t need): the platform-specific Ktor engines, the Koin processor wiring, the H2 database layer, the krill-pi4j hardware bridge, and the Compose UI.

Installation

Java 21+, Gradle Kotlin DSL, version catalog:

1
2
3
4
5
6
# gradle/libs.versions.toml
[versions]
krill-sdk = "0.0.16"

[libraries]
krill-sdk = { module = "com.krillforge:krill-sdk", version.ref = "krill-sdk" }
1
2
3
4
5
6
7
8
// build.gradle.kts
repositories {
    mavenCentral()
}

dependencies {
    implementation(libs.krill.sdk)
}

Transitively the SDK pulls in kotlinx-coroutines-core, kotlinx-serialization-core/json, ktor-http, ktor-client-core, and kermit (logging). You bring your own Ktor engine on each platform (CIO on JVM, OkHttp on Android, Darwin on iOS, JS on wasm).

Quick examples

Build a node

1
2
3
4
5
6
7
8
9
10
import krill.zone.shared.KrillApp
import krill.zone.shared.krillapp.server.pin.PinMetaData
import krill.zone.shared.node.NodeBuilder

val fan = NodeBuilder()
    .type(KrillApp.Server.Pin)
    .host(serverInstallId)
    .parent(serverInstallId)
    .meta(PinMetaData(name = "fan", pinNumber = 17))
    .create()

Derive a bearer token from a PIN

1
2
3
4
5
6
7
8
import krill.zone.shared.security.PinDerivation

val token   = PinDerivation.deriveBearerToken(pin = "123456")
val beacon  = PinDerivation.deriveBeaconToken(
    pin = "123456",
    nodeUuid = serverUuid,
    epochSeconds = Clock.System.now().epochSeconds,
)

deriveBearerToken is stable per PIN; persist it locally via your platform’s ClientPinStore and send it as Authorization: Bearer <token> on every call. deriveBeaconToken rolls every 30 seconds and rides inside multicast beacons to prove cluster membership without leaking the PIN.

Talk to a server with NodeHttp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
import krill.zone.shared.node.Node
import krill.zone.shared.node.NodeHttp

val client = HttpClient {
    install(ContentNegotiation) { json(yourPolymorphicJson) }
}

val nodeHttp = NodeHttp(
    httpClient = client,
    trustHost = trustHost,                 // your TrustHost implementation
    bearerTokenProvider = { pinStore.bearerToken() },
)

val server: Node = ...
val children   = nodeHttp.readNodes(server)
val healthy    = nodeHttp.readHealth(server) != null

Walk the type hierarchy

1
2
3
4
5
6
7
8
9
10
11
12
import krill.zone.shared.allKrillApps
import krill.zone.shared.krillAppChildren
import krill.zone.shared.lookup

// Every node type, flattened.
allKrillApps.forEach(::println)

// Direct children of `Server`.
krillAppChildren[KrillApp.Server].orEmpty()

// Resolve a type by short name or fully-qualified discriminator.
val cron = lookup("Trigger.CronTimer")

Decode a beacon datagram

1
2
3
4
5
6
7
8
import krill.zone.shared.io.http.decodeBeacon
import krill.zone.shared.io.http.PeerConstants

val socket = openMulticast(PeerConstants.MULTICAST_GROUP_V4, PeerConstants.MULTICAST_PORT)
socket.receive { bytes ->
    val wire = decodeBeacon(json = yourJson, bytes = bytes) ?: return@receive
    println("peer ${wire.installId} at ${wire.url()} on ${wire.platform}")
}

Compute the worst state of a TaskList

1
2
3
4
5
6
7
8
9
10
import krill.zone.shared.krillapp.project.tasklist.computeTaskListState
import krill.zone.shared.krillapp.project.tasklist.Priority
import krill.zone.shared.krillapp.project.tasklist.Task

val state = computeTaskListState(
    tasks = listOf(Task(description = "rotate logs", dueDate = lastWeek)),
    priority = Priority.HIGH,
    nowMillis = Clock.System.now().toEpochMilliseconds(),
)
// → NodeState.SEVERE

The same function powers the chip-colour escalation in the Krill UI and the server-side scheduler — using it in your own tooling guarantees pixel-identical behaviour.

Status

The SDK shipped its first release in March and has had a long migration tail since: every value type, MetaData subclass, HTTP primitive, and protocol helper from the Krill shared module is now mirrored in the SDK at the same package paths. The Krill app itself consumes the SDK as a regular Maven dependency. We expect a 1.0 once one round of API feedback comes back from external integrators.

Issues, requests, and PRs welcome at the GitHub repo.

Last verified: 2026-04-26

This post is licensed under CC BY 4.0 by Sautner Studio, LLC.