Skip to content

Commit 8317d9a

Browse files
committed
feat!: WIP: support for device code authentication scheme
1 parent 046e9e8 commit 8317d9a

60 files changed

Lines changed: 1343 additions & 906 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ ARG TEST_IMAGE=eclipse-temurin:17
33
ARG RUNTIME_IMAGE=eclipse-temurin:17-jre
44
ARG MAVEN_OPTS="-Xmx2000m"
55

6-
FROM --platform=$BUILDPLATFORM $BUILD_IMAGE as builder
6+
FROM --platform=$BUILDPLATFORM $BUILD_IMAGE AS builder
77
ARG MAVEN_OPTS
88

99
WORKDIR /build
@@ -26,7 +26,7 @@ COPY tado-exporter/src /build/tado-exporter/src
2626
RUN ./mvnw -DskipTests -B package
2727

2828
# Integration tests
29-
FROM --platform=$BUILDPLATFORM $TEST_IMAGE as test
29+
FROM --platform=$BUILDPLATFORM $TEST_IMAGE AS test
3030
ARG MAVEN_OPTS
3131

3232
WORKDIR /build
@@ -43,4 +43,4 @@ COPY --from=builder /build/tado-exporter/target/tado-exporter-*.jar tado-exporte
4343
ENV JAVA_OPTS="-Xmx64m -Xms64m"
4444
EXPOSE 8080
4545
USER 65535:65535
46-
CMD exec java ${JAVA_OPTS} -jar tado-exporter.jar
46+
CMD [ "sh", "-c", "exec java $JAVA_OPTS -jar tado-exporter.jar" ]

pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
</properties>
5252

5353
<modules>
54-
<module>tado-util</module>
5554
<module>tado-api</module>
5655
<module>tado-exporter</module>
5756
</modules>

tado-exporter/pom.xml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@
2828
<groupId>click.dobel.tado</groupId>
2929
<artifactId>tado-api</artifactId>
3030
</dependency>
31-
<dependency>
32-
<groupId>click.dobel.tado</groupId>
33-
<artifactId>tado-util</artifactId>
34-
</dependency>
3531

3632
<dependency>
3733
<groupId>org.springframework.boot</groupId>
@@ -60,7 +56,7 @@
6056
<groupId>io.github.microutils</groupId>
6157
<artifactId>kotlin-logging-jvm</artifactId>
6258
</dependency>
63-
59+
6460
<dependency>
6561
<groupId>io.micrometer</groupId>
6662
<artifactId>micrometer-registry-prometheus</artifactId>
@@ -82,6 +78,12 @@
8278
<scope>runtime</scope>
8379
</dependency>
8480

81+
<dependency>
82+
<groupId>com.fasterxml.jackson.datatype</groupId>
83+
<artifactId>jackson-datatype-jsr310</artifactId>
84+
<scope>runtime</scope>
85+
</dependency>
86+
8587
<dependency>
8688
<groupId>org.springframework.boot</groupId>
8789
<artifactId>spring-boot-starter-test</artifactId>

tado-exporter/src/main/kotlin/click/dobel/tado/exporter/TadoExporterConfiguration.kt

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import click.dobel.tado.exporter.apiclient.TadoConfigurationProperties
1010
import click.dobel.tado.exporter.metrics.TadoMeterFactory
1111
import click.dobel.tado.exporter.metrics.ValueFilteringPrometheusRegistry
1212
import click.dobel.tado.exporter.ratelimit.OncePerIntervalRateLimiter
13-
import click.dobel.tado.exporter.ratelimit.RateLimiter
14-
import click.dobel.tado.util.aop.CallLoggingInterceptor
13+
import click.dobel.tado.exporter.ratelimit.RateLimiterFactory
1514
import com.github.benmanes.caffeine.cache.Caffeine
1615
import com.github.benmanes.caffeine.cache.stats.StatsCounter
1716
import io.micrometer.core.instrument.Meter
@@ -27,7 +26,6 @@ import org.springframework.context.annotation.Bean
2726
import org.springframework.context.annotation.Configuration
2827
import org.springframework.scheduling.annotation.EnableScheduling
2928
import java.util.concurrent.TimeUnit
30-
import kotlin.time.Duration.Companion.minutes
3129

3230
@Configuration
3331
@EnableCaching
@@ -79,13 +77,7 @@ class TadoExporterConfiguration {
7977
}
8078

8179
@Bean
82-
fun authRateLimiter(): RateLimiter = OncePerIntervalRateLimiter(
83-
"Authentication attempt",
84-
1.minutes
85-
)
86-
87-
@Bean
88-
fun aopLogger() = CallLoggingInterceptor()
80+
fun rateLimiterFactory(): RateLimiterFactory = ::OncePerIntervalRateLimiter
8981

9082
@Bean
9183
fun prometheusRegistry(): PrometheusRegistry {
Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
11
package click.dobel.tado.exporter.apiclient
22

3-
import org.springframework.http.ResponseEntity
3+
import click.dobel.tado.exporter.apiclient.auth.model.accesstoken.AccessToken
4+
import org.springframework.boot.web.client.RestTemplateBuilder
5+
import org.springframework.http.HttpHeaders
46
import org.springframework.web.client.ResourceAccessException
5-
import org.springframework.web.client.RestTemplate
7+
import org.springframework.web.client.RestClient
8+
import org.springframework.web.client.RestClient.RequestHeadersSpec
69

7-
inline fun <reified T : Any> RestTemplate.getForEntity(
8-
url: String,
9-
vararg uriVariables: Any
10-
): ResponseEntity<T> {
11-
return getForEntity(url, T::class.java, uriVariables)
12-
}
10+
fun RestTemplateBuilder.userAgent(): RestTemplateBuilder =
11+
defaultHeader(HttpHeaders.USER_AGENT, "tado-exporter (https://github.com/easimon/tado-exporter)")
1312

14-
inline fun <reified T : Any> RestTemplate.getForObject(
15-
url: String,
16-
vararg uriVariables: Any
17-
): T {
18-
return getForObject(url, T::class.java, uriVariables)
19-
?: throw ResourceAccessException("I/O error on GET $url: null response.")
20-
}
13+
fun <S : RequestHeadersSpec<S>> RequestHeadersSpec<S>.bearerAuth(token: AccessToken.BearerToken) =
14+
header(HttpHeaders.AUTHORIZATION, "Bearer ${token.value}")
2115

22-
inline fun <reified R : Any, reified T : Any> RestTemplate.postForObject(
23-
url: String,
24-
body: R,
25-
vararg uriVariables: Any
26-
): T {
27-
return postForObject(url, body, T::class.java, uriVariables)
28-
?: throw ResourceAccessException("I/O error on POST $url: null response.")
29-
}
16+
inline fun <reified T> RestClient.ResponseSpec.bodyOrError(): T =
17+
body(T::class.java) ?: throw ResourceAccessException("HTTP request returned null")

tado-exporter/src/main/kotlin/click/dobel/tado/exporter/apiclient/TadoApiClient.kt

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,27 @@ import click.dobel.tado.api.HomeState
55
import click.dobel.tado.api.User
66
import click.dobel.tado.api.WeatherReport
77
import click.dobel.tado.api.ZoneState
8-
import click.dobel.tado.exporter.apiclient.auth.TadoAuthFilter
8+
import click.dobel.tado.exporter.apiclient.auth.TadoAuthenticator
9+
import click.dobel.tado.exporter.apiclient.auth.model.accesstoken.AccessToken
10+
import click.dobel.tado.exporter.apiclient.logging.loggingIfConfigured
911
import click.dobel.tado.exporter.apiclient.model.Zones
10-
import click.dobel.tado.util.aop.Logged
12+
import mu.KotlinLogging
1113
import org.springframework.boot.web.client.RestTemplateBuilder
1214
import org.springframework.cache.annotation.Cacheable
15+
import org.springframework.http.MediaType
1316
import org.springframework.stereotype.Component
14-
import org.springframework.web.client.RestTemplate
17+
import org.springframework.web.client.ResourceAccessException
18+
import org.springframework.web.client.RestClient
1519

1620
@Component
1721
class TadoApiClient(
1822
private val configuration: TadoConfigurationProperties,
19-
authFilter: TadoAuthFilter,
23+
private val authenticationState: TadoAuthenticator,
2024
restTemplateBuilder: RestTemplateBuilder
2125
) {
2226

2327
companion object {
28+
private val logger = KotlinLogging.logger {}
2429
const val BASE_URL = "/api/v2"
2530

2631
const val ME_PATH = "/me"
@@ -30,6 +35,8 @@ class TadoApiClient(
3035
const val WEATHER_PATH = "/weather"
3136
}
3237

38+
val isAuthenticated: Boolean get() = authenticationState.accessToken is AccessToken.BearerToken
39+
3340
@Cacheable("User", sync = true)
3441
fun me() = get<User>(ME_PATH)
3542

@@ -46,19 +53,32 @@ class TadoApiClient(
4653
get("$HOMES_PATH/${homeId}/$ZONES_PATH")
4754

4855
@Cacheable("ZoneState", sync = true)
49-
@Logged("Retrieving fresh zone state for HomeId {}, ZoneId {}.", ["homeId", "zoneId"])
50-
fun zoneState(homeId: Int, zoneId: Int): ZoneState =
51-
get("$HOMES_PATH/${homeId}/$ZONES_PATH/${zoneId}/$STATE_PATH")
56+
fun zoneState(homeId: Int, zoneId: Int): ZoneState = logger
57+
.info { "Retrieving fresh zone state for HomeId $homeId, ZoneId $zoneId." }
58+
.run { get("$HOMES_PATH/${homeId}/$ZONES_PATH/${zoneId}/$STATE_PATH") }
5259

5360
@Cacheable("WeatherReport", sync = true)
54-
@Logged("Retrieving fresh weather report for HomeId {}.", ["homeId"])
55-
fun weather(homeId: Int): WeatherReport =
56-
get("$HOMES_PATH/${homeId}/$WEATHER_PATH")
61+
fun weather(homeId: Int): WeatherReport = logger
62+
.info { "Retrieving fresh weather report for HomeId $homeId." }
63+
.run { get("$HOMES_PATH/${homeId}/$WEATHER_PATH") }
5764

5865
private inline fun <reified T : Any> get(
5966
path: String
60-
): T {
61-
return restTemplate.getForObject(url(path))
67+
): T = when (val token = authenticationState.accessToken) {
68+
is AccessToken.BearerToken -> {
69+
restClient.get()
70+
.uri(url(path))
71+
.bearerAuth(token)
72+
.accept(MediaType.APPLICATION_JSON)
73+
.retrieve()
74+
.bodyOrError()
75+
}
76+
77+
else -> {
78+
throw ResourceAccessException(
79+
"Skipping request, not authenticated (${authenticationState.currentStateAsString})"
80+
)
81+
}
6282
}
6383

6484
private fun url(
@@ -69,8 +89,10 @@ class TadoApiClient(
6989
path: String
7090
): String = if (path.startsWith("/")) path else "/$path"
7191

72-
private val restTemplate: RestTemplate = restTemplateBuilder
73-
.additionalInterceptors(authFilter)
74-
.build()
92+
private val restClient: RestClient = RestClient.create(
93+
restTemplateBuilder
94+
.loggingIfConfigured(configuration)
95+
.userAgent()
96+
.build()
97+
)
7598
}
76-
Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package click.dobel.tado.exporter.apiclient
22

33
import org.springframework.boot.context.properties.ConfigurationProperties
4-
import java.time.Duration
54

65
@ConfigurationProperties("tado")
7-
class TadoConfigurationProperties(
8-
val username: String,
9-
val password: String,
6+
data class TadoConfigurationProperties(
107
val clientId: String,
11-
val clientSecret: String,
12-
val scope: String,
138
val apiServer: String,
149
val authServer: String,
15-
val zoneDiscoveryInterval: Duration
10+
val authCachePath: String,
11+
val zoneDiscoveryInterval: JavaDuration,
12+
val debug: TadoDebuggingConfig = TadoDebuggingConfig(),
1613
)
14+
15+
data class TadoDebuggingConfig(
16+
val http: Boolean = false
17+
)
18+
19+
typealias JavaDuration = java.time.Duration

tado-exporter/src/main/kotlin/click/dobel/tado/exporter/apiclient/auth/AuthClient.kt

Lines changed: 0 additions & 35 deletions
This file was deleted.

tado-exporter/src/main/kotlin/click/dobel/tado/exporter/apiclient/auth/RateLimitException.kt

Lines changed: 0 additions & 6 deletions
This file was deleted.

tado-exporter/src/main/kotlin/click/dobel/tado/exporter/apiclient/auth/TadoAuthFilter.kt

Lines changed: 0 additions & 71 deletions
This file was deleted.

0 commit comments

Comments
 (0)