Add pollen indicator
This commit is contained in:
parent
0381b8d14c
commit
426d5b4453
9 changed files with 463 additions and 157 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -8,10 +8,10 @@ authors = ["Felix Suchert <dev@felixsuchert.de>"]
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
embedded-graphics = "0.7.1"
|
embedded-graphics = "0.8"
|
||||||
embedded-plots = "0.2.0"
|
embedded-plots = "0.2"
|
||||||
tinybmp = "0.3.3"
|
tinybmp = "0.5"
|
||||||
profont = "0.5.0"
|
profont = "0.6"
|
||||||
ssd1675 = { git = "https://github.com/Feliix42/ssd1675" }
|
ssd1675 = { git = "https://github.com/Feliix42/ssd1675" }
|
||||||
#ssd1675 = { path = "../ssd1675" }
|
#ssd1675 = { path = "../ssd1675" }
|
||||||
|
|
||||||
|
@ -21,9 +21,11 @@ serde_json = "1.0"
|
||||||
|
|
||||||
# for retrieving weather data from the API
|
# for retrieving weather data from the API
|
||||||
ureq = { version = "2.6", features = ["json"] }
|
ureq = { version = "2.6", features = ["json"] }
|
||||||
|
# for parsing timestamps from APIs
|
||||||
|
time = { version = "0.3", features = ["macros", "parsing", "local-offset"] }
|
||||||
|
|
||||||
linux-embedded-hal = { version = "0.3.2", optional = true }
|
linux-embedded-hal = { version = "0.3.2", optional = true }
|
||||||
embedded-graphics-simulator = { version = "0.3.0", optional = true }
|
embedded-graphics-simulator = { version = "0.5", optional = true }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use embedded_graphics::prelude::*;
|
use embedded_graphics::prelude::*;
|
||||||
use embedded_graphics_simulator::{OutputSettings, SimulatorDisplay, Window};
|
use embedded_graphics_simulator::{OutputSettings, SimulatorDisplay, Window};
|
||||||
use ssd1675::Color;
|
use ssd1675::Color;
|
||||||
|
use inky_ticker::ApplicationState;
|
||||||
|
|
||||||
fn main() -> Result<(), core::convert::Infallible> {
|
fn main() -> Result<(), core::convert::Infallible> {
|
||||||
println!("Initialize Display");
|
println!("Initialize Display");
|
||||||
|
@ -9,18 +10,25 @@ fn main() -> Result<(), core::convert::Infallible> {
|
||||||
println!("Creating Window");
|
println!("Creating Window");
|
||||||
let mut window = Window::new("Hello World", &OutputSettings::default());
|
let mut window = Window::new("Hello World", &OutputSettings::default());
|
||||||
|
|
||||||
//loop {
|
let mut state = ApplicationState::default();
|
||||||
//println!("Populate display...");
|
|
||||||
//inky_ticker::populate(&mut display);
|
|
||||||
|
|
||||||
//println!("Updating window");
|
// switch between states every few minutes
|
||||||
//window.update(&display);
|
let minute = std::time::Duration::from_secs(60);
|
||||||
|
|
||||||
//std::thread::sleep(std::time::Duration::from_secs(1));
|
loop {
|
||||||
//}
|
display.clear(Color::White).expect("failed to clear screen");
|
||||||
|
|
||||||
inky_ticker::populate(&mut display);
|
println!("Populating screen");
|
||||||
window.show_static(&display);
|
|
||||||
|
|
||||||
Ok(())
|
inky_ticker::populate(&mut display, &mut state);
|
||||||
|
|
||||||
|
println!("Updating window");
|
||||||
|
window.update(&display);
|
||||||
|
|
||||||
|
|
||||||
|
println!("Going to sleep");
|
||||||
|
|
||||||
|
state.advance_screen();
|
||||||
|
std::thread::sleep(minute);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
38
src/error.rs
Normal file
38
src/error.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use embedded_graphics::{
|
||||||
|
draw_target::DrawTarget,
|
||||||
|
mono_font::MonoTextStyle,
|
||||||
|
prelude::*,
|
||||||
|
text::{Alignment, Baseline, Text},
|
||||||
|
};
|
||||||
|
use profont::{PROFONT_12_POINT, PROFONT_24_POINT};
|
||||||
|
use ssd1675::Color;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
pub fn display_no_data<D, E>(display: &mut D, err: E)
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Color>,
|
||||||
|
D::Error: Debug,
|
||||||
|
E: Error,
|
||||||
|
{
|
||||||
|
Text::with_alignment(
|
||||||
|
"KEINE DATEN",
|
||||||
|
Point::new(106, 28),
|
||||||
|
MonoTextStyle::new(&PROFONT_24_POINT, Color::Red),
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(display)
|
||||||
|
.expect("error drawing text");
|
||||||
|
|
||||||
|
eprintln!("[error] Failed to get/parse JSON:\n{}", err);
|
||||||
|
|
||||||
|
// TODO(feliix42): As soon as there is a custom text renderer for full texts, use it here!
|
||||||
|
Text::with_baseline(
|
||||||
|
&err.to_string(),
|
||||||
|
Point::new(5, 32),
|
||||||
|
MonoTextStyle::new(&PROFONT_12_POINT, Color::Black),
|
||||||
|
Baseline::Top,
|
||||||
|
)
|
||||||
|
.draw(display)
|
||||||
|
.expect("error drawing text");
|
||||||
|
}
|
149
src/lib.rs
149
src/lib.rs
|
@ -1,121 +1,50 @@
|
||||||
use embedded_graphics::{
|
use embedded_graphics::draw_target::DrawTarget;
|
||||||
draw_target::DrawTarget, image::Image, mono_font::MonoTextStyle, prelude::*, text::Text,
|
|
||||||
};
|
|
||||||
use profont::{PROFONT_12_POINT, PROFONT_14_POINT, PROFONT_24_POINT, PROFONT_9_POINT};
|
|
||||||
use ssd1675::Color;
|
use ssd1675::Color;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::process::Command;
|
|
||||||
use std::{fs, io};
|
|
||||||
use tinybmp::Bmp;
|
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod pollen;
|
||||||
mod weather;
|
mod weather;
|
||||||
|
|
||||||
pub fn populate<D>(display: &mut D)
|
/// Represents the different screens that can be shown on the ticker display.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Screen {
|
||||||
|
Weather,
|
||||||
|
PollenRadar,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApplicationState {
|
||||||
|
/// Current copy of the pollen data, if any is available.
|
||||||
|
pub pollen_data: Option<pollen::data::PollenData>,
|
||||||
|
/// Currently shown screen
|
||||||
|
pub current_screen: Screen,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ApplicationState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
pollen_data: None,
|
||||||
|
current_screen: Screen::Weather,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationState {
|
||||||
|
pub fn advance_screen(&mut self) {
|
||||||
|
self.current_screen = match self.current_screen {
|
||||||
|
Screen::Weather => Screen::PollenRadar,
|
||||||
|
Screen::PollenRadar => Screen::Weather,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn populate<D>(display: &mut D, state: &mut ApplicationState)
|
||||||
where
|
where
|
||||||
D: DrawTarget<Color = Color>,
|
D: DrawTarget<Color = Color>,
|
||||||
D::Error: Debug,
|
D::Error: Debug,
|
||||||
{
|
{
|
||||||
weather::get_weather(display)
|
match state.current_screen {
|
||||||
|
Screen::Weather => weather::get_weather(display),
|
||||||
|
Screen::PollenRadar => pollen::get_pollen(display, state),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(feliix42): Remove dead code here
|
|
||||||
|
|
||||||
pub fn populate_default<D>(display: &mut D)
|
|
||||||
where
|
|
||||||
D: DrawTarget<Color = Color>,
|
|
||||||
D::Error: Debug,
|
|
||||||
{
|
|
||||||
Text::new(
|
|
||||||
"Raspberry Pi",
|
|
||||||
Point::new(1, 20),
|
|
||||||
MonoTextStyle::new(&PROFONT_24_POINT, Color::Red),
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("error drawing text");
|
|
||||||
|
|
||||||
if let Ok(cpu_temp) = read_cpu_temp() {
|
|
||||||
Text::new(
|
|
||||||
"CPU Temp:",
|
|
||||||
Point::new(1, 40),
|
|
||||||
MonoTextStyle::new(&PROFONT_14_POINT, Color::Black),
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("error drawing text");
|
|
||||||
Text::new(
|
|
||||||
&format!("{:.1}°C", cpu_temp),
|
|
||||||
Point::new(95, 40),
|
|
||||||
MonoTextStyle::new(&PROFONT_12_POINT, Color::Red),
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("error drawing text");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(uptime) = read_uptime() {
|
|
||||||
Text::new(
|
|
||||||
uptime.trim(),
|
|
||||||
Point::new(1, 93),
|
|
||||||
MonoTextStyle::new(&PROFONT_9_POINT, Color::Black),
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("error drawing text");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(uname) = read_uname() {
|
|
||||||
Text::new(
|
|
||||||
uname.trim(),
|
|
||||||
Point::new(1, 84),
|
|
||||||
MonoTextStyle::new(&PROFONT_9_POINT, Color::Black),
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("error drawing text");
|
|
||||||
}
|
|
||||||
|
|
||||||
let bmp_data = include_bytes!("../assets/rain.bmp");
|
|
||||||
|
|
||||||
// Load 16 BPP 8x8px image.
|
|
||||||
// Note: The color type is specified explicitly to match the format used by the BMP image.
|
|
||||||
let bmp = Bmp::<Color>::from_slice(bmp_data).unwrap();
|
|
||||||
//let bmp: Bmp<Color> = bmp.into();
|
|
||||||
|
|
||||||
// Draw the image with the top left corner at (10, 20) by wrapping it in
|
|
||||||
// an embedded-graphics `Image`.
|
|
||||||
Image::new(&bmp, Point::new(10, 20))
|
|
||||||
.draw(display)
|
|
||||||
.expect("Failed to draw image");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_cpu_temp() -> Result<f64, io::Error> {
|
|
||||||
fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")?
|
|
||||||
.trim()
|
|
||||||
.parse::<i32>()
|
|
||||||
.map(|temp| temp as f64 / 1000.)
|
|
||||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_uptime() -> Option<String> {
|
|
||||||
Command::new("uptime")
|
|
||||||
.arg("-p")
|
|
||||||
.output()
|
|
||||||
.ok()
|
|
||||||
.and_then(|output| {
|
|
||||||
if output.status.success() {
|
|
||||||
String::from_utf8(output.stdout).ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_uname() -> Option<String> {
|
|
||||||
Command::new("uname")
|
|
||||||
.arg("-smr")
|
|
||||||
.output()
|
|
||||||
.ok()
|
|
||||||
.and_then(|output| {
|
|
||||||
if output.status.success() {
|
|
||||||
String::from_utf8(output.stdout).ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
7
src/pollen/api.rs
Normal file
7
src/pollen/api.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
use super::data::RawPollenData;
|
||||||
|
|
||||||
|
pub fn get_pollen_data() -> Result<RawPollenData, ureq::Error> {
|
||||||
|
let api_url = "https://opendata.dwd.de/climate_environment/health/alerts/s31fg.json";
|
||||||
|
|
||||||
|
Ok(ureq::get(api_url).call()?.into_json()?)
|
||||||
|
}
|
204
src/pollen/data.rs
Normal file
204
src/pollen/data.rs
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
//! Rust data types representing the return values of the [DWD Pollen
|
||||||
|
//! API](https://opendata.dwd.de/climate_environment/health/alerts/Beschreibung_pollen_s31fg.pdf).
|
||||||
|
//!
|
||||||
|
//! Individual region keys and and other data is explained in the OpenData portal documentation
|
||||||
|
//! linked above.
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use time::{macros::format_description, OffsetDateTime, PrimitiveDateTime, UtcOffset};
|
||||||
|
|
||||||
|
// TODO(feliix42): Transparent error handling: remove `unwrap`!
|
||||||
|
|
||||||
|
/// Raw Response Data from the DWD Pollen API with both current Pollen data and forecast information for the
|
||||||
|
/// next two days.
|
||||||
|
///
|
||||||
|
/// Not intended to be used as is. It must be converted to `PollenData` first using
|
||||||
|
/// [PollenData::from].
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RawPollenData {
|
||||||
|
// meta fields
|
||||||
|
/// Name of the Data retrieved (i.e., the title of the forecast)
|
||||||
|
//pub name: String,
|
||||||
|
/// Creator of the data
|
||||||
|
//pub sender: String,
|
||||||
|
// TODO(feliix42): Write a custom parser stripping the `Uhr` string at the end.
|
||||||
|
/// Date and time when this report was created
|
||||||
|
last_update: String,
|
||||||
|
/// Next update to the data
|
||||||
|
next_update: String,
|
||||||
|
///// Explanations for the individual hazard levels
|
||||||
|
//pub legend: HashMap<String, String>,
|
||||||
|
|
||||||
|
// actual data
|
||||||
|
/// A list of all data for Germany
|
||||||
|
content: Vec<RawRegionPollenData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RawRegionPollenData {
|
||||||
|
pub region_name: String,
|
||||||
|
#[serde(rename = "partregion_name")]
|
||||||
|
pub part_of_region: String,
|
||||||
|
pub region_id: u32,
|
||||||
|
#[serde(rename = "partregion_id")]
|
||||||
|
pub part_of_region_id: i32,
|
||||||
|
#[serde(rename = "Pollen")]
|
||||||
|
pub pollen: HashMap<Allergene, RawAllergenDetails>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RawAllergenDetails {
|
||||||
|
pub today: String,
|
||||||
|
pub tomorrow: String,
|
||||||
|
pub dayafter_to: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum ParseError {
|
||||||
|
// UnknownAllergen,
|
||||||
|
UnknownIntensity,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response from the DWD Pollen API with both current Pollen data and forecast information for the
|
||||||
|
/// next two days.
|
||||||
|
pub struct PollenData {
|
||||||
|
/// Date and time when this report was created
|
||||||
|
pub last_update: OffsetDateTime,
|
||||||
|
/// Next update to the data
|
||||||
|
pub next_update: OffsetDateTime,
|
||||||
|
|
||||||
|
/// A list of all data for Germany
|
||||||
|
pub content: Vec<RegionPollenData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PollenData {
|
||||||
|
pub fn get_region_by_ids<'a>(&'a self, id: u32, part: i32) -> Option<&'a RegionPollenData> {
|
||||||
|
self.content.iter().find(|&x| x.region_id == id && x.part_of_region_id == part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RawPollenData> for PollenData {
|
||||||
|
fn from(value: RawPollenData) -> Self {
|
||||||
|
let format = format_description!("[year]-[month]-[day] [hour]:[minute] Uhr");
|
||||||
|
|
||||||
|
// we assume the DWD server has the same time zone offset as our Pi.
|
||||||
|
let local_offset = UtcOffset::current_local_offset().unwrap();
|
||||||
|
|
||||||
|
let last = PrimitiveDateTime::parse(&value.last_update, format)
|
||||||
|
.unwrap()
|
||||||
|
.assume_offset(local_offset);
|
||||||
|
let next = PrimitiveDateTime::parse(&value.next_update, format)
|
||||||
|
.unwrap()
|
||||||
|
.assume_offset(local_offset);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
last_update: last,
|
||||||
|
next_update: next,
|
||||||
|
|
||||||
|
content: value
|
||||||
|
.content
|
||||||
|
.into_iter()
|
||||||
|
.map(RegionPollenData::from)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RegionPollenData {
|
||||||
|
pub region_name: String,
|
||||||
|
pub part_of_region: String,
|
||||||
|
pub region_id: u32,
|
||||||
|
pub part_of_region_id: i32,
|
||||||
|
pub pollen: HashMap<Allergene, AllergenDetails>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RawRegionPollenData> for RegionPollenData {
|
||||||
|
fn from(value: RawRegionPollenData) -> Self {
|
||||||
|
Self {
|
||||||
|
region_name: value.region_name,
|
||||||
|
part_of_region: value.part_of_region,
|
||||||
|
region_id: value.region_id,
|
||||||
|
part_of_region_id: value.part_of_region_id,
|
||||||
|
pollen: value
|
||||||
|
.pollen
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, AllergenDetails::from(v)))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Details about a specific allergen
|
||||||
|
pub struct AllergenDetails {
|
||||||
|
pub today: Intensity,
|
||||||
|
pub tomorrow: Intensity,
|
||||||
|
pub dayafter_to: Intensity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RawAllergenDetails> for AllergenDetails {
|
||||||
|
fn from(value: RawAllergenDetails) -> Self {
|
||||||
|
Self {
|
||||||
|
today: Intensity::from_str(&value.today).unwrap(),
|
||||||
|
tomorrow: Intensity::from_str(&value.tomorrow).unwrap(),
|
||||||
|
dayafter_to: Intensity::from_str(&value.dayafter_to).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Individual allergenes
|
||||||
|
#[derive(Deserialize, Eq, Hash, PartialEq)]
|
||||||
|
pub enum Allergene {
|
||||||
|
Ambrosia,
|
||||||
|
Roggen,
|
||||||
|
Birke,
|
||||||
|
Erle,
|
||||||
|
Beifuss,
|
||||||
|
Esche,
|
||||||
|
Graeser,
|
||||||
|
Hasel,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Intensity of an allergen
|
||||||
|
#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
|
pub enum Intensity {
|
||||||
|
Keine,
|
||||||
|
KeinBisGering,
|
||||||
|
Gering,
|
||||||
|
GeringBisMittel,
|
||||||
|
Mittel,
|
||||||
|
MittelBisHoch,
|
||||||
|
Hoch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Intensity {
|
||||||
|
pub fn as_fraction(&self) -> f32 {
|
||||||
|
match *self {
|
||||||
|
Intensity::Keine => 0_f32,
|
||||||
|
Intensity::KeinBisGering => 0.1666,
|
||||||
|
Intensity::Gering => 0.3333,
|
||||||
|
Intensity::GeringBisMittel => 0.5,
|
||||||
|
Intensity::Mittel => 0.6666,
|
||||||
|
Intensity::MittelBisHoch => 0.8333,
|
||||||
|
Intensity::Hoch => 1_f32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Intensity {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"0" => Ok(Intensity::Keine),
|
||||||
|
"0-1" => Ok(Intensity::KeinBisGering),
|
||||||
|
"1" => Ok(Intensity::Gering),
|
||||||
|
"1-2" => Ok(Intensity::GeringBisMittel),
|
||||||
|
"2" => Ok(Intensity::Mittel),
|
||||||
|
"2-3" => Ok(Intensity::MittelBisHoch),
|
||||||
|
"3" => Ok(Intensity::Hoch),
|
||||||
|
_ => Err(ParseError::UnknownIntensity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
145
src/pollen/mod.rs
Normal file
145
src/pollen/mod.rs
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
use embedded_graphics::{
|
||||||
|
draw_target::DrawTarget,
|
||||||
|
mono_font::MonoTextStyle,
|
||||||
|
prelude::*,
|
||||||
|
primitives::{Rectangle, RoundedRectangle, PrimitiveStyleBuilder},
|
||||||
|
text::{Alignment, Text},
|
||||||
|
};
|
||||||
|
use profont::{
|
||||||
|
PROFONT_12_POINT, PROFONT_18_POINT,
|
||||||
|
};
|
||||||
|
use ssd1675::Color;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use crate::error::display_no_data;
|
||||||
|
use crate::ApplicationState;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use data::{Allergene, Intensity};
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
pub mod data;
|
||||||
|
|
||||||
|
pub fn get_pollen<D>(display: &mut D, state: &mut ApplicationState)
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Color>,
|
||||||
|
D::Error: Debug,
|
||||||
|
{
|
||||||
|
// determine whether an update to the state is necessary
|
||||||
|
let update_needed = if let Some(data) = &state.pollen_data {
|
||||||
|
let now = OffsetDateTime::now_local().unwrap();
|
||||||
|
(data.next_update - now).is_negative()
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
let pollen_data = if update_needed {
|
||||||
|
match api::get_pollen_data() {
|
||||||
|
Ok(d) => data::PollenData::from(d),
|
||||||
|
Err(e) => {
|
||||||
|
state.pollen_data = None;
|
||||||
|
display_no_data(display, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::mem::take(&mut state.pollen_data).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 80: Sachsen
|
||||||
|
// 81: Tiefland
|
||||||
|
let local_data = pollen_data.get_region_by_ids(80, 81).unwrap();
|
||||||
|
|
||||||
|
// draw results
|
||||||
|
Text::with_alignment(
|
||||||
|
"Pollenbelastung",
|
||||||
|
Point::new(106, 21),
|
||||||
|
MonoTextStyle::new(&PROFONT_18_POINT, Color::Black),
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(display)
|
||||||
|
.expect("error drawing text");
|
||||||
|
|
||||||
|
Text::new(
|
||||||
|
"Roggen",
|
||||||
|
Point::new(30, 49),
|
||||||
|
MonoTextStyle::new(&PROFONT_12_POINT, Color::Black),
|
||||||
|
)
|
||||||
|
.draw(display)
|
||||||
|
.expect("error drawing text");
|
||||||
|
|
||||||
|
let rye_intensity = local_data.pollen[&Allergene::Roggen].today;
|
||||||
|
let rye_value = (78_f32 * rye_intensity.as_fraction()).round() as u32;
|
||||||
|
let highlight_color = if rye_intensity >= Intensity::GeringBisMittel {
|
||||||
|
Color::Red
|
||||||
|
} else {
|
||||||
|
Color::Black
|
||||||
|
};
|
||||||
|
|
||||||
|
let outer_style = PrimitiveStyleBuilder::new()
|
||||||
|
.stroke_width(1)
|
||||||
|
.stroke_color(highlight_color)
|
||||||
|
.fill_color(Color::White)
|
||||||
|
.build();
|
||||||
|
let inner_style = PrimitiveStyleBuilder::new()
|
||||||
|
.stroke_width(0)
|
||||||
|
.stroke_color(highlight_color)
|
||||||
|
.fill_color(highlight_color)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RoundedRectangle::with_equal_corners(
|
||||||
|
Rectangle::new(Point::new(105, 39), Size::new(80, 12)),
|
||||||
|
Size::new(6, 6)
|
||||||
|
)
|
||||||
|
.into_styled(outer_style)
|
||||||
|
.draw(display)
|
||||||
|
.expect("error drawing rectangle");
|
||||||
|
|
||||||
|
RoundedRectangle::with_equal_corners(
|
||||||
|
Rectangle::new(Point::new(106, 40), Size::new(rye_value, 10)),
|
||||||
|
Size::new(5, 5)
|
||||||
|
)
|
||||||
|
.into_styled(inner_style)
|
||||||
|
.draw(display)
|
||||||
|
.expect("error drawing rectangle");
|
||||||
|
|
||||||
|
Text::new(
|
||||||
|
"Gräser",
|
||||||
|
Point::new(30, 69),
|
||||||
|
MonoTextStyle::new(&PROFONT_12_POINT, Color::Black),
|
||||||
|
)
|
||||||
|
.draw(display)
|
||||||
|
.expect("error drawing text");
|
||||||
|
|
||||||
|
let outer_style = PrimitiveStyleBuilder::new()
|
||||||
|
.stroke_width(1)
|
||||||
|
.stroke_color(Color::Black)
|
||||||
|
.fill_color(Color::White)
|
||||||
|
.build();
|
||||||
|
let inner_style = PrimitiveStyleBuilder::new()
|
||||||
|
.stroke_width(0)
|
||||||
|
.stroke_color(Color::Black)
|
||||||
|
.fill_color(Color::Black)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RoundedRectangle::with_equal_corners(
|
||||||
|
Rectangle::new(Point::new(105, 59), Size::new(80, 12)),
|
||||||
|
Size::new(6, 6)
|
||||||
|
)
|
||||||
|
.into_styled(outer_style)
|
||||||
|
.draw(display)
|
||||||
|
.expect("error drawing rectangle");
|
||||||
|
|
||||||
|
let graeser_intensity = local_data.pollen[&Allergene::Graeser].today;
|
||||||
|
let graeser_value = (78_f32 * graeser_intensity.as_fraction()).round() as u32;
|
||||||
|
|
||||||
|
RoundedRectangle::with_equal_corners(
|
||||||
|
Rectangle::new(Point::new(106, 60), Size::new(graeser_value, 10)),
|
||||||
|
Size::new(5, 5)
|
||||||
|
)
|
||||||
|
.into_styled(inner_style)
|
||||||
|
.draw(display)
|
||||||
|
.expect("error drawing rectangle");
|
||||||
|
|
||||||
|
|
||||||
|
// write back current status
|
||||||
|
state.pollen_data = Some(pollen_data);
|
||||||
|
}
|
|
@ -59,7 +59,7 @@ impl WeatherData {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_condition(&self) -> Bmp<'static, Color> {
|
pub fn current_condition(&self) -> Bmp<'static, Color> {
|
||||||
unimplemented!()
|
self.current.data.weather[0].condition.clone().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn forecast_plot_data(&self) -> Vec<PlotPoint> {
|
pub fn forecast_plot_data(&self) -> Vec<PlotPoint> {
|
||||||
|
@ -175,7 +175,7 @@ pub struct WeatherDetails {
|
||||||
/// specification](https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2).
|
/// specification](https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2).
|
||||||
/// It ignores the fine details within weather conditions (e.g. heavy thunderstorm and light
|
/// It ignores the fine details within weather conditions (e.g. heavy thunderstorm and light
|
||||||
/// thunderstorm both map to `Thunderstorm`).
|
/// thunderstorm both map to `Thunderstorm`).
|
||||||
#[derive(Deserialize)] //Debug)]
|
#[derive(Clone, Deserialize)] //Debug)]
|
||||||
#[serde(from = "u16")]
|
#[serde(from = "u16")]
|
||||||
pub enum WeatherCondition {
|
pub enum WeatherCondition {
|
||||||
Thunderstorm,
|
Thunderstorm,
|
||||||
|
|
|
@ -3,15 +3,15 @@ use embedded_graphics::{
|
||||||
image::Image,
|
image::Image,
|
||||||
mono_font::MonoTextStyle,
|
mono_font::MonoTextStyle,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
text::{Alignment, Baseline, Text},
|
text::Text
|
||||||
};
|
};
|
||||||
use embedded_plots::{axis::Scale, curve::Curve, single_plot::SinglePlot};
|
use embedded_plots::{axis::Scale, curve::Curve, single_plot::SinglePlot};
|
||||||
use profont::{
|
use profont::{
|
||||||
PROFONT_12_POINT, PROFONT_14_POINT, PROFONT_24_POINT, PROFONT_7_POINT, PROFONT_9_POINT,
|
PROFONT_14_POINT, PROFONT_24_POINT, PROFONT_7_POINT, PROFONT_9_POINT,
|
||||||
};
|
};
|
||||||
use ssd1675::Color;
|
use ssd1675::Color;
|
||||||
use std::error::Error;
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use crate::error::display_no_data;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
mod data;
|
mod data;
|
||||||
|
@ -119,30 +119,3 @@ where
|
||||||
.expect("Failed to draw the temperature plot");
|
.expect("Failed to draw the temperature plot");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_no_data<D, E>(display: &mut D, err: E)
|
|
||||||
where
|
|
||||||
D: DrawTarget<Color = Color>,
|
|
||||||
D::Error: Debug,
|
|
||||||
E: Error,
|
|
||||||
{
|
|
||||||
Text::with_alignment(
|
|
||||||
"KEINE DATEN",
|
|
||||||
Point::new(106, 28),
|
|
||||||
MonoTextStyle::new(&PROFONT_24_POINT, Color::Red),
|
|
||||||
Alignment::Center,
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("error drawing text");
|
|
||||||
|
|
||||||
eprintln!("[error] Failed to get/parse JSON:\n{}", err);
|
|
||||||
|
|
||||||
// TODO(feliix42): As soon as there is a custom text renderer for full texts, use it here!
|
|
||||||
Text::with_baseline(
|
|
||||||
&err.to_string(),
|
|
||||||
Point::new(5, 32),
|
|
||||||
MonoTextStyle::new(&PROFONT_12_POINT, Color::Black),
|
|
||||||
Baseline::Top,
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("error drawing text");
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue