diff --git a/app/src/main/java/ink/trmnl/android/network/TrmnlApiService.kt b/app/src/main/java/ink/trmnl/android/network/TrmnlApiService.kt index 3f285fc..a5f36c7 100644 --- a/app/src/main/java/ink/trmnl/android/network/TrmnlApiService.kt +++ b/app/src/main/java/ink/trmnl/android/network/TrmnlApiService.kt @@ -12,15 +12,20 @@ import retrofit2.http.Headers import retrofit2.http.Url /** - * API service interface for TRMNL or BYOS servers. + * API service interface for TRMNL device-level API endpoints. * - * This interface defines the endpoints for the TRMNL API. + * This interface defines endpoints that require device-level authentication via + * Access-Token header (Device API key), as opposed to user-level Bearer token authentication. + * + * For user-level (account) API endpoints, see [TrmnlUserApiService]. * * See: * - https://docs.usetrmnl.com/go * - https://docs.usetrmnl.com/go/private-api/introduction + * - https://trmnl.com/api-docs/index.html (OpenAPI documentation) * * @see TrmnlDisplayRepository + * @see TrmnlUserApiService */ interface TrmnlApiService { companion object { diff --git a/app/src/main/java/ink/trmnl/android/network/TrmnlUserApiService.kt b/app/src/main/java/ink/trmnl/android/network/TrmnlUserApiService.kt new file mode 100644 index 0000000..34afc94 --- /dev/null +++ b/app/src/main/java/ink/trmnl/android/network/TrmnlUserApiService.kt @@ -0,0 +1,78 @@ +package ink.trmnl.android.network + +import com.slack.eithernet.ApiResult +import ink.trmnl.android.network.model.TrmnlDeviceResponse +import ink.trmnl.android.network.model.TrmnlDeviceUpdateRequest +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Headers +import retrofit2.http.PATCH +import retrofit2.http.Url + +/** + * API service interface for TRMNL user-level (account) API endpoints. + * + * This interface defines endpoints that require user-level authentication via Bearer token + * (Account API key), as opposed to device-level authentication. + * + * See: + * - https://docs.usetrmnl.com/go + * - https://trmnl.com/api-docs/index.html (OpenAPI documentation) + */ +interface TrmnlUserApiService { + companion object { + /** + * Path template for the TRMNL API endpoint to get or update a specific device. + * + * Replace `{id}` with the actual device ID. + * + * **Authentication:** Requires Bearer token (user-level Account API key) + * + * See: https://trmnl.com/api-docs/index.html#/Devices + * + * @see getDevice + * @see updateDevice + */ + internal const val DEVICE_API_PATH = "api/devices/{id}" + } + + /** + * Retrieve device data for a specific device using [DEVICE_API_PATH]. + * + * This endpoint provides information about a single device including its configuration, + * battery status, WiFi strength, and sleep mode settings. + * + * **Authentication:** Requires Bearer token with user-level Account API key + * + * @param fullApiUrl The complete API URL to call (e.g., "https://usetrmnl.com/api/devices/1") + * @param accessToken The bearer authentication token (format: "Bearer your_api_key") + * @return An [ApiResult] containing [TrmnlDeviceResponse] with the device data + */ + @GET + suspend fun getDevice( + @Url fullApiUrl: String, + @Header("Authorization") accessToken: String, + ): ApiResult + + /** + * Update device settings for a specific device using [DEVICE_API_PATH]. + * + * This endpoint allows updating device configuration such as sleep mode settings + * and battery charge percentage. + * + * **Authentication:** Requires Bearer token with user-level Account API key + * + * @param fullApiUrl The complete API URL to call (e.g., "https://usetrmnl.com/api/devices/1") + * @param accessToken The bearer authentication token (format: "Bearer your_api_key") + * @param updateRequest The device update request containing the fields to update + * @return An [ApiResult] containing [TrmnlDeviceResponse] with the updated device data + */ + @Headers("Content-Type: application/json") + @PATCH + suspend fun updateDevice( + @Url fullApiUrl: String, + @Header("Authorization") accessToken: String, + @Body updateRequest: TrmnlDeviceUpdateRequest, + ): ApiResult +} diff --git a/app/src/main/java/ink/trmnl/android/network/model/TrmnlDeviceResponse.kt b/app/src/main/java/ink/trmnl/android/network/model/TrmnlDeviceResponse.kt new file mode 100644 index 0000000..e38cd99 --- /dev/null +++ b/app/src/main/java/ink/trmnl/android/network/model/TrmnlDeviceResponse.kt @@ -0,0 +1,65 @@ +package ink.trmnl.android.network.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Data class representing the response from the TRMNL device API. + * + * Sample JSON response: + * ```json + * { + * "data": { + * "id": 1, + * "name": "BYOD TRMNL", + * "friendly_id": "_____", + * "mac_address": "********", + * "battery_voltage": null, + * "rssi": null, + * "sleep_mode_enabled": false, + * "sleep_start_time": 1320, + * "sleep_end_time": 480, + * "percent_charged": 100, + * "wifi_strength": 100 + * } + * } + * ``` + * + * @property data The device data object. + * @see ink.trmnl.android.network.TrmnlApiService.getDevice + * @see ink.trmnl.android.network.TrmnlApiService.updateDevice + */ +@JsonClass(generateAdapter = true) +data class TrmnlDeviceResponse( + @Json(name = "data") val data: TrmnlDevice, +) + +/** + * Data class representing a TRMNL device. + * + * @property id The unique identifier for the device (e.g., 123). + * @property name The name of the device (e.g., "My TRMNL"). + * @property friendlyId A user-friendly identifier for the device (e.g., "ABC-123"). + * @property macAddress The MAC address of the device (e.g., "12:34:56:78:9A:BC"). + * @property batteryVoltage The battery voltage of the device in volts (e.g., 3.7), nullable. + * @property rssi The received signal strength indicator in dBm (e.g., -70), nullable. + * @property sleepModeEnabled Whether sleep mode is enabled (e.g., false), nullable in API response. + * @property sleepStartTime The time when sleep mode starts in minutes from midnight (e.g., 1320 = 10:00 PM), nullable. + * @property sleepEndTime The time when sleep mode ends in minutes from midnight (e.g., 480 = 8:00 AM), nullable. + * @property percentCharged The battery percentage charged (e.g., 85.0). Valid range: 0.0 to 100.0. + * @property wifiStrength The WiFi signal strength percentage (e.g., 75.0). Valid range: 0.0 to 100.0. + */ +@JsonClass(generateAdapter = true) +data class TrmnlDevice( + @Json(name = "id") val id: Int, + @Json(name = "name") val name: String, + @Json(name = "friendly_id") val friendlyId: String, + @Json(name = "mac_address") val macAddress: String, + @Json(name = "battery_voltage") val batteryVoltage: Double?, + @Json(name = "rssi") val rssi: Int?, + @Json(name = "sleep_mode_enabled") val sleepModeEnabled: Boolean?, + @Json(name = "sleep_start_time") val sleepStartTime: Int?, + @Json(name = "sleep_end_time") val sleepEndTime: Int?, + @Json(name = "percent_charged") val percentCharged: Double, + @Json(name = "wifi_strength") val wifiStrength: Double, +) diff --git a/app/src/main/java/ink/trmnl/android/network/model/TrmnlDeviceUpdateRequest.kt b/app/src/main/java/ink/trmnl/android/network/model/TrmnlDeviceUpdateRequest.kt new file mode 100644 index 0000000..f9d5971 --- /dev/null +++ b/app/src/main/java/ink/trmnl/android/network/model/TrmnlDeviceUpdateRequest.kt @@ -0,0 +1,33 @@ +package ink.trmnl.android.network.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Data class representing a request to update a TRMNL device. + * + * All fields are optional - only include the fields you want to update. + * + * Sample JSON request: + * ```json + * { + * "sleep_mode_enabled": true, + * "sleep_start_time": 1320, + * "sleep_end_time": 480, + * "percent_charged": 69.0 + * } + * ``` + * + * @property sleepModeEnabled Whether sleep mode is enabled. + * @property sleepStartTime The time when sleep mode starts (minutes from midnight). + * @property sleepEndTime The time when sleep mode ends (minutes from midnight). + * @property percentCharged The battery percentage charged. + * @see ink.trmnl.android.network.TrmnlApiService.updateDevice + */ +@JsonClass(generateAdapter = true) +data class TrmnlDeviceUpdateRequest( + @Json(name = "sleep_mode_enabled") val sleepModeEnabled: Boolean? = null, + @Json(name = "sleep_start_time") val sleepStartTime: Int? = null, + @Json(name = "sleep_end_time") val sleepEndTime: Int? = null, + @Json(name = "percent_charged") val percentCharged: Double? = null, +) diff --git a/app/src/test/java/ink/trmnl/android/network/model/TrmnlDeviceResponseTest.kt b/app/src/test/java/ink/trmnl/android/network/model/TrmnlDeviceResponseTest.kt new file mode 100644 index 0000000..ad1eb5b --- /dev/null +++ b/app/src/test/java/ink/trmnl/android/network/model/TrmnlDeviceResponseTest.kt @@ -0,0 +1,241 @@ +package ink.trmnl.android.network.model + +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.Moshi +import org.junit.Test + +/** + * Unit tests for [TrmnlDeviceResponse] and [TrmnlDevice]. + * + * These tests verify JSON parsing of the /api/devices endpoint responses. + */ +class TrmnlDeviceResponseTest { + private val moshi = + Moshi + .Builder() + .build() + + private val deviceResponseAdapter = moshi.adapter(TrmnlDeviceResponse::class.java) + + @Test + fun `parse device response from JSON successfully`() { + // Arrange - Load the test JSON from resources + val json = + javaClass.classLoader + ?.getResourceAsStream("device_response.json") + ?.bufferedReader() + ?.use { it.readText() } + + // Assert that we successfully loaded the JSON + assertThat(json).isNotNull() + + // Act + val response = deviceResponseAdapter.fromJson(json!!) + + // Assert + assertThat(response).isNotNull() + assertThat(response?.data).isNotNull() + } + + @Test + fun `parse device with all fields correctly`() { + // Arrange + val json = + javaClass.classLoader + ?.getResourceAsStream("device_response.json") + ?.bufferedReader() + ?.use { it.readText() } + + // Act + val response = deviceResponseAdapter.fromJson(json!!) + val device = response?.data + + // Assert - Verify all fields + assertThat(device).isNotNull() + assertThat(device?.id).isEqualTo(123) + assertThat(device?.name).isEqualTo("My TRMNL") + assertThat(device?.friendlyId).isEqualTo("ABC-123") + assertThat(device?.macAddress).isEqualTo("12:34:56:78:9A:BC") + assertThat(device?.batteryVoltage).isEqualTo(3.7) + assertThat(device?.rssi).isEqualTo(-70) + assertThat(device?.sleepModeEnabled).isEqualTo(false) + assertThat(device?.sleepStartTime).isEqualTo(1320) + assertThat(device?.sleepEndTime).isEqualTo(480) + assertThat(device?.percentCharged).isEqualTo(85.0) + assertThat(device?.wifiStrength).isEqualTo(75.0) + } + + @Test + fun `parse device with nullable fields as null`() { + // Arrange + val json = + javaClass.classLoader + ?.getResourceAsStream("device_response_nullable_fields.json") + ?.bufferedReader() + ?.use { it.readText() } + + // Act + val response = deviceResponseAdapter.fromJson(json!!) + val device = response?.data + + // Assert - Verify nullable fields are null + assertThat(device).isNotNull() + assertThat(device?.id).isEqualTo(456) + assertThat(device?.name).isEqualTo("Test Device") + assertThat(device?.batteryVoltage).isNull() + assertThat(device?.rssi).isNull() + assertThat(device?.sleepModeEnabled).isNull() + assertThat(device?.sleepStartTime).isNull() + assertThat(device?.sleepEndTime).isNull() + assertThat(device?.percentCharged).isEqualTo(100.0) + assertThat(device?.wifiStrength).isEqualTo(100.0) + } + + @Test + fun `parse device with edge case values`() { + // Arrange + val json = + javaClass.classLoader + ?.getResourceAsStream("device_response_edge_cases.json") + ?.bufferedReader() + ?.use { it.readText() } + + // Act + val response = deviceResponseAdapter.fromJson(json!!) + val device = response?.data + + // Assert - Verify edge case values + assertThat(device).isNotNull() + assertThat(device?.id).isEqualTo(789) + assertThat(device?.sleepModeEnabled).isEqualTo(true) + assertThat(device?.sleepStartTime).isEqualTo(0) // Midnight + assertThat(device?.sleepEndTime).isEqualTo(1439) // 11:59 PM + assertThat(device?.percentCharged).isEqualTo(0.0) // Minimum + assertThat(device?.wifiStrength).isEqualTo(0.0) // Minimum + } + + @Test + fun `parse sleep time values correctly`() { + // Arrange + val json = + javaClass.classLoader + ?.getResourceAsStream("device_response.json") + ?.bufferedReader() + ?.use { it.readText() } + + // Act + val response = deviceResponseAdapter.fromJson(json!!) + val device = response?.data + + // Assert - Verify time calculations + assertThat(device).isNotNull() + // 1320 minutes from midnight = 22:00 (10:00 PM) + assertThat(device?.sleepStartTime).isEqualTo(1320) + // 480 minutes from midnight = 08:00 (8:00 AM) + assertThat(device?.sleepEndTime).isEqualTo(480) + } + + @Test + fun `parse battery and wifi percentages within valid range`() { + // Arrange + val json = + javaClass.classLoader + ?.getResourceAsStream("device_response.json") + ?.bufferedReader() + ?.use { it.readText() } + + // Act + val response = deviceResponseAdapter.fromJson(json!!) + val device = response?.data + + // Assert - Verify percentages are in valid range (0-100) + assertThat(device).isNotNull() + assertThat(device?.percentCharged).isAtLeast(0.0) + assertThat(device?.percentCharged).isAtMost(100.0) + assertThat(device?.wifiStrength).isAtLeast(0.0) + assertThat(device?.wifiStrength).isAtMost(100.0) + } + + @Test + fun `parse MAC address format correctly`() { + // Arrange + val json = + javaClass.classLoader + ?.getResourceAsStream("device_response.json") + ?.bufferedReader() + ?.use { it.readText() } + + // Act + val response = deviceResponseAdapter.fromJson(json!!) + val device = response?.data + + // Assert - Verify MAC address format + assertThat(device).isNotNull() + assertThat(device?.macAddress).matches("[0-9A-Fa-f:]{17}") + assertThat(device?.macAddress).contains(":") + } + + @Test + fun `parse battery voltage as double`() { + // Arrange + val json = + javaClass.classLoader + ?.getResourceAsStream("device_response.json") + ?.bufferedReader() + ?.use { it.readText() } + + // Act + val response = deviceResponseAdapter.fromJson(json!!) + val device = response?.data + + // Assert - Verify battery voltage is a Double type + assertThat(device).isNotNull() + assertThat(device?.batteryVoltage).isInstanceOf(Double::class.java) + assertThat(device?.batteryVoltage).isGreaterThan(0.0) + } + + @Test + fun `parse RSSI as negative integer`() { + // Arrange + val json = + javaClass.classLoader + ?.getResourceAsStream("device_response.json") + ?.bufferedReader() + ?.use { it.readText() } + + // Act + val response = deviceResponseAdapter.fromJson(json!!) + val device = response?.data + + // Assert - Verify RSSI is typically negative (dBm) + assertThat(device).isNotNull() + assertThat(device?.rssi).isInstanceOf(Int::class.java) + assertThat(device?.rssi).isLessThan(0) + } + + @Test + fun `verify all JSON field mappings`() { + // Arrange + val json = + javaClass.classLoader + ?.getResourceAsStream("device_response.json") + ?.bufferedReader() + ?.use { it.readText() } + + // Act + val response = deviceResponseAdapter.fromJson(json!!) + val device = response?.data + + // Assert - Verify snake_case to camelCase mapping + assertThat(device).isNotNull() + // Verify that JSON snake_case fields are mapped correctly + assertThat(device?.friendlyId).isNotNull() // friendly_id -> friendlyId + assertThat(device?.macAddress).isNotNull() // mac_address -> macAddress + assertThat(device?.batteryVoltage).isNotNull() // battery_voltage -> batteryVoltage + assertThat(device?.sleepModeEnabled).isNotNull() // sleep_mode_enabled -> sleepModeEnabled + assertThat(device?.sleepStartTime).isNotNull() // sleep_start_time -> sleepStartTime + assertThat(device?.sleepEndTime).isNotNull() // sleep_end_time -> sleepEndTime + assertThat(device?.percentCharged).isNotNull() // percent_charged -> percentCharged + assertThat(device?.wifiStrength).isNotNull() // wifi_strength -> wifiStrength + } +} diff --git a/app/src/test/resources/device_response.json b/app/src/test/resources/device_response.json new file mode 100644 index 0000000..6cd0194 --- /dev/null +++ b/app/src/test/resources/device_response.json @@ -0,0 +1,15 @@ +{ + "data": { + "id": 123, + "name": "My TRMNL", + "friendly_id": "ABC-123", + "mac_address": "12:34:56:78:9A:BC", + "battery_voltage": 3.7, + "rssi": -70, + "sleep_mode_enabled": false, + "sleep_start_time": 1320, + "sleep_end_time": 480, + "percent_charged": 85.0, + "wifi_strength": 75.0 + } +} diff --git a/app/src/test/resources/device_response_edge_cases.json b/app/src/test/resources/device_response_edge_cases.json new file mode 100644 index 0000000..c79e554 --- /dev/null +++ b/app/src/test/resources/device_response_edge_cases.json @@ -0,0 +1,15 @@ +{ + "data": { + "id": 789, + "name": "Edge Case Device", + "friendly_id": "EDGE-789", + "mac_address": "00:00:00:00:00:00", + "battery_voltage": 4.2, + "rssi": -100, + "sleep_mode_enabled": true, + "sleep_start_time": 0, + "sleep_end_time": 1439, + "percent_charged": 0.0, + "wifi_strength": 0.0 + } +} diff --git a/app/src/test/resources/device_response_nullable_fields.json b/app/src/test/resources/device_response_nullable_fields.json new file mode 100644 index 0000000..c1f56ec --- /dev/null +++ b/app/src/test/resources/device_response_nullable_fields.json @@ -0,0 +1,15 @@ +{ + "data": { + "id": 456, + "name": "Test Device", + "friendly_id": "TEST-456", + "mac_address": "AA:BB:CC:DD:EE:FF", + "battery_voltage": null, + "rssi": null, + "sleep_mode_enabled": null, + "sleep_start_time": null, + "sleep_end_time": null, + "percent_charged": 100.0, + "wifi_strength": 100.0 + } +} diff --git a/project-resources/trmnl-api/README.md b/project-resources/trmnl-api/README.md new file mode 100644 index 0000000..da76ae9 --- /dev/null +++ b/project-resources/trmnl-api/README.md @@ -0,0 +1,25 @@ +# TRMNL API Specification + +This directory contains the OpenAPI specification for the TRMNL API. + +## Resources + +- **OpenAPI Portal:** https://trmnl.com/api-docs/index.html +- **API Documentation:** https://docs.usetrmnl.com/go +- **Private API Docs:** https://docs.usetrmnl.com/go/private-api/introduction + +## Files + +- `trmnl-openapi.yaml` - OpenAPI 3.0.1 specification exported from the TRMNL API portal + +## Authentication + +The TRMNL API uses two authentication methods: + +1. **Device-level authentication** - Uses `Access-Token` header with device API key + - Implemented in `TrmnlApiService.kt` + - Used for device operations (display, setup, etc.) + +2. **User-level authentication** - Uses `Authorization: Bearer` header with account API key + - Implemented in `TrmnlUserApiService.kt` + - Used for account operations (device management, plugin settings, etc.) diff --git a/project-resources/trmnl-api/trmnl-openapi.yaml b/project-resources/trmnl-api/trmnl-openapi.yaml new file mode 100644 index 0000000..df7436d --- /dev/null +++ b/project-resources/trmnl-api/trmnl-openapi.yaml @@ -0,0 +1,1137 @@ +--- +openapi: 3.0.1 +info: + title: TRMNL API + version: '1' +paths: + "/api/display": + get: + summary: Fetch the next screen + tags: + - Device API + parameters: + - name: Access-Token + in: header + required: true + description: Device API Key (eg. abc-123) + schema: + type: string + - name: Battery-Voltage + in: header + required: false + description: Device battery voltage (eg. 3.7) + schema: + type: number + - name: FW-Version + in: header + required: false + description: Device firmware version (eg. 0.0.1) + schema: + type: string + - name: RSSI + in: header + required: false + description: Device RSSI (eg. -69) + schema: + type: number + - name: Height + in: header + required: false + description: Device screen height (eg. 480) + schema: + type: string + - name: Width + in: header + required: false + description: Device screen width (eg. 800) + schema: + type: string + - name: Special-Function + in: header + required: false + description: Device special function (eg. true) + schema: + type: boolean + - name: BASE64 + in: header + required: false + description: Encode image function (eg. true) + schema: + type: boolean + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + status: + type: integer + example: 200 + image_url: + type: string + nullable: true + example: https://trmnl.com/images/setup/setup-logo.bmp + filename: + type: string + nullable: true + example: setup-logo.bmp + refresh_rate: + type: integer + example: 300 + reset_firmware: + type: boolean + example: false + update_firmware: + type: boolean + example: false + firmware_url: + type: string + nullable: true + example: https://trmnl.com/firmware/1.0.0.bin + special_function: + type: string + nullable: false + example: identify + action: + type: string + nullable: true + example: identify + "/api/display/current": + get: + summary: Fetch the current screen + tags: + - Device API + parameters: + - name: Access-Token + in: header + required: true + description: Device API Key (eg. abc-123) + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + status: + type: integer + example: 200 + refresh_rate: + type: integer + example: 300 + image_url: + type: string + nullable: true + example: https://trmnl.com/images/setup/setup-logo.bmp + filename: + type: string + nullable: true + example: setup-logo.bmp + rendered_at: + type: string + nullable: true + example: '2023-01-01T00:00:00Z' + "/api/log": + post: + summary: Log with logs[] (array) + tags: + - Device API + parameters: + - name: Access-Token + in: header + required: true + description: Device API Key (eg. abc-123) + schema: + type: string + responses: + '204': + description: Logs created when ignore_log_messages is not set + requestBody: + content: + application/json: + schema: + type: object + properties: + logs: [] + required: + - logs + additionalProperties: false + required: true + description: 'An array of log entries. Each entry can be any JSON type: string, + object, etc.' + "/api/setup": + get: + summary: Set up device + tags: + - Device API + parameters: + - name: ID + in: header + required: true + description: Device MAC Address (eg. 41:B4:10:39:A1:24) + schema: + type: string + - name: Model + in: header + required: true + description: DEVICE_MODEL from firmware definitions + schema: + type: string + description: | + Please note that the returned `status` JSON value may NOT always equal the HTTP status code. Notably, if a device MAC address is not found, + then the HTTP status code will be 200 but the `status` code in the response will be 404. + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + status: + type: integer + example: 200 + api_key: + type: string + nullable: true + example: abc-123 + friendly_id: + type: string + nullable: true + example: ABC-123 + image_url: + type: string + nullable: true + example: https://trmnl.com/images/setup/setup-logo.bmp + message: + type: string + example: Register at trmnl.com/signup with Device ID 'ABC-123' + "/api/categories": + get: + summary: List all plugin categories + tags: + - Categories + description: Returns a list of approved plugin categories. + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + status: + type: integer + example: 200 + data: + type: array + items: + type: string + example: + - life + - marketing + - ecommerce + "/api/ips": + get: + summary: List all TRMNL server IP addresses + tags: + - Server IPs + description: | + Returns a list of public IP addresses for all TRMNL core servers. + + Plugin poll requests will only originate from these IPs. + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + type: object + properties: + ipv4: + type: array + items: + type: string + ipv6: + type: array + items: + type: string + "/api/markup": + post: + summary: Render Liquid template + tags: + - Markup + parameters: [] + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + oneOf: + - type: string + - type: array + items: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + markup: + oneOf: + - type: string + example: Hello, {{ name }}! + - type: array + items: + type: string + example: + - Hello, {{ name }}! + - Goodbye, {{ name }}! + variables: + type: object + example: + name: World + required: + - markup + - variables + additionalProperties: false + required: true + description: The Liquid markup(s) to render and an optional set of variables + to use in the rendering process. + "/api/models": + get: + summary: List all device models + tags: + - Models + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + "$ref": "#/components/schemas/Model" + "/api/palettes": + get: + summary: List all palettes + tags: + - Palettes + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + "$ref": "#/components/schemas/Palette" + "/api/devices": + get: + summary: List my devices + tags: + - Devices + security: + - bearer_auth: [] + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + "$ref": "#/components/schemas/Device" + "/api/devices/{id}": + parameters: + - name: id + in: path + description: Device ID + required: true + schema: + type: integer + get: + summary: Get the data of a device + tags: + - Devices + security: + - bearer_auth: [] + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + "$ref": "#/components/schemas/Device" + '404': + description: Not found + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + patch: + summary: Update a device + tags: + - Devices + parameters: [] + security: + - bearer_auth: [] + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Updated + requestBody: + content: + application/json: + schema: + type: object + properties: + sleep_mode_enabled: + type: boolean + example: true + sleep_start_time: + type: integer + example: 1320 + sleep_end_time: + type: integer + example: 480 + percent_charged: + type: number + example: 69.0 + "/api/me": + get: + summary: Get my user data + tags: + - Users + security: + - bearer_auth: [] + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + "$ref": "#/components/schemas/User" + "/api/playlists/items": + get: + summary: List my playlist items + tags: + - Playlists + security: + - bearer_auth: [] + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + "$ref": "#/components/schemas/PlaylistItem" + "/api/playlists/items/{id}": + patch: + summary: Update a playlist item + tags: + - Playlists + parameters: + - name: id + in: path + description: ID of the playlist item + required: true + schema: + type: integer + security: + - bearer_auth: [] + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Updated + requestBody: + content: + application/json: + schema: + type: object + properties: + visible: + type: boolean + required: + - visible + "/api/plugin_settings/{id}/archive": + parameters: + - name: id + in: path + description: Plugin setting ID + required: true + schema: + type: integer + get: + summary: Download a plugin setting archive + tags: + - Plugin Settings + description: | + This endpoint is available for unauthenticated requests. + + When unauthenticated, any published recipe may be archived. + + When authenticated, the requesting user's private plugins are also archivable. + security: + - bearer_auth: [] + responses: + '200': + description: Success + '404': + description: Not Found + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '422': + description: Unprocessable Entity + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + post: + summary: Upload a plugin setting archive + tags: + - Plugin Settings + security: + - bearer_auth: [] + parameters: [] + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '422': + description: Unprocessable Entity + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + "$ref": "#/components/schemas/PluginSettingArchive" + requestBody: + content: + multipart/form-data: + schema: + type: file + required: true + description: Plugin setting archive file + "/api/plugin_settings/{id}/data": + parameters: + - name: id + in: path + description: Plugin setting ID or UUID + required: true + schema: + type: string + get: + summary: Get the data of a plugin setting + tags: + - Plugin Settings + security: + - bearer_auth: [] + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + type: object + '404': + description: Not found + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '422': + description: Data is not available + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + post: + summary: Update data for a plugin setting + tags: + - Plugin Settings + security: + - bearer_auth: [] + parameters: [] + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Success with UUID (no auth required) + content: + application/json: + schema: + type: object + properties: + data: + type: object + '404': + description: Not found + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '422': + description: Data cannot be modified + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + requestBody: + content: + application/json: + schema: + type: object + properties: + merge_variables: {} + required: + - merge_variables + additionalProperties: false + required: true + description: The value of `merge_variables` must be a JSON object + "/api/plugin_settings/{id}/image": + parameters: + - name: id + in: path + description: Plugin setting UUID + required: true + schema: + type: string + post: + summary: Upload an image for a webhook_image plugin + tags: + - Plugin Settings + responses: + '200': + description: Success with home_assistant_screenshot plugin + content: + application/json: + schema: + type: object + properties: + data: + type: object + properties: + message: + type: string + '404': + description: Not found + '422': + description: Image too large + '429': + description: Rate limited + "/api/plugin_settings": + get: + summary: List my plugin settings + tags: + - Plugin Settings + security: + - bearer_auth: [] + parameters: + - name: plugin_id + in: query + required: false + description: ID of a plugin or "calendars" to filter calendar plugins + schema: + type: string + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Returns all calendar plugin settings except Google Calendar + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + "$ref": "#/components/schemas/PluginSetting" + post: + summary: Create a new plugin setting + tags: + - Plugin Settings + security: + - bearer_auth: [] + parameters: [] + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + "$ref": "#/components/schemas/PluginSetting" + '422': + description: Unprocessable Entity + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + requestBody: + content: + application/json: + schema: + "$ref": "#/components/schemas/PluginSettingParams" + required: true + "/api/plugin_settings/{id}": + delete: + summary: Delete a plugin setting + tags: + - Plugin Settings + security: + - bearer_auth: [] + parameters: + - name: id + in: path + required: true + description: ID of the plugin setting to delete + schema: + type: integer + responses: + '401': + description: Unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + '204': + description: Deleted + '404': + description: Not found + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" +servers: +- url: https://{defaultHost} + variables: + defaultHost: + default: trmnl.com +components: + securitySchemes: + bearer_auth: + type: http + scheme: bearer + bearerFormat: Account API key + schemas: + Error: + type: object + additionalProperties: false + properties: + error: + type: string + example: An error occurred + Device: + type: object + additionalProperties: false + properties: + id: + type: integer + example: 123 + name: + type: string + example: My TRMNL + friendly_id: + type: string + example: ABC-123 + mac_address: + type: string + example: 12:34:56:78:9A:BC + battery_voltage: + type: number + nullable: true + example: 3.7 + rssi: + type: integer + nullable: true + example: -70 + sleep_mode_enabled: + type: boolean + nullable: false + example: false + sleep_start_time: + type: integer + example: 1320 + sleep_end_time: + type: integer + example: 480 + percent_charged: + type: number + example: 85.0 + minimum: 0 + maximum: 100 + wifi_strength: + type: number + example: 75.0 + minimum: 0 + maximum: 100 + Model: + type: object + additionalProperties: false + properties: + name: + type: string + example: trmnl_original + description: Unique identifier + label: + type: string + example: TRMNL + description: Human-readable name + description: + type: string + example: Original TRMNL model + description: Description + width: + type: integer + example: 800 + description: Screen width in pixels + height: + type: integer + example: 480 + description: Screen height in pixels + colors: + type: integer + example: 2 + description: Number of colors supported + bit_depth: + type: integer + example: 1 + description: Color bit depth + scale_factor: + type: number + example: 1.0 + description: Display scale factor + rotation: + type: integer + example: 90 + description: Screen rotation in degrees + mime_type: + type: string + example: image/png + description: Image MIME type + offset_x: + type: integer + example: 10 + description: X offset for image rendering + offset_y: + type: integer + example: 20 + description: Y offset for image rendering + kind: + type: string + example: trmnl + description: Device kind (e.g., trmnl, kindle, byod) + enum: + - trmnl + - kindle + - byod + - tidbyt + palette_ids: + type: array + items: + type: string + example: + - bw + - gray-4 + - gray-16 + description: Supported color palette IDs + image_size_limit: + type: integer + example: 92160 + description: Maximum image file size in bytes for webhook uploads + image_upload_supported: + type: boolean + example: true + description: Whether webhook image uploads are supported for this device + type + css: + type: object + nullable: true + description: CSS classes and variables for web rendering + properties: + classes: + type: object + properties: + device: + type: string + example: screen--og_plus + size: + type: string + example: screen--md + variables: + type: array + items: + type: array + items: + type: string + example: + - "--screen-w" + - 800px + Palette: + type: object + additionalProperties: false + properties: + id: + type: string + example: gray-16 + description: Unique identifier + required: true + name: + type: string + example: 16-Gray + description: Human-readable name + required: true + grays: + type: integer + nullable: true + example: 16 + description: Number of grayscale levels (null for color palettes) + colors: + type: array + nullable: true + items: + type: string + example: + - "#FF0000" + - "#00FF00" + - "#0000FF" + - "#FFFF00" + - "#000000" + - "#FFFFFF" + description: Array of hex color codes (null for grayscale palettes) + framework_class: + type: string + example: screen--4bit + description: Framework CSS class for this palette + PlaylistItem: + type: object + additionalProperties: false + properties: + created_at: + type: string + format: date_time + example: '2023-10-01T12:00:00Z' + device_id: + type: integer + example: 1 + id: + type: integer + example: 1 + mashup_id: + type: integer + example: 1 + nullable: true + mirror: + type: boolean + example: true + playlist_group_id: + type: integer + example: 1 + plugin_setting: + "$ref": "#/components/schemas/PluginSetting" + plugin_setting_id: + type: integer + example: 1 + rendered_at: + type: string + format: date_time + example: '2023-10-01T12:00:00Z' + row_order: + type: integer + example: 1 + updated_at: + type: string + format: date_time + example: '2023-10-01T12:00:00Z' + visible: + type: boolean + example: true + PlaylistItemParams: + type: object + additionalProperties: false + properties: + visible: + type: boolean + example: true + PluginSetting: + type: object + additionalProperties: false + properties: + id: + type: integer + example: 1 + name: + type: string + example: My Plugin Setting + plugin_id: + type: integer + example: 1 + PluginSettingArchive: + type: object + properties: + settings_yaml: + type: string + description: YAML settings file + PluginSettingParams: + type: object + additionalProperties: false + properties: + name: + type: string + example: My Plugin Setting + required: true + plugin_id: + type: integer + example: 1 + required: true + PluginSettingDataParams: + type: object + additionalProperties: false + properties: + merge_variables: + type: content + required: true + example: true + error: + type: string + required: false + User: + type: object + additionalProperties: false + properties: + id: + type: integer + example: 42 + name: + type: string + example: Jim Bob + email: + type: string + example: jimbob@gmail.net + first_name: + type: string + example: Jim + last_name: + type: string + example: Bob + locale: + type: string + example: en + time_zone: + type: string + example: Eastern Time (US & Canada) + time_zone_iana: + type: string + example: America/New_York + utc_offset: + type: integer + example: -14400 + api_key: + type: string + example: user_xxxxxx