diff --git a/CHANGELOG.md b/CHANGELOG.md index 6961ea0f..7637fce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Change Log ========== +1.1.3 +----- + +### Fixed: + + - Handle null `lastInstall` fields on hue lights' update state. + 1.1.2 ----- diff --git a/lights/build.gradle.kts b/lights/build.gradle.kts index b132fc4f..95ae6c57 100644 --- a/lights/build.gradle.kts +++ b/lights/build.gradle.kts @@ -21,4 +21,6 @@ dependencies { kapt(moshi("moshi-kotlin-codegen")) compile(okHttp()) compile(threeTen()) + + testImplementation(jUnit()) } diff --git a/lights/src/main/kotlin/inkapplications/shade/lights/HueLightsApi.kt b/lights/src/main/kotlin/inkapplications/shade/lights/HueLightsApi.kt index f86578b0..58e95cbe 100644 --- a/lights/src/main/kotlin/inkapplications/shade/lights/HueLightsApi.kt +++ b/lights/src/main/kotlin/inkapplications/shade/lights/HueLightsApi.kt @@ -285,10 +285,23 @@ data class ControlCapabilities( * of this aren't documented anywhere I can find. * @property lastInstall Timestamp of the last firmware update. */ -@JsonClass(generateAdapter = true) data class UpdateState( val state: String, - @Json(name="lastinstall") val lastInstall: Instant + @Deprecated( + message = "Use lastKnownInstall to handle optional cases.", + replaceWith = ReplaceWith("lastKnownInstall") + ) + val lastInstall: Instant, + val lastKnownInstall: Instant? = lastInstall +) + +/** + * Internal representation of the [UpdateState] class for Json Transformation. + */ +@JsonClass(generateAdapter = true) +internal data class HueUpdateState( + val state: String, + @Json(name="lastinstall") val lastInstall: Instant? ) /** diff --git a/lights/src/main/kotlin/inkapplications/shade/lights/HueUpdateStateTransformer.kt b/lights/src/main/kotlin/inkapplications/shade/lights/HueUpdateStateTransformer.kt new file mode 100644 index 00000000..31e230ed --- /dev/null +++ b/lights/src/main/kotlin/inkapplications/shade/lights/HueUpdateStateTransformer.kt @@ -0,0 +1,20 @@ +package inkapplications.shade.lights + +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson +import org.threeten.bp.Instant + +internal object HueUpdateStateTransformer { + @ToJson + fun toHueFormat(state: UpdateState): HueUpdateState = HueUpdateState( + state = state.state, + lastInstall = state.lastKnownInstall.takeIf { it != Instant.MIN } + ) + + @FromJson + fun fromHueFormat(state: HueUpdateState): UpdateState = UpdateState( + state = state.state, + lastInstall = state.lastInstall ?: Instant.MIN, + lastKnownInstall = state.lastInstall + ) +} diff --git a/lights/src/main/kotlin/inkapplications/shade/lights/ShadeLightsModule.kt b/lights/src/main/kotlin/inkapplications/shade/lights/ShadeLightsModule.kt index cc091816..bb2f65c2 100644 --- a/lights/src/main/kotlin/inkapplications/shade/lights/ShadeLightsModule.kt +++ b/lights/src/main/kotlin/inkapplications/shade/lights/ShadeLightsModule.kt @@ -21,7 +21,8 @@ class ShadeLightsModule { TemperatureRangeTransformer, BrightnessTransformer, DurationTransformer, - HueLightStateTransformer + HueLightStateTransformer, + HueUpdateStateTransformer ) /** diff --git a/lights/src/test/kotlin/inkapplications/shade/lights/HueUpdateStateTransformerTest.kt b/lights/src/test/kotlin/inkapplications/shade/lights/HueUpdateStateTransformerTest.kt new file mode 100644 index 00000000..8897cb4e --- /dev/null +++ b/lights/src/test/kotlin/inkapplications/shade/lights/HueUpdateStateTransformerTest.kt @@ -0,0 +1,74 @@ +package inkapplications.shade.lights + +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import org.junit.Test +import org.threeten.bp.Instant + +class HueUpdateStateTransformerTest { + @Test + fun fromHueFormat() { + val now = Instant.now() + val given = HueUpdateState("fake", now) + + val result = HueUpdateStateTransformer.fromHueFormat(given) + + assertEquals("State is not modified", "fake", result.state) + assertEquals("Last install is used in nullable", now, result.lastKnownInstall) + assertEquals("Last install is used in non-nullable field", now, result.lastInstall) + } + + @Test + fun fromNullHueFormat() { + val given = HueUpdateState("fake", null) + + val result = HueUpdateStateTransformer.fromHueFormat(given) + + assertEquals("State is not modified", "fake", result.state) + assertNull("Nullable field should reflect null last install", result.lastKnownInstall) + assertEquals("Non-nullable field should use MIN as timestamp", Instant.MIN, result.lastInstall) + } + + @Test + fun toHueFormatCompat() { + val now = Instant.now() + val given = UpdateState("fake", now) + + val result = HueUpdateStateTransformer.toHueFormat(given) + + assertEquals("State is not modified", "fake", result.state) + assertEquals("Nullable last install should be passed along", now, result.lastInstall) + } + + @Test + fun toHueFormatCompatSpecified() { + val now = Instant.now() + val given = UpdateState("fake", Instant.MIN, now) + + val result = HueUpdateStateTransformer.toHueFormat(given) + + assertEquals("State is not modified", "fake", result.state) + assertEquals("Nullable field should take precedent if specified", now, result.lastInstall) + } + + @Test + fun toHueFormatCompatNull() { + val now = Instant.now() + val given = UpdateState("fake", now, null) + + val result = HueUpdateStateTransformer.toHueFormat(given) + + assertEquals("State is not modified", "fake", result.state) + assertNull("Nullable field should take precedent if specified, even if null", result.lastInstall) + } + + @Test + fun toHueFormatCompatNullFromMin() { + val given = UpdateState("fake", Instant.MIN) + + val result = HueUpdateStateTransformer.toHueFormat(given) + + assertEquals("State is not modified", "fake", result.state) + assertNull("Min instants should be interpreted as null for compatibility", result.lastInstall) + } +}