Fixed typos in docs
This commit is contained in:
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -2,7 +2,7 @@ name: Docker
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- master
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -14,4 +14,4 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mv compose.yml docker-compose.yml
|
mv compose.yml docker-compose.yml
|
||||||
echo -e "version: \"2.2\"\n$(cat docker-compose.yml)" > docker-compose.yml
|
echo -e "version: \"2.2\"\n$(cat docker-compose.yml)" > docker-compose.yml
|
||||||
docker compose build
|
docker compose build
|
||||||
|
|||||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -2,7 +2,7 @@ name: Tests
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- master
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -16,4 +16,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
- name: "Running unit tests"
|
- name: "Running unit tests"
|
||||||
run: go test ./... -v
|
run: go test ./... -v
|
||||||
|
|||||||
57
README.md
57
README.md
@@ -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) |
|
||||||
@@ -342,4 +345,4 @@ You can run the unit tests by issuing the following command:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
This software is released under the GPLv3 license. You can find a copy of the license with this repository or by visiting the [following page](https://choosealicense.com/licenses/gpl-3.0/).
|
This software is released under the GPLv3 license. You can find a copy of the license with this repository or by visiting the [following page](https://choosealicense.com/licenses/gpl-3.0/).
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
5
main.go
5
main.go
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user