Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesign #36

Merged
merged 1 commit into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions aqimon/aqi_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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")


Expand Down
17 changes: 17 additions & 0 deletions aqimon/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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."""
Expand Down
84 changes: 84 additions & 0 deletions elm/src/CurrentReads.elm
Original file line number Diff line number Diff line change
@@ -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"
27 changes: 12 additions & 15 deletions elm/src/DeviceStatus.elm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]


Expand Down
54 changes: 54 additions & 0 deletions elm/src/EpaCommon.elm
Original file line number Diff line number Diff line change
@@ -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"
40 changes: 40 additions & 0 deletions elm/src/EpaLevel.elm
Original file line number Diff line number Diff line change
@@ -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" ]
18 changes: 10 additions & 8 deletions elm/src/Graph.elm
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
]
Expand Down
Loading