Fixed typos in docs

This commit is contained in:
2025-06-23 05:40:20 +02:00
parent a987c0fb34
commit 82f67515e7
7 changed files with 44 additions and 40 deletions

View File

@@ -2,7 +2,7 @@ name: Docker
on: on:
push: push:
branches: branches:
- main - master
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View File

@@ -2,7 +2,7 @@ name: Tests
on: on:
push: push:
branches: branches:
- main - master
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View File

@@ -23,7 +23,7 @@ any kind of internet-based project or device. I already use it on a widget
on my phone, on my terminal, on the tmux's status bar, in a couple of on my phone, on my terminal, on the tmux's status bar, in a couple of
smart bedside clocks I've built and as a standalone web app. smart bedside clocks I've built and as a standalone web app.
## Basic Usage ## Weather
As state before, Zephyr talks via HTTP using the JSON format. Therefore, you As state before, Zephyr talks via HTTP using the JSON format. Therefore, you
can query it using any HTTP client of your choice. Below you can find some examples can query it using any HTTP client of your choice. Below you can find some examples
using `curl`: using `curl`:
@@ -118,26 +118,30 @@ which yields:
{ {
"forecast": [ "forecast": [
{ {
"condEmoji": "🌧️",
"condition": "Rain",
"date": "Tuesday, 2025/05/06", "date": "Tuesday, 2025/05/06",
"min": "-2°C",
"max": "6°C",
"condition": "Rain",
"emoji": "🌧️",
"feelsLike": "0°C", "feelsLike": "0°C",
"tempMax": "6°C", "wind": {
"tempMin": "-2°C", "arrow": "↗️",
"windArrow": "↗️", "direction": "SSW",
"windDirection": "SSW", "speed": "14.7 km/h"
"windSpeed": "14.7 km/h" }
}, },
{ {
"condEmoji": "☃️",
"condition": "Snow",
"date": "Wednesday, 2025/05/07", "date": "Wednesday, 2025/05/07",
"min": "2°C",
"max": "9°C",
"condition": "Snow",
"emoji": "☃️",
"feelsLike": "7°C", "feelsLike": "7°C",
"tempMax": "9°C", "wind": {
"tempMin": "2°C", "arrow": "↘️",
"windArrow": "↘️", "direction": "NNW",
"windDirection": "NNW", "speed": "13.9 km/h"
"windSpeed": "13.9 km/h" }
} }
] ]
} }
@@ -173,7 +177,7 @@ will yield
> $$ > $$
## Statistical analysis ## Statistical analysis
In addition to the weather data, Zephyr also provides statistical analysis of pase In addition to the weather data, Zephyr also provides statistical analysis of past
meteorological records. This is done through the `/stats/:city` endpoint, which meteorological records. This is done through the `/stats/:city` endpoint, which
returns additional information about the weather of the previous days such as returns additional information about the weather of the previous days such as
the average temperature, the maximum and minimum temperatures, the standard deviation, the average temperature, the maximum and minimum temperatures, the standard deviation,
@@ -205,9 +209,7 @@ which yields:
"anomaly": null "anomaly": null
} }
``` ```
After enough data has been collected, the service will also be able to detect The service is also able to detect anomalies in the temperature data using a built-in statistical model.
anomalies in the temperature data using a built-in statistical model.
For instance, two temperature spikes, such as `+34°C` and `-15°C`, with a mean of `25°C` and a standard deviation of `0.2°C`, For instance, two temperature spikes, such as `+34°C` and `-15°C`, with a mean of `25°C` and a standard deviation of `0.2°C`,
will be flagged as outliers by the model and will be reported as such: will be flagged as outliers by the model and will be reported as such:
@@ -237,14 +239,14 @@ will be flagged as outliers by the model and will be reported as such:
The anomaly detection algorithm is based on a modified version of the The anomaly detection algorithm is based on a modified version of the
[Z-Score](https://en.wikipedia.org/wiki/Standard_score) algorithm, which uses the [Z-Score](https://en.wikipedia.org/wiki/Standard_score) algorithm, which uses the
[Median Absolute Deviation](https://en.wikipedia.org/wiki/Median_absolute_deviation) to measure the variability [Median Absolute Deviation](https://en.wikipedia.org/wiki/Median_absolute_deviation) to measure the variability
in a given sample of quantitative data. The algorithm can be summarized as follows(let $x$ be the sample): in a given sample of quantitative data. The algorithm can be summarized as follows(let $X$ be the sample):
$$ $$
\tilde{x} = \text{median}({X}) \tilde{x} = \text{median}({X})
$$ $$
Compute The median absolute deviation Compute the median absolute deviation
$$ $$
\text{MAD} = \text{median}\{ |x_i - \tilde{x}| : \forall i = 0, \dots, n-1 \} \text{MAD} = \text{median}\{ |x_i - \tilde{x}| : \forall i = 0, \dots, n-1 \}
@@ -258,6 +260,7 @@ $$
$$ $$
Flag $x_i$ as an outlier if: Flag $x_i$ as an outlier if:
$$ $$
|z_i| > 4.5 |z_i| > 4.5
$$ $$
@@ -276,8 +279,8 @@ These constants have been fine-tuned to work well with the weather data of
a wide range of climates and to ignore daily temperature fluctuations while a wide range of climates and to ignore daily temperature fluctuations while
still being able to detect significant anomalies. still being able to detect significant anomalies.
Daily temperatures collected over a short time window(1/2 months, but not less than a few days) According to the Q-Q plots, daily temperatures collected over a time window of no more than 1/2 months
// *should* be normally distributed. This algorithm only work under this assumption. but no less than a week, *should* follow a normal distribution.
> [!IMPORTANT] > [!IMPORTANT]
> The anomaly detection algorithm works under the assumption that the weather data > The anomaly detection algorithm works under the assumption that the weather data
@@ -285,14 +288,13 @@ Daily temperatures collected over a short time window(1/2 months, but not less t
> with a very small number of samples(e.g. few days of data) or with a large > with a very small number of samples(e.g. few days of data) or with a large
> number of samples(e.g. multi-seasonal data). > number of samples(e.g. multi-seasonal data).
My statistical benchmarks(QQ plots) show that the algorithm works The algorithm works quite well when these conditions are met, and even with real world data,
quite well when these conditions are met, and even with real world data,
the results were quite satisfactory. However, if it the results were quite satisfactory. However, if it
start to produce false positives, you will need to dump the whole in-memory start to produce false positives, you will need to dump the whole in-memory
database and start from scratch. I recommend to do this at every change of season. database and start from scratch. I recommend to do this at every change of season.
## Embedded Cache System ## Embedded Cache System
To minimize the amount of requests sent to the OpenWeatherMap API, Zephyr provides an built-in, To minimize the amount of requests sent to the OpenWeatherMap API, Zephyr provides a built-in,
in-memory cache data structure that stores fetched weather data. Each time a client requests in-memory cache data structure that stores fetched weather data. Each time a client requests
weather data for a given location, the service will first check if it's already available on the cache. weather data for a given location, the service will first check if it's already available on the cache.
If it is found, the cached value will be returned, otherwise a new request will be sent to the OpenWeatherMap API If it is found, the cached value will be returned, otherwise a new request will be sent to the OpenWeatherMap API
@@ -309,6 +311,7 @@ Zephyr requires the following environment variables to be set:
| Variable | Meaning | | Variable | Meaning |
|----------------------|----------------------------------------| |----------------------|----------------------------------------|
| `ZEPHYR_ADDR` | Listen address |
| `ZEPHYR_PORT` | Listen port | | `ZEPHYR_PORT` | Listen port |
| `ZEPHYR_TOKEN` | OpenWeatherMap API key | | `ZEPHYR_TOKEN` | OpenWeatherMap API key |
| `ZEPHYR_CACHE_TTL` | Cache time-to-live(expressed in hours) | | `ZEPHYR_CACHE_TTL` | Cache time-to-live(expressed in hours) |

View File

@@ -4,9 +4,10 @@ services:
build: . build: .
container_name: "zephyr" container_name: "zephyr"
environment: environment:
ZEPHYR_PORT: 3000 # Listen port ZEPHYR_ADDR: 127.0.0.1 # Listen address
ZEPHYR_TOKEN: "" # OpenWeatherMap API Key ZEPHYR_PORT: 3000 # Listen port
ZEPHYR_CACHE_TTL: 3 # Cache time-to-live in hour ZEPHYR_TOKEN: "" # OpenWeatherMap API Key
ZEPHYR_CACHE_TTL: 3 # Cache time-to-live in hour
restart: always restart: always
volumes: volumes:
- "/etc/localtime:/etc/localtime:ro" - "/etc/localtime:/etc/localtime:ro"

View File

@@ -14,12 +14,13 @@ import (
func main() { func main() {
// Retrieve listening port, API token and cache time-to-live from environment variables // Retrieve listening port, API token and cache time-to-live from environment variables
var ( var (
host = os.Getenv("ZEPHYR_ADDR")
port = os.Getenv("ZEPHYR_PORT") port = os.Getenv("ZEPHYR_PORT")
token = os.Getenv("ZEPHYR_TOKEN") token = os.Getenv("ZEPHYR_TOKEN")
ttl, _ = strconv.ParseInt(os.Getenv("ZEPHYR_CACHE_TTL"), 10, 8) ttl, _ = strconv.ParseInt(os.Getenv("ZEPHYR_CACHE_TTL"), 10, 8)
) )
if port == "" || token == "" || ttl == 0 { if host == "" || port == "" || token == "" || ttl == 0 {
log.Fatalf("Environment variables not set") log.Fatalf("Environment variables not set")
} }
@@ -56,7 +57,7 @@ func main() {
controller.GetStatistics(res, req, statDB) controller.GetStatistics(res, req, statDB)
}) })
listenAddr := fmt.Sprintf(":%s", port) listenAddr := fmt.Sprintf("%s:%s", host, port)
log.Printf("Server listening on %s", listenAddr) log.Printf("Server listening on %s", listenAddr)
http.ListenAndServe(listenAddr, nil) http.ListenAndServe(listenAddr, nil)
} }

View File

@@ -12,8 +12,7 @@ type ForecastEntity struct {
Wind Wind `json:"wind"` Wind Wind `json:"wind"`
} }
// The Forecast data type, representing the an set // The Forecast data type, representing a set of ForecastEntity
// of ForecastEntity
type Forecast struct { type Forecast struct {
Forecast []ForecastEntity Forecast []ForecastEntity `json:"forecast"`
} }

View File

@@ -18,7 +18,7 @@ func InitDB() *StatDB {
} }
func (statDB *StatDB) AddStatistic(cityName string, weather Weather) { func (statDB *StatDB) AddStatistic(cityName string, weather Weather) {
key := fmt.Sprintf("%s@%s", weather.Date.Date, cityName) key := fmt.Sprintf("%s@%s", weather.Date.Date.Format("2006-01-02"), cityName)
// Insert weather statistic into the database only if it isn't present // Insert weather statistic into the database only if it isn't present
if _, isPresent := statDB.db[key]; isPresent { if _, isPresent := statDB.db[key]; isPresent {