Skip to content

Commit

Permalink
Update graphs
Browse files Browse the repository at this point in the history
  • Loading branch information
TimOrme committed Apr 16, 2023
1 parent ad2b8e2 commit cc21868
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 43 deletions.
27 changes: 21 additions & 6 deletions aqimon/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@
import databases
from pathlib import Path
from datetime import datetime, timedelta
from .database import get_all_reads, add_read, add_epa_read, get_averaged_reads, create_tables, clean_old, ReadLogEntry
from .database import (
get_all_reads,
get_all_epa_aqis,
add_read,
add_epa_read,
get_averaged_reads,
create_tables,
clean_old,
ReadLogEntry,
EpaAqiLogEntry,
)
from .read import AqiRead, Reader
from .read.mock import MockReader
from .read.novapm import NovaPmReader
Expand Down Expand Up @@ -164,9 +174,12 @@ async def read_function() -> None:
await repeater(read_function)()


def convert_all_to_view_dict(results: List[ReadLogEntry]):
def convert_all_to_view_dict(reads: List[ReadLogEntry], epas: List[EpaAqiLogEntry]):
"""Convert data result to dictionary for view."""
view = [{"t": int(x.event_time.timestamp()), "epa": 123.0, "pm25": x.pm25, "pm10": x.pm10} for x in results]
view = {
"reads": [{"t": int(x.event_time.timestamp()), "pm25": x.pm25, "pm10": x.pm10} for x in reads],
"epas": [{"t": int(x.event_time.timestamp()), "epa": x.epa_aqi} for x in epas],
}
return view


Expand All @@ -190,10 +203,12 @@ async def all_data(
elif window == "week":
window_delta = timedelta(weeks=1)
if window_delta:
all_stats = await get_all_reads(database, datetime.now() - window_delta)
all_reads = await get_all_reads(database, datetime.now() - window_delta)
all_epas = await get_all_epa_aqis(database, datetime.now() - window_delta)
else:
all_stats = await get_all_reads(database, None)
all_json = convert_all_to_view_dict(all_stats)
all_reads = await get_all_reads(database, None)
all_epas = await get_all_epa_aqis(database, None)
all_json = convert_all_to_view_dict(all_reads, all_epas)
return all_json


Expand Down
71 changes: 61 additions & 10 deletions elm/src/Graph.elm
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,46 @@ import Html.Attributes exposing (style)
import Time exposing (..)


{-| A graph model, including all data for the graph, as well as the current hover state.
{-| A graph model, including all data for the read graph, as well as the current hover state.
-}
type alias GraphModel =
{ graphData : List GraphData
, currentHover : List (CI.One GraphData CI.Dot)
type alias GraphReadModel =
{ graphData : List GraphReadData
, currentHover : List (CI.One GraphReadData CI.Dot)
}


{-| A graph model, including all data for the EPA graph, as well as the current hover state.
-}
type alias GraphEpaModel =
{ graphData : List GraphEpaData
, currentHover : List (CI.One GraphEpaData CI.Dot)
}


{-| Graph data, representing a point on the graph.
-}
type alias GraphData =
type alias GraphReadData =
{ time : Float
, epa : Float
, pm25 : Float
, pm10 : Float
}


{-| Graph data, representing a point on the graph.
-}
type alias GraphEpaData =
{ time : Float
, epa : Float
}


{-| Get a chart of read data.
Accepts a model of graph data, and an event that occurs on graph hover.
-}
getChart : GraphModel -> (List (CI.One GraphData CI.Dot) -> msg) -> Html msg
getChart graphModel onHover =
getReadChart : GraphReadModel -> (List (CI.One GraphReadData CI.Dot) -> msg) -> Html msg
getReadChart graphModel onHover =
C.chart
[ CA.height 300
, CA.width 1000
Expand All @@ -43,8 +58,7 @@ getChart graphModel onHover =
[ C.xLabels [ CA.moveDown 25, CA.withGrid, CA.rotate 60, CA.format formatTime ]
, C.yLabels [ CA.withGrid ]
, C.series .time
[ C.interpolated .epa [ CA.monotone, CA.color CA.blue ] [ CA.circle, CA.size 3 ] |> C.named "EPA"
, C.interpolated .pm25 [ CA.monotone, CA.color CA.yellow ] [ CA.circle, CA.size 3 ] |> C.named "PM2.5"
[ 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"
]
graphModel.graphData
Expand All @@ -66,6 +80,43 @@ getChart graphModel onHover =
]


{-| Get a chart of read data.
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
, 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.series .time
[ C.interpolated .epa [ CA.monotone, CA.color CA.blue ] [ CA.circle, CA.size 3 ] |> C.named "EPA"
]
graphModel.graphData
, C.each graphModel.currentHover <|
\p item ->
[ C.tooltip item [] [] [] ]
, C.legendsAt .min
.max
[ CA.row -- Appear as column instead of row
, CA.alignLeft -- Anchor legends to the right
, CA.spacing 5 -- Spacing between legends
, CA.background "Azure" -- Color background
, CA.border "gray" -- Add border
, CA.borderWidth 1 -- Set border width
, CA.htmlAttrs [ style "padding" "0px 4px" ]
]
[ CA.fontSize 12 -- Change font size
]
]


{-| Format a unix timestamp as a string like MM/DD HH:MM:SS
-}
formatTime : Float -> String
Expand Down
127 changes: 100 additions & 27 deletions elm/src/Main.elm
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import Browser
import Chart.Item as CI
import DeviceStatus as DS exposing (..)
import Graph as G exposing (..)
import Html exposing (Attribute, Html, div, h1, h5, text)
import Html exposing (Attribute, Html, div, h1, h2, h5, text)
import Html.Attributes exposing (class, style)
import Http
import Json.Decode exposing (Decoder, andThen, fail, field, float, int, list, map3, map4, maybe, string, succeed)
import Json.Decode exposing (Decoder, andThen, fail, field, float, int, list, map2, map3, map4, maybe, string, succeed)
import Task
import Time exposing (..)

Expand Down Expand Up @@ -42,16 +42,42 @@ type WindowDuration
| Week


{-| Lastest data point
-}
type alias LatestData =
{ readTime : Float
, pm25 : Float
, pm10 : Float
, epaTime : Float
, epa : Float
}


{-| Read data from the device
-}
type alias ReadData =
{ time : Float
, epa : Float
, pm25 : Float
, pm10 : Float
}


{-| EPA AQI data
-}
type alias EpaData =
{ time : Float
, epa : Float
}


{-| All data wrapper
-}
type alias AllData =
{ reads : List ReadData
, epas : List EpaData
}


{-| Hard error model. In cases where we have unexpected failures.
-}
type alias ErrorData =
Expand All @@ -67,11 +93,13 @@ type alias Model =
{ currentTime : Maybe Posix
, lastStatusPoll : Maybe Posix
, readerState : DeviceInfo
, lastReads : ReadData
, lastReads : LatestData
, allReads : List ReadData
, allEpas : List EpaData
, windowDuration : WindowDuration
, dataLoading : Bool
, hovering : List (CI.One ReadData CI.Dot)
, hoveringReads : List (CI.One ReadData CI.Dot)
, hoveringEpas : List (CI.One EpaData CI.Dot)
, errorData : ErrorData
}

Expand All @@ -83,11 +111,13 @@ init _ =
( { currentTime = Nothing
, lastStatusPoll = Nothing
, readerState = { state = Idle, lastException = Nothing, currentTime = Nothing, nextSchedule = Nothing }
, lastReads = { time = 0, epa = 0, pm25 = 0.0, pm10 = 0.0 }
, lastReads = { readTime = 0, pm25 = 0.0, pm10 = 0.0, epaTime = 0, epa = 0.0 }
, allReads = []
, allEpas = []
, windowDuration = Hour
, dataLoading = True
, hovering = []
, hoveringReads = []
, hoveringEpas = []
, errorData = { hasError = False, errorTitle = "", errorMessage = "" }
}
, Task.perform FetchData Time.now
Expand Down Expand Up @@ -115,7 +145,7 @@ getData windowDuration =
in
Http.get
{ url = "/api/sensor_data?window=" ++ stringDuration
, expect = Http.expectJson GotData dataDecoder
, expect = Http.expectJson GotData allDataDecoder
}


Expand All @@ -136,10 +166,11 @@ getStatus =
type Msg
= FetchData Posix
| FetchStatus Posix
| GotData (Result Http.Error (List ReadData))
| GotData (Result Http.Error AllData)
| GotStatus (Result Http.Error DeviceInfoResponse)
| ChangeWindow WindowDuration
| OnHover (List (CI.One ReadData CI.Dot))
| OnReadHover (List (CI.One ReadData CI.Dot))
| OnEpaHover (List (CI.One EpaData CI.Dot))
| Tick Posix


Expand All @@ -152,7 +183,7 @@ update msg model =
-- On Data received
case result of
Ok data ->
( { model | lastReads = getLastListItem data, allReads = data, errorData = { hasError = False, errorTitle = "", errorMessage = "" } }, Cmd.none )
( { model | lastReads = getLastListItem data, 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 )
Expand Down Expand Up @@ -185,8 +216,13 @@ update msg model =
-- Window duration changed
( { model | windowDuration = window }, Task.perform FetchData Time.now )

OnHover hovering ->
( { model | hovering = hovering }, Cmd.none )
OnReadHover hovering ->
-- Hover over a datapoint on the read graph
( { model | hoveringReads = hovering }, Cmd.none )

OnEpaHover hovering ->
-- Hover over a datapoint on the EPA graph
( { model | hoveringEpas = hovering }, Cmd.none )

Tick newTime ->
let
Expand Down Expand Up @@ -265,10 +301,18 @@ view model =
]
]
]
, Grid.row [ Row.attrs [ style "padding-top" "1em" ], Row.centerMd ]
, 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.getChart { graphData = model.allReads, currentHover = model.hovering } OnHover ]
[ 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 ]
]
]
]
Expand Down Expand Up @@ -319,17 +363,34 @@ viewBigNumber value numberType =

{-| Decoder function for JSON read data
-}
dataDecoder : Decoder (List ReadData)
dataDecoder =
readDataDecoder : Decoder (List ReadData)
readDataDecoder =
list
(map4 ReadData
(map3 ReadData
(field "t" float)
(field "epa" float)
(field "pm25" float)
(field "pm10" float)
)


{-| Decoder function for JSON epa data
-}
epaDataDecoder : Decoder (List EpaData)
epaDataDecoder =
list
(map2 EpaData
(field "t" float)
(field "epa" float)
)


allDataDecoder : Decoder AllData
allDataDecoder =
map2 AllData
(field "reads" readDataDecoder)
(field "epas" epaDataDecoder)


type alias DeviceInfoResponse =
{ readerStatus : DS.DeviceState
, readerException : Maybe String
Expand Down Expand Up @@ -380,14 +441,26 @@ getLastListItem [
] = [{time = 3, epa = 3, pm25 = 3, pm 10 = 3}]
-}
getLastListItem : List ReadData -> ReadData
getLastListItem myList =
case List.head (List.reverse myList) of
Just a ->
a

Nothing ->
{ time = 0, epa = 0, pm25 = 0, pm10 = 0 }
getLastListItem : AllData -> LatestData
getLastListItem allData =
let
lastReads =
case List.head (List.reverse allData.reads) of
Just a ->
a

Nothing ->
{ time = 0, pm25 = 0, pm10 = 0 }

lastEpas =
case List.head (List.reverse allData.epas) of
Just a ->
a

Nothing ->
{ time = 0, epa = 0 }
in
{ readTime = lastReads.time, pm25 = lastReads.pm25, pm10 = lastReads.pm10, epaTime = lastEpas.time, epa = lastEpas.epa }


{-| Convert HTTP error to a string.
Expand Down

0 comments on commit cc21868

Please sign in to comment.