From 49b28f239cd8119e0fba2a298bc443f685201cc5 Mon Sep 17 00:00:00 2001 From: Tim Orme Date: Mon, 24 Apr 2023 21:52:26 -0700 Subject: [PATCH] Redesign --- aqimon/aqi_common.py | 22 +++- aqimon/server.py | 17 +++ elm/src/CurrentReads.elm | 84 ++++++++++++++ elm/src/DeviceStatus.elm | 27 ++--- elm/src/EpaCommon.elm | 54 +++++++++ elm/src/EpaLevel.elm | 40 +++++++ elm/src/Graph.elm | 18 +-- elm/src/Main.elm | 234 ++++++++++++++++++++++----------------- 8 files changed, 366 insertions(+), 130 deletions(-) create mode 100644 elm/src/CurrentReads.elm create mode 100644 elm/src/EpaCommon.elm create mode 100644 elm/src/EpaLevel.elm diff --git a/aqimon/aqi_common.py b/aqimon/aqi_common.py index e60823c..6fb98d7 100644 --- a/aqimon/aqi_common.py +++ b/aqimon/aqi_common.py @@ -15,14 +15,24 @@ class Pollutant(Enum): PM_10 = 1 +class EpaLevels(Enum): + """Enum of EPA levels.""" + + GOOD = 0 + MODERATE = 1 + UNHEALTHY_FOR_SENSITIVE = 2 + UNHEALTHY = 3 + VERY_UNHEALTHY = 4 + HAZARDOUS = 5 + + AQI: List[Tuple[int, int]] = [ (0, 50), (51, 100), (101, 150), (151, 200), (201, 300), - (301, 400), - (401, 500), + (301, 500), ] PM_25: List[Tuple[float, float]] = [ (0.0, 12.0), @@ -63,11 +73,11 @@ class EpaAqi: responsible_pollutant: Pollutant -def get_level_from_pm25(pm25: float) -> int: +def get_epa_level(epa_reading: float) -> EpaLevels: """Get the EPA level from a PM25 reading.""" - for i, pair in enumerate(PM_25): - if pair[0] <= pm25 <= pair[1]: - return i + for i, pair in enumerate(AQI): + if pair[0] <= epa_reading <= pair[1]: + return EpaLevels(i) raise ValueError("Invalid PM value") diff --git a/aqimon/server.py b/aqimon/server.py index 5ee37bc..73f6006 100644 --- a/aqimon/server.py +++ b/aqimon/server.py @@ -11,6 +11,8 @@ from .database import ( get_all_reads, get_all_epa_aqis, + get_latest_read, + get_latest_epa_aqi, add_read, add_epa_read, get_averaged_reads, @@ -215,6 +217,21 @@ async def all_data( return all_json +@app.get("/api/latest_data") +async def latest_data( + database: databases.Database = Depends(get_database), +): + """Retrieve most recent reads.""" + latest_reads = await get_latest_read(database) + latest_epa = await get_latest_epa_aqi(database) + return { + "epa": latest_epa.epa_aqi, + "level": aqi_common.get_epa_level(latest_epa.epa_aqi).name, + "pm25": latest_reads.pm25, + "pm10": latest_reads.pm10, + } + + @app.get("/api/status") async def status(reader: ScheduledReader = Depends(get_reader)): """Get the system status.""" diff --git a/elm/src/CurrentReads.elm b/elm/src/CurrentReads.elm new file mode 100644 index 0000000..163fd37 --- /dev/null +++ b/elm/src/CurrentReads.elm @@ -0,0 +1,84 @@ +module CurrentReads exposing (..) + +import Bootstrap.Grid as Grid +import Bootstrap.Grid.Col as Col +import Bootstrap.Grid.Row as Row +import EpaCommon exposing (EpaLevel, getColorForLevel) +import Html exposing (Attribute, Html, a, div, h1, h5, img, text) +import Html.Attributes exposing (alt, class, href, src, style) +import Maybe +import Time exposing (..) + + +{-| Model for device info widget +-} +type alias CurrentReads = + { epaLevel : Maybe EpaLevel + , lastEpaRead : Maybe Float + , lastPm10 : Maybe Float + , lastPm25 : Maybe Float + } + + +{-| HTML widget for displaying the device status. + +Includes general status, time to next read, and exception info if applicable. + +-} +getCurrentReads : CurrentReads -> Html msg +getCurrentReads currentReads = + Grid.container [ style "padding" "1em" ] + [ Grid.row [ Row.attrs [ class "align-items-center" ] ] + [ Grid.col [ Col.attrs [ style "text-align" "center", style "padding" "2em" ] ] + [ h1 + [ style "color" (Maybe.map getColorForLevel currentReads.epaLevel |> Maybe.withDefault "black") + ] + [ text (Maybe.map String.fromFloat currentReads.lastEpaRead |> Maybe.withDefault "NA") ] + ] + ] + , Grid.row [ Row.attrs [ class "align-items-center" ] ] + [ Grid.col [ Col.attrs [ style "text-align" "center", style "padding-bottom" "2em" ] ] [ a [ href "https://www.airnow.gov/aqi/aqi-basics/" ] [ text "EPA AQI Score" ] ] ] + , Grid.row [ Row.attrs [ class "align-items-center" ] ] + [ Grid.col [ Col.attrs [ style "text-align" "right" ] ] [ text "Last PM10:" ] + , Grid.col [ Col.attrs [ style "text-align" "center", style "font-weight" "bold" ] ] [ text (Maybe.map String.fromFloat currentReads.lastPm10 |> Maybe.withDefault "NA") ] + ] + , Grid.row [ Row.attrs [ class "align-items-center" ] ] + [ Grid.col [ Col.attrs [ style "text-align" "right" ] ] [ text "Last PM25:" ] + , Grid.col [ Col.attrs [ style "text-align" "center", style "font-weight" "bold" ] ] [ text (Maybe.map String.fromFloat currentReads.lastPm25 |> Maybe.withDefault "NA") ] + ] + ] + + +{-| Conditionally display some block of HTML +-} +htmlIf : Html msg -> Bool -> Html msg +htmlIf el cond = + if cond then + el + + else + text "" + + +{-| Format a unix timestamp as a string like MM/DD HH:MM:SS +-} +formatDuration : Posix -> Posix -> String +formatDuration currentTime scheduledTime = + let + durationMillis = + posixToMillis scheduledTime - posixToMillis currentTime + + hour = + durationMillis // 1000 // 60 // 60 + + minute = + modBy 60 (durationMillis // 1000 // 60) + + second = + modBy 60 (durationMillis // 1000) + in + if durationMillis > 0 then + String.padLeft 2 '0' (String.fromInt hour) ++ ":" ++ String.padLeft 2 '0' (String.fromInt minute) ++ ":" ++ String.padLeft 2 '0' (String.fromInt second) + + else + "00:00:00" diff --git a/elm/src/DeviceStatus.elm b/elm/src/DeviceStatus.elm index 0d555c9..808e712 100644 --- a/elm/src/DeviceStatus.elm +++ b/elm/src/DeviceStatus.elm @@ -36,22 +36,19 @@ Includes general status, time to next read, and exception info if applicable. getDeviceInfo : DeviceInfo -> Html msg getDeviceInfo deviceInfo = Grid.container [] - [ Grid.row [ Row.attrs [ class "align-items-center" ] ] - [ Grid.col [ Col.attrs [ style "max-width" "64px", style "margin-right" "1em" ] ] [ img [ src (deviceStatusImage deviceInfo.state) ] [] ] - , Grid.col [] - [ Grid.row [] - [ Grid.col [] - [ h5 [ style "color" (deviceStatusColor deviceInfo.state) ] [ text (deviceStatusToString deviceInfo.state) ] ] - ] - , Grid.row [] - [ Grid.col [] [ text (deviceInfo.lastException |> Maybe.withDefault "") ] ] - , htmlIf - (Grid.row [] - [ Grid.col [] [ text ("Next read in: " ++ (Maybe.map2 formatDuration deviceInfo.currentTime deviceInfo.nextSchedule |> Maybe.withDefault "")) ] ] - ) - (shouldShowTimer deviceInfo.state) + [ Grid.row [ Row.attrs [] ] + [ Grid.col [ Col.attrs [ class "text-center", style "padding" "2em" ] ] [ img [ src (deviceStatusImage deviceInfo.state) ] [] ] ] + , Grid.row [] + [ Grid.col [ Col.attrs [ style "text-align" "center" ] ] [ h5 [ style "color" (deviceStatusColor deviceInfo.state) ] [ text (deviceStatusToString deviceInfo.state) ] ] ] + , Grid.row [] + [ Grid.col [ Col.attrs [ style "text-align" "center" ] ] [ text (deviceInfo.lastException |> Maybe.withDefault "") ] ] + , htmlIf + (Grid.row [] + [ Grid.col [ Col.attrs [ style "text-align" "center" ] ] + [ text ("Next read in: " ++ (Maybe.map2 formatDuration deviceInfo.currentTime deviceInfo.nextSchedule |> Maybe.withDefault "")) ] ] - ] + ) + (shouldShowTimer deviceInfo.state) ] diff --git a/elm/src/EpaCommon.elm b/elm/src/EpaCommon.elm new file mode 100644 index 0000000..732ef61 --- /dev/null +++ b/elm/src/EpaCommon.elm @@ -0,0 +1,54 @@ +module EpaCommon exposing (..) + + +type EpaLevel + = Hazardous + | VeryUnhealthy + | Unhealthy + | UnhealthyForSensitive + | Moderate + | Good + + +getColorForLevel : EpaLevel -> String +getColorForLevel level = + case level of + Hazardous -> + "maroon" + + VeryUnhealthy -> + "purple" + + Unhealthy -> + "red" + + UnhealthyForSensitive -> + "orange" + + Moderate -> + "yellow" + + Good -> + "green" + + +getLabelForLevel : EpaLevel -> String +getLabelForLevel level = + case level of + Hazardous -> + "Hazardous" + + VeryUnhealthy -> + "Very Unhealthy" + + Unhealthy -> + "Unhealthy" + + UnhealthyForSensitive -> + "Unhealthy For Sensitive" + + Moderate -> + "Moderate" + + Good -> + "Good" diff --git a/elm/src/EpaLevel.elm b/elm/src/EpaLevel.elm new file mode 100644 index 0000000..5cbb30b --- /dev/null +++ b/elm/src/EpaLevel.elm @@ -0,0 +1,40 @@ +module EpaLevel exposing (..) + +import Bootstrap.Grid as Grid +import Bootstrap.Grid.Col as Col +import Bootstrap.Grid.Row as Row +import EpaCommon exposing (..) +import Html exposing (Attribute, Html, a, div, h1, h5, img, text) +import Html.Attributes exposing (alt, class, href, src, style) + + +{-| HTML widget for displaying the device status. + +Includes general status, time to next read, and exception info if applicable. + +-} +getEpaLevel : Maybe EpaLevel -> Html msg +getEpaLevel currentLevel = + Grid.container [ class "align-middle", style "padding" "1em" ] + [ getRow Hazardous (currentLevel == Just Hazardous) + , getRow VeryUnhealthy (currentLevel == Just VeryUnhealthy) + , getRow Unhealthy (currentLevel == Just Unhealthy) + , getRow UnhealthyForSensitive (currentLevel == Just UnhealthyForSensitive) + , getRow Moderate (currentLevel == Just Moderate) + , getRow Good (currentLevel == Just Good) + ] + + +getRow : EpaLevel -> Bool -> Html msg +getRow level isSelected = + Grid.row [ Row.attrs (List.append [ style "background-color" (getColorForLevel level) ] (selectedStyles isSelected)) ] + [ Grid.col [ Col.attrs [ style "text-align" "center", style "padding" ".25em", style "color" "white" ] ] [ text (getLabelForLevel level) ] ] + + +selectedStyles : Bool -> List (Attribute msg) +selectedStyles isSelected = + if isSelected then + [ style "border-radius" "1em" ] + + else + [ style "margin" "0 0.25em 0 0.25em" ] diff --git a/elm/src/Graph.elm b/elm/src/Graph.elm index db9439a..28313ca 100644 --- a/elm/src/Graph.elm +++ b/elm/src/Graph.elm @@ -50,13 +50,14 @@ Accepts a model of graph data, and an event that occurs on graph hover. getReadChart : GraphReadModel -> (List (CI.One GraphReadData CI.Dot) -> msg) -> Html msg getReadChart graphModel onHover = C.chart - [ CA.height 300 - , CA.width 1000 + [ CA.height 200 + , CA.width 800 + , CA.margin { left = 40, top = 0, right = 20, bottom = 0 } , CE.onMouseMove onHover (CE.getNearest CI.dots) , CE.onMouseLeave (onHover []) ] - [ C.xLabels [ CA.moveDown 25, CA.withGrid, CA.rotate 60, CA.format formatTime ] - , C.yLabels [ CA.withGrid ] + [ C.xLabels [ CA.fontSize 10, CA.withGrid, CA.format formatTime ] + , C.yLabels [ CA.fontSize 10, CA.withGrid ] , C.series .time [ C.interpolated .pm25 [ CA.monotone, CA.color CA.yellow ] [ CA.circle, CA.size 3 ] |> C.named "PM2.5" , C.interpolated .pm10 [ CA.monotone, CA.color CA.red ] [ CA.circle, CA.size 3 ] |> C.named "PM10" @@ -88,13 +89,14 @@ Accepts a model of graph data, and an event that occurs on graph hover. getEpaChart : GraphEpaModel -> (List (CI.One GraphEpaData CI.Dot) -> msg) -> Html msg getEpaChart graphModel onHover = C.chart - [ CA.height 300 - , CA.width 1000 + [ CA.height 200 + , CA.width 800 + , CA.margin { left = 40, top = 0, right = 20, bottom = 0 } , CE.onMouseMove onHover (CE.getNearest CI.dots) , CE.onMouseLeave (onHover []) ] - [ C.xLabels [ CA.moveDown 25, CA.withGrid, CA.rotate 60, CA.format formatTime ] - , C.yLabels [ CA.withGrid ] + [ C.xLabels [ CA.fontSize 10, CA.withGrid, CA.format formatTime ] + , C.yLabels [ CA.fontSize 10, CA.withGrid ] , C.series .time [ C.interpolated .epa [ CA.monotone, CA.color CA.blue ] [ CA.circle, CA.size 3 ] |> C.named "EPA" ] diff --git a/elm/src/Main.elm b/elm/src/Main.elm index 8416cfb..12e5da1 100644 --- a/elm/src/Main.elm +++ b/elm/src/Main.elm @@ -2,16 +2,20 @@ module Main exposing (..) import Bootstrap.Button as Button import Bootstrap.ButtonGroup as ButtonGroup +import Bootstrap.Form.Select as Select import Bootstrap.Grid as Grid import Bootstrap.Grid.Col as Col import Bootstrap.Grid.Row as Row import Bootstrap.Text as Text import Browser import Chart.Item as CI +import CurrentReads as CR exposing (..) import DeviceStatus as DS exposing (..) +import EpaCommon as EC exposing (..) +import EpaLevel as EL exposing (..) import Graph as G exposing (..) import Html exposing (Attribute, Html, div, h1, h2, h5, text) -import Html.Attributes exposing (class, style) +import Html.Attributes exposing (class, style, value) import Http import Json.Decode exposing (Decoder, andThen, fail, field, float, int, list, map2, map3, map4, maybe, string, succeed) import Task @@ -42,14 +46,18 @@ type WindowDuration | Week +type VisibleGraph + = Epa + | ParticleMatter + + {-| Lastest data point -} type alias LatestData = - { readTime : Float - , pm25 : Float - , pm10 : Float - , epaTime : Float - , epa : Float + { pm25 : Maybe Float + , pm10 : Maybe Float + , epa : Maybe Float + , epaLevel : Maybe EpaLevel } @@ -101,6 +109,7 @@ type alias Model = , hoveringReads : List (CI.One ReadData CI.Dot) , hoveringEpas : List (CI.One EpaData CI.Dot) , errorData : ErrorData + , currentGraph : VisibleGraph } @@ -111,7 +120,7 @@ init _ = ( { currentTime = Nothing , lastStatusPoll = Nothing , readerState = { state = Idle, lastException = Nothing, currentTime = Nothing, nextSchedule = Nothing } - , lastReads = { readTime = 0, pm25 = 0.0, pm10 = 0.0, epaTime = 0, epa = 0.0 } + , lastReads = { pm25 = Nothing, pm10 = Nothing, epa = Nothing, epaLevel = Nothing } , allReads = [] , allEpas = [] , windowDuration = Hour @@ -119,8 +128,9 @@ init _ = , hoveringReads = [] , hoveringEpas = [] , errorData = { hasError = False, errorTitle = "", errorMessage = "" } + , currentGraph = Epa } - , Task.perform FetchData Time.now + , Cmd.batch [ Task.perform FetchData Time.now, Task.perform FetchLatest Time.now ] ) @@ -149,6 +159,16 @@ getData windowDuration = } +{-| Get latest read data. +-} +getLatest : Cmd Msg +getLatest = + Http.get + { url = "/api/latest_data" + , expect = Http.expectJson GotLatest latestDataDecoder + } + + getStatus : Cmd Msg getStatus = Http.get @@ -165,13 +185,16 @@ getStatus = -} type Msg = FetchData Posix + | FetchLatest Posix | FetchStatus Posix | GotData (Result Http.Error AllData) + | GotLatest (Result Http.Error LatestData) | GotStatus (Result Http.Error DeviceInfoResponse) | ChangeWindow WindowDuration | OnReadHover (List (CI.One ReadData CI.Dot)) | OnEpaHover (List (CI.One EpaData CI.Dot)) | Tick Posix + | ChangeGraphView String {-| Core update handler. @@ -183,7 +206,7 @@ update msg model = -- On Data received case result of Ok data -> - ( { model | lastReads = getLastListItem data, allReads = data.reads, allEpas = data.epas, errorData = { hasError = False, errorTitle = "", errorMessage = "" } }, Cmd.none ) + ( { model | allReads = data.reads, allEpas = data.epas, errorData = { hasError = False, errorTitle = "", errorMessage = "" } }, Cmd.none ) Err e -> ( { model | errorData = { hasError = True, errorTitle = "Failed to retrieve read data", errorMessage = errorToString e } }, Cmd.none ) @@ -192,6 +215,17 @@ update msg model = -- Data requested ( { model | currentTime = Just newTime }, getData model.windowDuration ) + GotLatest result -> + case result of + Ok data -> + ( { model | lastReads = data, errorData = { hasError = False, errorTitle = "", errorMessage = "" } }, Cmd.none ) + + Err e -> + ( { model | errorData = { hasError = True, errorTitle = "Failed to retrieve latest data", errorMessage = errorToString e } }, Cmd.none ) + + FetchLatest newTime -> + ( { model | currentTime = Just newTime }, getLatest ) + GotStatus result -> case result of Ok data -> @@ -241,6 +275,17 @@ update msg model = in ( { model | currentTime = Just newTime, readerState = updatedReaderState }, cmd ) + ChangeGraphView newView -> + let + newGraph = + if newView == "pm" then + ParticleMatter + + else + Epa + in + ( { model | currentGraph = newGraph }, Cmd.none ) + -- SUBSCRIPTIONS @@ -250,7 +295,7 @@ update msg model = -} subscriptions : Model -> Sub Msg subscriptions model = - Sub.batch [ Time.every 5000 FetchData, Time.every 500 Tick ] + Sub.batch [ Time.every 5000 FetchData, Time.every 5000 FetchLatest, Time.every 500 Tick ] @@ -264,36 +309,44 @@ view : Model -> Html Msg view model = div [] [ Grid.container [ style "margin-bottom" ".5em" ] - [ Grid.row [ Row.attrs [ class "bg-info", style "padding" "1em" ] ] + [ Grid.row [ Row.attrs [ style "padding" "1em" ] ] [ Grid.col [] [ h1 - [ class "text-center" - ] + [] [ text "AQI Monitor" ] ] - , Grid.col [] [ DS.getDeviceInfo model.readerState ] ] - ] - , htmlIf - (Grid.container [] - [ Grid.row [] + , Grid.row [ Row.attrs [ style "padding" "1em" ] ] + [ Grid.col [ Col.attrs [ style "background-color" "#D9D9D9", style "margin" "1em" ] ] + [ CR.getCurrentReads { epaLevel = model.lastReads.epaLevel, lastEpaRead = model.lastReads.epa, lastPm25 = model.lastReads.pm25, lastPm10 = model.lastReads.pm10 } ] + , Grid.col [ Col.attrs [ class "align-items-center", class "d-flex", style "background-color" "#D9D9D9", style "margin" "1em" ] ] + [ EL.getEpaLevel model.lastReads.epaLevel ] + , Grid.col [ Col.attrs [ style "background-color" "#D9D9D9", style "margin" "1em" ] ] + [ DS.getDeviceInfo model.readerState ] + ] + , htmlIf + (Grid.row [] [ Grid.col [ Col.attrs [ class "alert", class "alert-danger" ] ] [ h5 [] [ text model.errorData.errorTitle ] , text model.errorData.errorMessage ] ] - ] - ) - model.errorData.hasError - , Grid.container [] - [ Grid.row [ Row.centerMd ] - [ Grid.col [ Col.lg3 ] [ viewBigNumber model.lastReads.epa "EPA" ] - , Grid.col [ Col.lg3 ] [ viewBigNumber model.lastReads.pm25 "PM2.5" ] - , Grid.col [ Col.lg3 ] [ viewBigNumber model.lastReads.pm10 "PM10" ] - ] - , Grid.row [ Row.attrs [ style "padding-top" "1em", class "justify-content-end" ] ] - [ Grid.col [ Col.lg3 ] - [ ButtonGroup.radioButtonGroup [] + ) + model.errorData.hasError + , Grid.row [ Row.attrs [ style "padding" "1em" ] ] + [ Grid.col [] + [ h2 + [] + [ text "History" ] + ] + , Grid.col [] + [ Select.select [ Select.onChange ChangeGraphView ] + [ Select.item [ value "epa" ] [ text "EPA AQI" ] + , Select.item [ value "pm" ] [ text "Particulate Matter" ] + ] + ] + , Grid.col [ Col.lg3 ] + [ ButtonGroup.radioButtonGroup [ ButtonGroup.attrs [] ] [ getSelector All "All" model.windowDuration , getSelector Hour "Hour" model.windowDuration , getSelector Day "Day" model.windowDuration @@ -301,20 +354,24 @@ view model = ] ] ] - , Grid.row [] [ Grid.col [] [ h2 [] [ text "EPA AQI" ] ] ] - , Grid.row [ Row.attrs [ style "padding-top" "1em", style "padding-bottom" "3em" ], Row.centerMd ] - [ Grid.col [ Col.lg ] - [ div [ style "height" "400px" ] - [ G.getEpaChart { graphData = model.allEpas, currentHover = model.hoveringEpas } OnEpaHover ] + , htmlIf + (Grid.row [ Row.attrs [ style "padding-top" "1em", style "padding-bottom" "3em" ], Row.centerMd ] + [ Grid.col [ Col.lg ] + [ div [ style "height" "400px" ] + [ G.getEpaChart { graphData = model.allEpas, currentHover = model.hoveringEpas } OnEpaHover ] + ] ] - ] - , Grid.row [] [ Grid.col [] [ h2 [] [ text "PM2.5/PM10 Reads" ] ] ] - , Grid.row [ Row.attrs [ style "padding-top" "1em", style "padding-bottom" "3em" ], Row.centerMd ] - [ Grid.col [ Col.lg ] - [ div [ style "height" "400px" ] - [ G.getReadChart { graphData = model.allReads, currentHover = model.hoveringReads } OnReadHover ] + ) + (model.currentGraph == Epa) + , htmlIf + (Grid.row [ Row.attrs [ style "padding-top" "1em", style "padding-bottom" "3em" ], Row.centerMd ] + [ Grid.col [ Col.lg ] + [ div [ style "height" "400px" ] + [ G.getReadChart { graphData = model.allReads, currentHover = model.hoveringReads } OnReadHover ] + ] ] - ] + ) + (model.currentGraph == ParticleMatter) ] ] @@ -325,42 +382,10 @@ getSelector : WindowDuration -> String -> WindowDuration -> ButtonGroup.RadioBut getSelector windowDuration textDuration currentDuration = ButtonGroup.radioButton (windowDuration == currentDuration) - [ Button.outlinePrimary, Button.onClick <| ChangeWindow windowDuration ] + [ Button.outlineDark, Button.onClick <| ChangeWindow windowDuration ] [ text textDuration ] -{-| Get a "big number" view for the headline. --} -viewBigNumber : Float -> String -> Html Msg -viewBigNumber value numberType = - Grid.container [ style "background-clip" "border-box", style "border" "1px solid darkgray", style "padding" "0", style "border-radius" ".25rem" ] - [ Grid.row [] - [ Grid.col - [ Col.textAlign Text.alignMdCenter ] - [ h1 - [ style "padding" ".5em" - , style "margin" "0" - , style "color" "white" - , style "background-color" "lightblue" - ] - [ text (String.fromFloat value) ] - ] - ] - , Grid.row [] - [ Grid.col - [ Col.textAlign Text.alignMdCenter ] - [ h5 - [ style "padding" ".25em" - , style "margin" "0" - , style "color" "darkblue" - , style "background-color" "lightgray" - ] - [ text numberType ] - ] - ] - ] - - {-| Decoder function for JSON read data -} readDataDecoder : Decoder (List ReadData) @@ -391,6 +416,15 @@ allDataDecoder = (field "epas" epaDataDecoder) +latestDataDecoder : Decoder LatestData +latestDataDecoder = + map4 LatestData + (maybe (field "pm25" float)) + (maybe (field "pm10" float)) + (maybe (field "epa" float)) + (maybe (field "level" epaLevelDecoder)) + + type alias DeviceInfoResponse = { readerStatus : DS.DeviceState , readerException : Maybe String @@ -433,37 +467,35 @@ stateDecoder = ) -{-| Given a list of read data, retrieve the last item from that list. -Useful for grabbing the most recent read from the device. -If the list is empty, a read with all 0 values is returned. +{-| JSON decoder to convert a device state to its type. +-} +epaLevelDecoder : Decoder EpaLevel +epaLevelDecoder = + string + |> andThen + (\str -> + case str of + "HAZARDOUS" -> + succeed Hazardous + + "VERY_UNHEALTHY" -> + succeed VeryUnhealthy -getLastListItem [ -{time = 1, epa = 1, pm25 = 1, pm 10 = 1}, -{time = 2, epa = 2, pm25 = 2, pm 10 = 2}, -{time = 3, epa = 3, pm25 = 3, pm 10 = 3}, -] = [{time = 3, epa = 3, pm25 = 3, pm 10 = 3}] + "UNHEALTHY" -> + succeed Unhealthy --} -getLastListItem : AllData -> LatestData -getLastListItem allData = - let - lastReads = - case List.head (List.reverse allData.reads) of - Just a -> - a + "UNHEALTHY_FOR_SENSITIVE" -> + succeed UnhealthyForSensitive - Nothing -> - { time = 0, pm25 = 0, pm10 = 0 } + "MODERATE" -> + succeed Moderate - lastEpas = - case List.head (List.reverse allData.epas) of - Just a -> - a + "GOOD" -> + succeed Good - Nothing -> - { time = 0, epa = 0 } - in - { readTime = lastReads.time, pm25 = lastReads.pm25, pm10 = lastReads.pm10, epaTime = lastEpas.time, epa = lastEpas.epa } + _ -> + fail "Invalid Epa Level" + ) {-| Convert HTTP error to a string.