1
+ package eu.slomkowski.octoglow.octoglowd.daemon.frontdisplay
2
+
3
+ import com.uchuhimo.konf.Config
4
+ import eu.slomkowski.octoglow.octoglowd.*
5
+ import eu.slomkowski.octoglow.octoglowd.hardware.Hardware
6
+ import eu.slomkowski.octoglow.octoglowd.hardware.Slot
7
+ import io.ktor.client.request.*
8
+ import kotlinx.coroutines.async
9
+ import kotlinx.coroutines.coroutineScope
10
+ import kotlinx.coroutines.launch
11
+ import kotlinx.datetime.Instant
12
+ import kotlinx.datetime.toJavaInstant
13
+ import kotlinx.datetime.toKotlinInstant
14
+ import kotlinx.serialization.SerialName
15
+ import kotlinx.serialization.Serializable
16
+ import mu.KLogging
17
+ import java.time.Duration
18
+ import java.time.ZoneId
19
+ import java.time.ZonedDateTime
20
+
21
+ class AirQualityView (
22
+ private val config : Config ,
23
+ private val database : DatabaseLayer ,
24
+ hardware : Hardware
25
+ ) : FrontDisplayView(
26
+ hardware,
27
+ " Air quality from powietrze.gios.gov.pl" ,
28
+ Duration .ofMinutes(10),
29
+ Duration .ofSeconds(15),
30
+ Duration .ofSeconds(13)
31
+ ) {
32
+
33
+ enum class AirQualityIndex (val text : String ) {
34
+ EXCELLENT (" excellent" ),
35
+ GOOD (" good" ),
36
+ FAIR (" fair" ),
37
+ POOR (" poor" ),
38
+ BAD (" BAD!" ),
39
+ HAZARDOUS (" HAZARDOUS!!" )
40
+ }
41
+
42
+ @Serializable
43
+ data class StIndexLevel (
44
+ @SerialName(" id" )
45
+ val id : Int ,
46
+
47
+ @SerialName(" indexLevelName" )
48
+ val levelName : String
49
+ ) {
50
+ val level: AirQualityIndex ?
51
+ get() = when (id) {
52
+ - 1 -> null
53
+ else -> AirQualityIndex .values()[id]
54
+ }
55
+ }
56
+
57
+ @Serializable
58
+ data class AirQualityDto (
59
+ val id : Long ,
60
+ val stIndexStatus : Boolean ,
61
+ val stIndexCrParam : String ,
62
+ val stIndexLevel : StIndexLevel ,
63
+
64
+ @Serializable(AirQualityInstantSerializer ::class )
65
+ val stCalcDate : Instant ,
66
+
67
+ @Serializable(AirQualityInstantSerializer ::class )
68
+ val stSourceDataDate : Instant
69
+ ) {
70
+ override fun toString (): String {
71
+ return " {$id , ${stIndexLevel.level} , $stIndexCrParam }"
72
+ }
73
+ }
74
+
75
+ data class AirQualityReport (
76
+ val name : String ,
77
+ val level : AirQualityIndex ,
78
+ val latest : Double ,
79
+ val historical : List <Double ?>
80
+ ) {
81
+ init {
82
+ require(name.isNotBlank())
83
+ // todo tests for name
84
+ }
85
+ }
86
+
87
+ data class CurrentReport (
88
+ val station1 : AirQualityReport ? ,
89
+ val station2 : AirQualityReport ?
90
+ ) {
91
+ val updateStatus: UpdateStatus
92
+ get() = if (station1 == null && station2 == null ) {
93
+ UpdateStatus .FAILURE
94
+ } else if (station1 != null && station2 != null ) {
95
+ UpdateStatus .FULL_SUCCESS
96
+ } else {
97
+ UpdateStatus .PARTIAL_SUCCESS
98
+ }
99
+ }
100
+
101
+ private var currentReport: CurrentReport ? = null
102
+
103
+ companion object : KLogging () {
104
+
105
+ private const val HISTORIC_VALUES_LENGTH = 14
106
+
107
+ suspend fun retrieveAirQualityData (stationId : Long ): AirQualityDto {
108
+ require(stationId > 0 )
109
+
110
+ logger.debug(" Downloading currency rates for station {}." , stationId)
111
+ val url = " http://api.gios.gov.pl/pjp-api/rest/aqindex/getIndex/$stationId "
112
+
113
+ val resp: AirQualityDto = httpClient.get(url)
114
+
115
+ check(resp.stIndexStatus) { " no air quality index for station ${resp.id} " }
116
+ checkNotNull(resp.stIndexLevel.level) { " no air quality index for station ${resp.id} " }
117
+ logger.debug(" Air quality is {}." , resp)
118
+
119
+ return resp
120
+ }
121
+ }
122
+
123
+ private suspend fun createStationReport (now : Instant , station : AirStationKey ) = try {
124
+ val dto = retrieveAirQualityData(station.id)
125
+ val dbKey = AirQuality (station.id)
126
+ val value = dto.stIndexLevel.id.toDouble()
127
+ val timestamp = dto.stSourceDataDate.toJavaInstant().atZone(ZoneId .systemDefault())
128
+ database.insertHistoricalValueAsync(timestamp, dbKey, value)
129
+
130
+ val history =
131
+ database.getLastHistoricalValuesByHourAsync(now, dbKey, HISTORIC_VALUES_LENGTH ).await()
132
+
133
+ AirQualityReport (station.name, checkNotNull(dto.stIndexLevel.level), value, history)
134
+ } catch (e: Exception ) {
135
+ logger.error(" Failed to update air quality of station ${station.id} ." , e)
136
+ null
137
+ }
138
+
139
+ override suspend fun poolInstantData (now : ZonedDateTime ) = UpdateStatus .NO_NEW_DATA
140
+
141
+ override suspend fun poolStatusData (now : ZonedDateTime ): UpdateStatus = coroutineScope {
142
+ val instantNow = now.toInstant().toKotlinInstant()
143
+
144
+ val rep1 = async { createStationReport(instantNow, config[AirQualityKey .station1]) }
145
+ val rep2 = async { createStationReport(instantNow, config[AirQualityKey .station2]) }
146
+
147
+ val newRep = CurrentReport (rep1.await(), rep2.await())
148
+
149
+ currentReport = newRep
150
+
151
+ newRep.updateStatus
152
+ }
153
+
154
+ override suspend fun redrawDisplay (redrawStatic : Boolean , redrawStatus : Boolean , now : ZonedDateTime ) =
155
+ coroutineScope {
156
+
157
+ val fd = hardware.frontDisplay
158
+ val rep = currentReport
159
+
160
+ launch {
161
+ fd.setStaticText(0 , " A" )
162
+ fd.setStaticText(20 , " Q" )
163
+ }
164
+
165
+ fun drawForStation (offset : Int , slot : Slot , report : AirQualityReport ? ) {
166
+ report?.let {
167
+ launch { fd.setOneLineDiffChart(5 * (offset + 16 ), it.latest, it.historical, 1.0 ) }
168
+ }
169
+
170
+ launch {
171
+ fd.setScrollingText(
172
+ slot,
173
+ offset + 2 ,
174
+ 13 ,
175
+ report?.let { " ${it.name} : ${it.level.text} " } ? : " no air quality data"
176
+ )
177
+ fd.setStaticText(offset + 19 , report?.level?.ordinal?.toString() ? : " -" )
178
+ }
179
+ }
180
+
181
+ drawForStation(0 , Slot .SLOT0 , rep?.station1)
182
+ drawForStation(20 , Slot .SLOT1 , rep?.station2)
183
+ }
184
+
185
+ }
0 commit comments