Merge pull request #84 from usetrmnl/76-byos_hanami-compatibility-part-2

[ADDED] Support for BYOS device setup and display image compatibility
This commit is contained in:
Hossain Khan
2025-06-17 17:16:56 -04:00
committed by GitHub
11 changed files with 337 additions and 40 deletions
@@ -0,0 +1,15 @@
package ink.trmnl.android.data
/**
* Represents the setup information for a TRMNL device.
*
* This data class is used to encapsulate the result of setting up a new device,
* including whether the setup was successful, the device's MAC ID, API key,
* and any relevant messages.
*/
data class DeviceSetupInfo(
val success: Boolean,
val deviceMacId: String,
val apiKey: String,
val message: String,
)
@@ -3,6 +3,7 @@ package ink.trmnl.android.data
import androidx.annotation.Keep
import ink.trmnl.android.data.AppConfig.DEFAULT_REFRESH_INTERVAL_SEC
import ink.trmnl.android.model.TrmnlDeviceType
import ink.trmnl.android.util.ERROR_TYPE_DEVICE_SETUP_REQUIRED
import ink.trmnl.android.util.HTTP_200
import ink.trmnl.android.util.HTTP_500
import ink.trmnl.android.util.HTTP_OK
@@ -19,7 +20,14 @@ data class TrmnlDisplayInfo constructor(
val status: Int,
val trmnlDeviceType: TrmnlDeviceType,
val imageUrl: String,
val imageName: String,
/**
* The file name of the image to be displayed.
*
* If this is an error type, it indicates a specific error condition.
* For example:
* - [ERROR_TYPE_DEVICE_SETUP_REQUIRED]
*/
val imageFileName: String,
val error: String? = null,
val refreshIntervalSeconds: Long? = DEFAULT_REFRESH_INTERVAL_SEC,
/**
@@ -2,6 +2,7 @@ package ink.trmnl.android.data
import com.slack.eithernet.ApiResult
import com.slack.eithernet.InternalEitherNetApi
import com.slack.eithernet.exceptionOrNull
import com.squareup.anvil.annotations.optional.SingleIn
import ink.trmnl.android.BuildConfig.USE_FAKE_API
import ink.trmnl.android.di.AppScope
@@ -10,6 +11,8 @@ import ink.trmnl.android.model.TrmnlDeviceType
import ink.trmnl.android.network.TrmnlApiService
import ink.trmnl.android.network.TrmnlApiService.Companion.CURRENT_PLAYLIST_SCREEN_API_PATH
import ink.trmnl.android.network.TrmnlApiService.Companion.NEXT_PLAYLIST_SCREEN_API_PATH
import ink.trmnl.android.network.model.TrmnlDisplayResponse
import ink.trmnl.android.util.ERROR_TYPE_DEVICE_SETUP_REQUIRED
import ink.trmnl.android.util.HTTP_200
import ink.trmnl.android.util.HTTP_500
import ink.trmnl.android.util.isHttpOk
@@ -57,7 +60,7 @@ class TrmnlDisplayRepository
deviceMacId = trmnlDeviceConfig.deviceMacId,
// TEMP FIX: Use Base64 encoding to avoid relative path issue
// See https://github.com/usetrmnl/trmnl-android/issues/76#issuecomment-2980018109
useBase64 = trmnlDeviceConfig.type == TrmnlDeviceType.BYOS,
// useBase64 = trmnlDeviceConfig.type == TrmnlDeviceType.BYOS, // Disabled for now
)
when (result) {
@@ -66,13 +69,18 @@ class TrmnlDisplayRepository
}
is ApiResult.Success -> {
// Map the response to the display info
val response = result.value
val response: TrmnlDisplayResponse = result.value
if (isDeviceSetupRequired(trmnlDeviceConfig, response)) {
return setupRequiredTrmnlDisplayInfo(trmnlDeviceConfig)
}
val displayInfo =
TrmnlDisplayInfo(
status = response.status,
trmnlDeviceType = trmnlDeviceConfig.type,
imageUrl = response.imageUrl ?: "",
imageName = response.imageName ?: "",
imageFileName = response.imageFileName ?: "",
error = response.error,
refreshIntervalSeconds = response.refreshRate,
httpResponseMetadata = extractHttpResponseMetadata(result),
@@ -132,7 +140,7 @@ class TrmnlDisplayRepository
status = response.status,
trmnlDeviceType = trmnlDeviceConfig.type,
imageUrl = response.imageUrl ?: "",
imageName = response.filename ?: "",
imageFileName = response.filename ?: "",
error = response.error,
refreshIntervalSeconds = response.refreshRateSec,
httpResponseMetadata = extractHttpResponseMetadata(result),
@@ -151,6 +159,46 @@ class TrmnlDisplayRepository
}
}
/**
* Sets up a new device by calling the setup API endpoint.
*
* This is only applicable for BYOS devices, as other device types do not require setup.
*
* @param trmnlDeviceConfig The configuration for the device to be set up.
* @return A [DeviceSetupInfo] object containing the result of the setup operation.
*/
suspend fun setupNewDevice(trmnlDeviceConfig: TrmnlDeviceConfig): DeviceSetupInfo {
if (trmnlDeviceConfig.type != TrmnlDeviceType.BYOS) {
Timber.w("Device setup is only applicable for BYOS devices.")
}
val result =
apiService.setupNewDevice(
fullApiUrl = constructApiUrl(trmnlDeviceConfig.apiBaseUrl, TrmnlApiService.SETUP_API_PATH),
deviceMacId = requireNotNull(trmnlDeviceConfig.deviceMacId) { "Device MAC ID is required for setup" },
)
when (result) {
is ApiResult.Failure -> {
Timber.e("Failed to setup device: ${result.exceptionOrNull()}")
return DeviceSetupInfo(
success = false,
deviceMacId = trmnlDeviceConfig.deviceMacId,
apiKey = "",
message = "Failed to setup device with ID (${trmnlDeviceConfig.deviceMacId}). Reason: $result",
)
}
is ApiResult.Success -> {
Timber.i("Device setup successful: ${result.value}")
return DeviceSetupInfo(
success = true,
deviceMacId = trmnlDeviceConfig.deviceMacId,
apiKey = result.value.apiKey,
message = result.value.message,
)
}
}
}
/**
* Generates fake display info for debugging purposes without wasting an API request.
*
@@ -169,7 +217,7 @@ class TrmnlDisplayRepository
status = HTTP_200,
trmnlDeviceType = TrmnlDeviceType.TRMNL,
imageUrl = mockImageUrl,
imageName = "mocked-image-" + mockImageUrl.substringAfterLast('?'),
imageFileName = "mocked-image-" + mockImageUrl.substringAfterLast('?'),
error = null,
refreshIntervalSeconds = mockRefreshRate,
)
@@ -208,7 +256,7 @@ class TrmnlDisplayRepository
status = HTTP_500,
trmnlDeviceType = trmnlDeviceConfig.type,
imageUrl = "",
imageName = "",
imageFileName = "",
error = "API failure",
refreshIntervalSeconds = 0L,
)
@@ -222,7 +270,7 @@ class TrmnlDisplayRepository
status = HTTP_500,
trmnlDeviceType = trmnlDeviceConfig.type,
imageUrl = "",
imageName = "",
imageFileName = "",
error = "HTTP failure: ${failure.code}, error: ${failure.error}",
refreshIntervalSeconds = 0L,
)
@@ -236,7 +284,7 @@ class TrmnlDisplayRepository
status = HTTP_500,
trmnlDeviceType = trmnlDeviceConfig.type,
imageUrl = "",
imageName = "",
imageFileName = "",
error = "Network failure: ${failure.error.localizedMessage}",
refreshIntervalSeconds = 0L,
)
@@ -250,7 +298,7 @@ class TrmnlDisplayRepository
status = HTTP_500,
trmnlDeviceType = trmnlDeviceConfig.type,
imageUrl = "",
imageName = "",
imageFileName = "",
error = "Unknown failure: ${failure.error.localizedMessage}",
refreshIntervalSeconds = 0L,
)
@@ -285,4 +333,38 @@ class TrmnlDisplayRepository
timestamp = System.currentTimeMillis(),
)
}
/**
* Right now there is no good known way to determine if a device requires setup.
* The logic here is based on sample responses from the Terminus server API.
*
* See
* - https://discord.com/channels/1281055965508141100/1331360842809348106/1384605617456545904
* - https://discord.com/channels/1281055965508141100/1384605617456545904/1384613229086511135
*/
private fun isDeviceSetupRequired(
trmnlDeviceConfig: TrmnlDeviceConfig,
response: TrmnlDisplayResponse,
): Boolean =
trmnlDeviceConfig.type == TrmnlDeviceType.BYOS &&
response.imageFileName?.startsWith("setup", ignoreCase = true) == true &&
// This ensures that no screen is generated yet for the device
response.imageUrl?.contains("screens", ignoreCase = true) == false
/**
* Creates a [TrmnlDisplayInfo] indicating that the device requires setup.
*
* This is used when the device is not yet configured and needs to be set up before it can display content.
* @see ERROR_TYPE_DEVICE_SETUP_REQUIRED
* @see [isDeviceSetupRequired]
*/
private fun setupRequiredTrmnlDisplayInfo(trmnlDeviceConfig: TrmnlDeviceConfig): TrmnlDisplayInfo =
TrmnlDisplayInfo(
status = HTTP_500,
trmnlDeviceType = trmnlDeviceConfig.type,
imageUrl = "",
imageFileName = ERROR_TYPE_DEVICE_SETUP_REQUIRED,
error = "Device setup required",
refreshIntervalSeconds = 0L,
)
}
@@ -0,0 +1,31 @@
package ink.trmnl.android.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Data class representing the response from the TRMNL setup API.
*
* Sample JSON response:
* ```json
* {
* "api_key": "abc1234567890abcdef",
* "friendly_id": "GO87665",
* "image_url": "https://localhost:1234/assets/setup.bmp",
* "message": "Welcome to Terminus!"
* }
* ```
*
* @property apiKey The API key for the device.
* @property friendlyId A user-friendly identifier for the device.
* @property imageUrl The URL of an image to display during setup.
* @property message A welcome message or setup instructions.
* @see ink.trmnl.android.network.TrmnlApiService.setupNewDevice
*/
@JsonClass(generateAdapter = true)
data class TrmnlSetupResponse(
@Json(name = "api_key") val apiKey: String,
@Json(name = "friendly_id") val friendlyId: String,
@Json(name = "image_url") val imageUrl: String,
@Json(name = "message") val message: String,
)
@@ -2,6 +2,7 @@ package ink.trmnl.android.network
import com.slack.eithernet.ApiResult
import ink.trmnl.android.data.TrmnlDisplayRepository
import ink.trmnl.android.model.TrmnlSetupResponse
import ink.trmnl.android.network.model.TrmnlCurrentImageResponse
import ink.trmnl.android.network.model.TrmnlDisplayResponse
import retrofit2.http.GET
@@ -9,7 +10,7 @@ import retrofit2.http.Header
import retrofit2.http.Url
/**
* API service interface for TRMNL.
* API service interface for TRMNL or BYOS servers.
*
* This interface defines the endpoints for the TRMNL API.
*
@@ -22,18 +23,39 @@ import retrofit2.http.Url
interface TrmnlApiService {
companion object {
/**
* https://docs.usetrmnl.com/go/private-api/fetch-screen-content#auto-advance-content
* Path for the TRMNL API endpoint that provides the next image in a playlist.
*
* - https://docs.usetrmnl.com/go/private-api/fetch-screen-content#auto-advance-content
* - https://github.com/usetrmnl/byos_hanami?tab=readme-ov-file#display
*
* @see getNextDisplayData
*/
internal const val NEXT_PLAYLIST_SCREEN_API_PATH = "api/display"
/**
* Path for the TRMNL API endpoint that provides the current image in a playlist.
*
* https://docs.usetrmnl.com/go/private-api/fetch-screen-content#current-screen
*
* @see getCurrentDisplayData
*/
internal const val CURRENT_PLAYLIST_SCREEN_API_PATH = "api/current_screen"
/**
* Path for the TRMNL API endpoint used for new device setup.
*
* https://github.com/usetrmnl/byos_hanami?tab=readme-ov-file#setup-1
*
* @see setupNewDevice
*/
internal const val SETUP_API_PATH = "api/setup/"
/**
* Default content type for API requests.
*
* This is used when setting up a new device or making other API calls that require a content type header.
*/
private const val DEFAULT_CONTENT_TYPE = "application/json"
}
/**
@@ -68,4 +90,21 @@ interface TrmnlApiService {
@Url fullApiUrl: String,
@Header("access-token") accessToken: String,
): ApiResult<TrmnlCurrentImageResponse, Unit>
/**
* Setup a new TRMNL device using it's MAC ID. Using same API with same ID has no effect.
*
* This API is typically used once during the initial setup of a BYOS device.
* See https://github.com/usetrmnl/byos_hanami?tab=readme-ov-file#setup-1
*
* @param fullApiUrl The complete API URL to call (e.g., "https://your-server.com/api/setup").
* @param deviceMacId The device's MAC address, sent in the "ID" header.
* @return An [ApiResult] containing [TrmnlSetupResponse] on success.
*/
@GET
suspend fun setupNewDevice(
@Url fullApiUrl: String,
@Header("ID") deviceMacId: String,
@Header("Content-Type") contentType: String = DEFAULT_CONTENT_TYPE,
): ApiResult<TrmnlSetupResponse, Unit>
}
@@ -49,6 +49,17 @@ import ink.trmnl.android.util.HTTP_NONE
* "update_firmware": false
* }
* ```
*
* Sample 4404 response from BYOS Hanami server:
* ```json
* {
* "type": "/problem_details#device_id",
* "title": "Not Found",
* "status": 404,
* "detail": "Invalid device ID.",
* "instance": "/api/display"
* }
* ```
*/
@JsonClass(generateAdapter = true)
data class TrmnlDisplayResponse(
@@ -64,7 +75,7 @@ data class TrmnlDisplayResponse(
*/
val status: Int = HTTP_NONE,
@Json(name = "image_url") val imageUrl: String?,
@Json(name = "filename") val imageName: String?,
@Json(name = "filename") val imageFileName: String?,
@Json(name = "update_firmware") val updateFirmware: Boolean?,
@Json(name = "firmware_url") val firmwareUrl: String?,
@Json(name = "refresh_rate") val refreshRate: Long?,
@@ -36,6 +36,7 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -85,6 +86,7 @@ import dagger.assisted.AssistedInject
import ink.trmnl.android.R
import ink.trmnl.android.data.AppConfig.DEFAULT_REFRESH_INTERVAL_SEC
import ink.trmnl.android.data.AppConfig.TRMNL_API_SERVER_BASE_URL
import ink.trmnl.android.data.DeviceSetupInfo
import ink.trmnl.android.data.RepositoryConfigProvider
import ink.trmnl.android.data.TrmnlDeviceConfigDataStore
import ink.trmnl.android.data.TrmnlDisplayRepository
@@ -99,6 +101,7 @@ import ink.trmnl.android.ui.settings.AppSettingsScreen.ValidationResult.InvalidS
import ink.trmnl.android.ui.settings.AppSettingsScreen.ValidationResult.Success
import ink.trmnl.android.ui.theme.TrmnlDisplayAppTheme
import ink.trmnl.android.util.CoilRequestUtils
import ink.trmnl.android.util.ERROR_TYPE_DEVICE_SETUP_REQUIRED
import ink.trmnl.android.util.NextImageRefreshDisplayInfo
import ink.trmnl.android.util.isHttpError
import ink.trmnl.android.util.isValidMacAddress
@@ -140,6 +143,8 @@ data class AppSettingsScreen(
val usesFakeApiData: Boolean,
val isLoading: Boolean = false,
val validationResult: ValidationResult? = null,
val isDeviceSetupLoading: Boolean = false,
val deviceSetupMessage: String? = null,
val nextRefreshJobInfo: NextImageRefreshDisplayInfo? = null,
val eventSink: (Event) -> Unit,
) : CircuitUiState
@@ -161,6 +166,10 @@ data class AppSettingsScreen(
data class Failure(
val message: String,
) : ValidationResult()
data class DeviceSetupRequired(
val message: String,
) : ValidationResult()
}
/**
@@ -213,6 +222,10 @@ data class AppSettingsScreen(
* Event triggered when the info icon is clicked.
*/
data object AppInfoPressed : Event()
data class SetupDevice(
val deviceMacId: String,
) : Event()
}
}
@@ -239,6 +252,8 @@ class AppSettingsPresenter
var deviceMacId by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var validationResult by remember { mutableStateOf<ValidationResult?>(null) }
var isDeviceSetupLoading by remember { mutableStateOf(false) }
var deviceSetupMessage by remember { mutableStateOf<String?>(null) }
val usesFakeApiData = repositoryConfigProvider.shouldUseFakeData
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
@@ -273,6 +288,8 @@ class AppSettingsPresenter
usesFakeApiData = usesFakeApiData,
isLoading = isLoading,
validationResult = validationResult,
isDeviceSetupLoading = isDeviceSetupLoading,
deviceSetupMessage = deviceSetupMessage,
nextRefreshJobInfo = nextRefreshInfo,
eventSink = { event ->
when (event) {
@@ -280,6 +297,7 @@ class AppSettingsPresenter
accessToken = event.token
// Clear previous validation when token changes
validationResult = null
deviceSetupMessage = null
}
AppSettingsScreen.Event.ValidateToken -> {
@@ -287,6 +305,7 @@ class AppSettingsPresenter
focusManager.clearFocus()
isLoading = true
validationResult = null
deviceSetupMessage = null
// First validate server URL if device type is BYOS
if (deviceType == TrmnlDeviceType.BYOS) {
@@ -333,10 +352,17 @@ class AppSettingsPresenter
}
if (response.status.isHttpError()) {
// Handle explicit error response
// FIXME - default error message is wrong, can't assume device not found
val errorMessage = response.error ?: "Device not found"
validationResult = Failure(errorMessage)
if (response.imageFileName == ERROR_TYPE_DEVICE_SETUP_REQUIRED) {
// Special case for device setup required
validationResult =
ValidationResult.DeviceSetupRequired(
response.error ?: "Device setup required. Please follow the setup instructions.",
)
} else {
// Handle explicit error response
val errorMessage = response.error ?: "Unexpected error occurred. Please check required inputs."
validationResult = Failure(errorMessage)
}
} else if (response.imageUrl.isNotBlank()) {
// Success case - we have an image URL
trmnlImageUpdateManager.updateImage(response.imageUrl, response.refreshIntervalSeconds)
@@ -392,6 +418,7 @@ class AppSettingsPresenter
deviceType = event.type
// Clear validation result when device type changes
validationResult = null
deviceSetupMessage = null
}
is AppSettingsScreen.Event.ServerUrlChanged -> {
@@ -399,6 +426,7 @@ class AppSettingsPresenter
// Clear validation result when server URL changes
if (validationResult is InvalidServerUrl) {
validationResult = null
deviceSetupMessage = null
}
}
@@ -406,12 +434,42 @@ class AppSettingsPresenter
deviceMacId = event.deviceMacId
// Clear previous validation when device ID changes
validationResult = null
deviceSetupMessage = null
}
AppSettingsScreen.Event.AppInfoPressed -> {
// Navigate to AppInfoScreen
navigator.goTo(AppInfoScreen)
}
is AppSettingsScreen.Event.SetupDevice -> {
isDeviceSetupLoading = true
deviceSetupMessage = null
scope.launch {
// Call the setup API with the provided device ID
val setupResult: DeviceSetupInfo =
displayRepository.setupNewDevice(
TrmnlDeviceConfig(
type = deviceType,
apiBaseUrl = serverBaseUrl.forDevice(deviceType),
apiAccessToken = accessToken,
deviceMacId = event.deviceMacId,
),
)
isDeviceSetupLoading = false
if (!setupResult.success) {
// Handle error response
deviceSetupMessage = setupResult.message
} else {
deviceSetupMessage = "Device setup successful! Re-validate ID/Token to continue."
// Also prepopulate the access token
accessToken = setupResult.apiKey
}
}
}
}
},
)
@@ -608,7 +666,7 @@ fun AppSettingsContent(
modifier = Modifier.fillMaxWidth(),
) {
when (result) {
is ValidationResult.Success -> {
is Success -> {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -645,7 +703,7 @@ fun AppSettingsContent(
}
}
}
is ValidationResult.InvalidServerUrl -> {
is InvalidServerUrl -> {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -681,7 +739,7 @@ fun AppSettingsContent(
)
}
}
is ValidationResult.Failure -> {
is Failure -> {
// Error state remains the same
Column(
modifier = Modifier.padding(16.dp),
@@ -700,6 +758,48 @@ fun AppSettingsContent(
)
}
}
is ValidationResult.DeviceSetupRequired -> {
Column(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
"⚠️ Device Setup Required",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.error,
)
Spacer(modifier = Modifier.height(8.dp))
if (state.deviceSetupMessage != null) {
Text(
text = state.deviceSetupMessage,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.primary,
)
} else {
FilledTonalButton(
onClick = {
state.eventSink(
AppSettingsScreen.Event.SetupDevice(
deviceMacId = state.deviceMacId,
),
)
},
enabled = !state.isDeviceSetupLoading,
) {
if (state.isDeviceSetupLoading) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
)
} else {
Text("Setup Device")
}
}
}
}
}
}
}
}
@@ -1,5 +1,7 @@
package ink.trmnl.android.util
import ink.trmnl.android.data.TrmnlDisplayInfo
/**
* 500 Internal Server Error - A generic error message, given when an unexpected
* condition was encountered and no more specific message is suitable.
@@ -32,3 +34,12 @@ internal fun Int?.isHttpOk(): Boolean = this == HTTP_OK || this == HTTP_200 || t
* Extension function to check if the HTTP status code is an error.
*/
internal fun Int?.isHttpError(): Boolean = this == HTTP_500 || this == null
/**
* Special error code provided in the [TrmnlDisplayInfo.imageFileName] as hack to indicate that the device requires setup.
*
* See following for additional context:
* - https://discord.com/channels/1281055965508141100/1331360842809348106/1384605617456545904
* - https://github.com/usetrmnl/trmnl-android/issues/83
*/
internal const val ERROR_TYPE_DEVICE_SETUP_REQUIRED = "device_requires_setup"
@@ -118,7 +118,7 @@ class TrmnlImageRefreshWorker(
refreshLogManager.addSuccessLog(
trmnlDeviceType = deviceConfig.type,
imageUrl = trmnlDisplayInfo.imageUrl,
imageName = trmnlDisplayInfo.imageName,
imageName = trmnlDisplayInfo.imageFileName,
refreshIntervalSeconds = trmnlDisplayInfo.refreshIntervalSeconds,
imageRefreshWorkType = workTypeValue,
httpResponseMetadata = trmnlDisplayInfo.httpResponseMetadata,
@@ -81,7 +81,7 @@ class TrmnlDisplayRepositoryTest {
TrmnlDisplayResponse(
status = 200,
imageUrl = "https://test.com/image.png",
imageName = "test-image.png",
imageFileName = "test-image.png",
refreshRate = 300L,
error = null,
updateFirmware = null,
@@ -105,7 +105,7 @@ class TrmnlDisplayRepositoryTest {
// Assert
assertThat(result.status).isEqualTo(200)
assertThat(result.imageUrl).isEqualTo("https://test.com/image.png")
assertThat(result.imageName).isEqualTo("test-image.png")
assertThat(result.imageFileName).isEqualTo("test-image.png")
assertThat(result.refreshIntervalSeconds).isEqualTo(300L)
assertThat(result.error).isNull()
assertThat(result.trmnlDeviceType).isEqualTo(TrmnlDeviceType.TRMNL)
@@ -122,7 +122,7 @@ class TrmnlDisplayRepositoryTest {
TrmnlDisplayResponse(
status = 500,
imageUrl = null,
imageName = null,
imageFileName = null,
refreshRate = null,
error = "Error fetching display",
updateFirmware = null,
@@ -146,7 +146,7 @@ class TrmnlDisplayRepositoryTest {
// Assert
assertThat(result.status).isEqualTo(500)
assertThat(result.imageUrl).isEmpty()
assertThat(result.imageName).isEmpty()
assertThat(result.imageFileName).isEmpty()
assertThat(result.refreshIntervalSeconds).isNull()
assertThat(result.error).isEqualTo("Error fetching display")
assertThat(result.trmnlDeviceType).isEqualTo(TrmnlDeviceType.TRMNL)
@@ -184,7 +184,7 @@ class TrmnlDisplayRepositoryTest {
// Assert
assertThat(result.status).isEqualTo(200)
assertThat(result.imageUrl).isEqualTo("https://test.com/current.png")
assertThat(result.imageName).isEqualTo("current-image.png")
assertThat(result.imageFileName).isEqualTo("current-image.png")
assertThat(result.refreshIntervalSeconds).isEqualTo(600L)
assertThat(result.error).isNull()
assertThat(result.trmnlDeviceType).isEqualTo(TrmnlDeviceType.TRMNL)
@@ -222,7 +222,7 @@ class TrmnlDisplayRepositoryTest {
// Assert
assertThat(result.status).isEqualTo(500)
assertThat(result.imageUrl).isEmpty()
assertThat(result.imageName).isEmpty()
assertThat(result.imageFileName).isEmpty()
assertThat(result.refreshIntervalSeconds).isNull()
assertThat(result.error).isEqualTo("Device not found")
assertThat(result.trmnlDeviceType).isEqualTo(TrmnlDeviceType.TRMNL)
@@ -243,7 +243,7 @@ class TrmnlDisplayRepositoryTest {
// Assert
assertThat(result.status).isEqualTo(200)
assertThat(result.imageUrl).contains("picsum.photos")
assertThat(result.imageName).contains("mocked-image-grayscale&time")
assertThat(result.imageFileName).contains("mocked-image-grayscale&time")
assertThat(result.refreshIntervalSeconds).isEqualTo(600L)
assertThat(result.error).isNull()
@@ -263,7 +263,7 @@ class TrmnlDisplayRepositoryTest {
// Assert
assertThat(result.status).isEqualTo(200)
assertThat(result.imageUrl).contains("picsum.photos")
assertThat(result.imageName).contains("mocked-image-grayscale&time")
assertThat(result.imageFileName).contains("mocked-image-grayscale&time")
assertThat(result.refreshIntervalSeconds).isEqualTo(600L)
assertThat(result.error).isNull()
@@ -341,7 +341,7 @@ class TrmnlDisplayRepositoryTest {
TrmnlDisplayResponse(
status = 200,
imageUrl = "https://test.com/image.png",
imageName = "test-image.png",
imageFileName = "test-image.png",
refreshRate = 300L,
error = null,
updateFirmware = null,
@@ -385,7 +385,7 @@ class TrmnlDisplayRepositoryTest {
TrmnlDisplayResponse(
status = 200,
imageUrl = "https://test.com/image.png",
imageName = "test-image.png",
imageFileName = "test-image.png",
refreshRate = 300L,
error = null,
updateFirmware = null,
@@ -139,7 +139,7 @@ class TrmnlImageRefreshWorkerTest {
status = HTTP_200,
trmnlDeviceType = TrmnlDeviceType.TRMNL,
imageUrl = validImageUrl,
imageName = "test-image.png",
imageFileName = "test-image.png",
refreshIntervalSeconds = validRefreshRate,
)
@@ -181,7 +181,7 @@ class TrmnlImageRefreshWorkerTest {
status = HTTP_200,
trmnlDeviceType = TrmnlDeviceType.BYOD,
imageUrl = validImageUrl,
imageName = "test-image.png",
imageFileName = "test-image.png",
refreshIntervalSeconds = validRefreshRate,
)
@@ -210,7 +210,7 @@ class TrmnlImageRefreshWorkerTest {
status = HTTP_200,
trmnlDeviceType = TrmnlDeviceType.BYOS,
imageUrl = validImageUrl,
imageName = "test-image.png",
imageFileName = "test-image.png",
refreshIntervalSeconds = validRefreshRate,
)
@@ -243,7 +243,7 @@ class TrmnlImageRefreshWorkerTest {
status = HTTP_200,
trmnlDeviceType = TrmnlDeviceType.TRMNL,
imageUrl = validImageUrl,
imageName = "test-image.png",
imageFileName = "test-image.png",
refreshIntervalSeconds = validRefreshRate,
)
@@ -270,7 +270,7 @@ class TrmnlImageRefreshWorkerTest {
status = HTTP_500,
trmnlDeviceType = TrmnlDeviceType.TRMNL,
imageUrl = "",
imageName = "",
imageFileName = "",
error = "Device not found",
refreshIntervalSeconds = null,
)
@@ -304,7 +304,7 @@ class TrmnlImageRefreshWorkerTest {
status = HTTP_200,
trmnlDeviceType = TrmnlDeviceType.TRMNL,
imageUrl = "",
imageName = "",
imageFileName = "",
refreshIntervalSeconds = validRefreshRate,
)
@@ -330,7 +330,7 @@ class TrmnlImageRefreshWorkerTest {
status = HTTP_200,
trmnlDeviceType = TrmnlDeviceType.TRMNL,
imageUrl = validImageUrl,
imageName = "test-image.png",
imageFileName = "test-image.png",
refreshIntervalSeconds = newRefreshRate,
)
@@ -357,7 +357,7 @@ class TrmnlImageRefreshWorkerTest {
status = HTTP_200,
trmnlDeviceType = TrmnlDeviceType.TRMNL,
imageUrl = validImageUrl,
imageName = "test-image.png",
imageFileName = "test-image.png",
refreshIntervalSeconds = validRefreshRate,
)
@@ -389,7 +389,7 @@ class TrmnlImageRefreshWorkerTest {
status = HTTP_200,
trmnlDeviceType = TrmnlDeviceType.TRMNL,
imageUrl = validImageUrl,
imageName = "test-image.png",
imageFileName = "test-image.png",
refreshIntervalSeconds = validRefreshRate,
)