{"id":28051537,"url":"https://github.com/ceticamarco/zephyr","last_synced_at":"2025-07-19T14:35:48.547Z","repository":{"id":287343234,"uuid":"955420652","full_name":"ceticamarco/zephyr","owner":"ceticamarco","description":"🌲 real-time weather forecast service","archived":false,"fork":false,"pushed_at":"2025-06-20T08:20:56.000Z","size":43,"stargazers_count":18,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-20T09:28:45.540Z","etag":null,"topics":["analytics","api","functional-programming","haskell","http","json","network","weather-data","weather-forecast","weather-station"],"latest_commit_sha":null,"homepage":"https://m.marcocetica.com","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ceticamarco.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-03-26T15:58:53.000Z","updated_at":"2025-06-20T08:21:01.000Z","dependencies_parsed_at":"2025-04-11T09:48:29.411Z","dependency_job_id":"a769678c-9863-4ea5-8266-760b5e6d9c50","html_url":"https://github.com/ceticamarco/zephyr","commit_stats":null,"previous_names":["ceticamarco/zephyrus","ceticamarco/zephyr"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ceticamarco/zephyr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceticamarco%2Fzephyr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceticamarco%2Fzephyr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceticamarco%2Fzephyr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceticamarco%2Fzephyr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ceticamarco","download_url":"https://codeload.github.com/ceticamarco/zephyr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceticamarco%2Fzephyr/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265947514,"owners_count":23853383,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["analytics","api","functional-programming","haskell","http","json","network","weather-data","weather-forecast","weather-station"],"created_at":"2025-05-12T01:55:08.778Z","updated_at":"2025-07-19T14:35:43.528Z","avatar_url":"https://github.com/ceticamarco.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\u003ch1\u003eZephyr 🌲\u003c/h1\u003e\n    \n\u003ch6\u003e\u003ci\u003ereal-time weather forecast service\u003c/i\u003e\u003c/h6\u003e\n\n[![](https://github.com/ceticamarco/zephyr/actions/workflows/docker.yml/badge.svg)](https://github.com/ceticamarco/zephyr/actions/workflows/docker.yml)\n[![](https://github.com/ceticamarco/zephyr/actions/workflows/linter.yml/badge.svg)](https://github.com/ceticamarco/zephyr/actions/workflows/linter.yml)\n\n\u003c/div\u003e\n\n**Zephyr** is a lightweight HTTP weather service designed to provide a simple way to\ngather meteorological data and apply statistical analysis to past weather conditions. It's written in \nHaskell using [Servant](https://www.servant.dev/) and [OpenWeatherMap](https://openweathermap.org).\n\nI've built this service out of frustration with existing\nweather platforms cluttered with ads, paywalls, clickbait contents and unnecessary features.\nZephyr only gets you the essential information about the weather conditions of a given location without any additional nonsense.\n\nThis service communicates through a JSON API, making\nit suitable for use in any kind of project or device. I already use it on my phone,\non my terminal, on the tmux's status bar and on a couple of smart bedside alarm clocks I've built.\n\n## Basic Usage\nAs stated before, Zephyr communicates via HTTP using the JSON format; therefore, you can\nquery it through any kind of HTTP client such as cURL. Below you can find some examples of use.\n\n### Weather\nThe `/weather/:city` route allows you to retrieve generic weather conditions-such as the temperature,\nthe condition icon(represented by an emoji) and a textual description. For example:\n\n```sh\n$ curl -s 'http://127.0.0.1:3000/weather/milan' | jq\n```\nwill yield the following:\n\n```json\n{\n  \"condEmoji\": \"☀️\",\n  \"condition\": \"Clear\",\n  \"date\": \"Tuesday, 29/04/2025\",\n  \"feelsLike\": \"21°C\",\n  \"temperature\": \"23°C\"\n}\n```\nTo get results in imperial units instead of metric, append the `i`(for _imperial_) query parameter to the URL. For example\n\n```sh\n$ curl -s 'http://127.0.0.1:3000/weather/milan?jq' | jq\n```\n\nwhich will yield:\n\n```json\n{\n  \"condEmoji\": \"☀️\",\n  \"condition\": \"Clear\",\n  \"date\": \"Tuesday, 29/04/2025\",\n  \"feelsLike\": \"69°F\",\n  \"temperature\": \"73°F\"\n}\n```\n\n### Metrics\n\nThe `/metrics/:city` route allows you to retrieve environmental metrics about a certain location,\nfor example:\n\n\n```sh\n$ curl -s 'http://127.0.0.1:3000/metrics/taipei' | jq\n```\n\nwill yield:\n\n```json\n{\n  \"dewPoint\": \"15°C\",\n  \"humidity\": \"66%\",\n  \"pressure\": \"1015 hPa\",\n  \"uvIndex\": 0,\n  \"visibility\": \"10km\"\n}\n```\n\nAs in the previous case, you can append the `i` query parameter to get results\nin imperial units.\n\n### Wind\nThe `/wind/:city` route allows you to retrieve wind related data(such as speed and the direction)\nof a specific location. For example,\n\n```sh\n$ curl -s 'http://127.0.0.1:3000/wind/bolzano' | jq\n```\n\nwill yield\n\n```json\n{\n  \"arrow\": \"↙️\",\n  \"direction\": \"ENE\",\n  \"speed\": \"11.3 km/h\"\n}\n```\n\nAs in the previous case, you can append the `i` query parameter to get results\nin imperial units.\n\n### Forecast\nThe `/forecast/:city` route allows you to get the weather forecast\nof the next five days(excluding the current one). For example,\n\n```sh\n$  curl -s 'http://127.0.0.1:3000/forecast/Yakutsk' | jq\n```\n\nwill yield\n\n```json\n{\n  \"forecast\": [\n    {\n      \"condEmoji\": \"🌧 \",\n      \"condition\": \"Rain\",\n      \"date\": \"Tuesday, 06/05/2025\",\n      \"feelsLike\": \"0°C\",\n      \"tempMax\": \"6°C\",\n      \"tempMin\": \"-2°C\",\n      \"windArrow\": \"↗️\",\n      \"windDirection\": \"SSW\",\n      \"windSpeed\": \"14.7 km/h\"\n    },\n    {\n      \"condEmoji\": \"☃️\",\n      \"condition\": \"Snow\",\n      \"date\": \"Wednesday, 07/05/2025\",\n      \"feelsLike\": \"7°C\",\n      \"tempMax\": \"9°C\",\n      \"tempMin\": \"2°C\",\n      \"windArrow\": \"↘️\",\n      \"windDirection\": \"NNW\",\n      \"windSpeed\": \"13.9 km/h\"\n    }\n  ]\n}\n```\n\nAs in the previous case, you can append the `i` query parameter to get results\nin imperial units.\n\n### Moon\nThe `/moon` route provides the current moon phase along with an icon representing it.\nFor example,\n\n```sh\n$ curl -s 'http://127.0.0.1:3000/moon' | jq  \n```\n\nwill yield\n\n```json\n{\n  \"icon\": \"🌔\",\n  \"percentage\": \"Waxing Gibbous\",\n  \"phase\": \"89%\"\n}\n```\n\nTo convert OpenWeatherMap's moon phase value to the illumination percentage, \nI've used the following formula:\n\n$$\n  \\sin(\\pi \\theta)^2 \\times 100\n$$\n\nwhere $\\theta$ represent the moon phase value.\n\n## Statistical analysis\nIn addition to the previous routes, Zephyr provides another endpoint, called `/stats/:city`,\nwhich can be used to retrieve additional statistics about the weather of the\nprevious days. This includes the arithmetical mean of the temperatures, \nthe maximum and the minimum values, the median, the mode and the standard deviation.\n\nThis endpoint becomes available only after the system has gathered sufficient\n_updated_ data; that if and only if there are **at least** two weather\nrecords for a given location, and they are **within the previous 48 hours**. If these\ntwo conditions aren't met, Zephyr will refuse to provide a statistical report.\n\nAfter enough data has been recorded in the in-memory database, you will be able\nto retrieve the statistics, for example:\n\n```sh\n$ curl -s 'http://127.0.0.1:3000/stats/berlin'\n```\n\nwill yield(not real data):\n\n```json\n{                                                                                 \n  \"anomaly\": null,                      \n  \"count\": 12,\n  \"maximum\": 30,\n  \"mean\": 13.9167,                       \n  \"median\": 15.25,                       \n  \"minimum\": -15,\n  \"mode\": 16,\n  \"standardDev\": 9.6562\n}\n```\n\nAfter enough data has been recorded, Zephyr can also detect and report\ntemperature anomalies using a built-in statistical model(more about that below).\n\nFor instance, two temperature spikes(high and low) of `+30°C` and `-15°C` will\nbe flagged as anomalous and included in\nthe statistical report(again, the data is made up):\n\n```json\n{                                                                                 \n  \"anomaly\": [                                                                    \n    {                                                                             \n      \"anomalyDate\": \"2025-04-06\",                                                \n      \"anomalyTemp\": 30                    \n    },                \n    {\n      \"anomalyDate\": \"2025-04-07\",         \n      \"anomalyTemp\": -15                   \n    }                                    \n  ],                                     \n  \"count\": 12,\n  \"maximum\": 30,\n  \"mean\": 13.9167,                       \n  \"median\": 15.25,                       \n  \"minimum\": -15,\n  \"mode\": 16,\n  \"standardDev\": 9.6562\n}  \n```\n\n### Anomaly Detection\nThe anomaly detection model is based on a modified version\nof the [Z-Score](https://en.wikipedia.org/wiki/Standard_score) algorithm\nthat uses the [Median Absolute Deviation](https://en.wikipedia.org/wiki/Median_absolute_deviation) to measure variability in a given sample of quantitative\ndata. The entire procedure can be summarized as follows(let $X$ be the dataset):\n\nCompute the median\n\n$$\n    \\tilde{x} = \\text{median}({X})\n$$\n\nCompute The median absolute deviation\n\n$$\n  \\text{MAD} = \\text{median}\\{ |x_i - \\tilde{x}| : \\forall i = 0, \\dots, n-1 \\}\n$$\n\nCompute the (modified)Z-score\n\n$$\n  z_i = \\frac{0.6745 (x_i - \\tilde{x})}{\\text{MAD}}\n  \\quad \\forall i = 0, \\dots, n-1\n$$\n\nFlag $x_i$ as an outlier if $|z_i| \u003e 3.5$\n\nHere, $\\Phi^{-1}(3/4) = \\Phi^{-1}(0.75) \\approx 0.6745$ reflects the fact\nthat 75% of values lie within $\\approx 0.6745$ standard deviation and 3.5 represent a fixed\nthreshold value.\n\n\u003e [!IMPORTANT]\n\u003e The anomaly detection system works under the assumption that the weather\n\u003e data is normally distributed(at least roughly), this might not always be the case\n\u003e on datasets sampled over a short time window. For accurate result, collect at least\n\u003e two weeks of weather data.\n\nThe in-memory statistics database is updated each time the `/weather/:city` route\nis consumed and is reset at each restart. At the time being, there is no plan\nto make data gathering non-volatile.\n\n## Embedded Cache System\nIn order to minimize the amount of calls made to the OpenWeatherMap servers, Zephyr\nprovides a built-in, in-memory cache data structure to store fetched weather data.\nEach time a client requests any kind of weather data of a given location, Zephyr\ntries to search it first on the cache system; if it is found, the cached value is returned\notherwise a new API call is made and the retrieved values is added to the cache before\nbeing returned to the client. The expiration date, expressed in hours, is controlled via\nthe `ZEPHYR_CACHE_TTL` environment variable. Once a cached value expires, Zephyr retrieves\nfresh data from OpenWeatherMap servers.\n\nThe caching system significantly improves Zephyr performance by decreasing latency. Additionally,\nit helps minimize the number of API calls made to OpenWeatherMap' servers, an important factor\nif you are using the OpenWeatherMap free tier.\n\n## Configuration\nBefore deploying the service, you need to configure the following environment variables.\n\n| Variable             | Meaning                                |\n|----------------------|----------------------------------------|\n| `ZEPHYR_PORT`        | Listen port                            |\n| `ZEPHYR_TOKEN`       | OpenWeatherMap API key                 |\n| `ZEPHYR_CACHE_TTL`   | Cache time-to-live(expressed in hours) |\n\nEach value must be set _before_ launching the application. If you plan to deploy Zephyr\nusing Docker, you can specify these environment variables by editing the `compose.yml` file.\n\nYou will also need an OpenWeatherMap API key, you can get one for free by following\nthe instructions [listed on their website](https://openweathermap.org/api).\n\n\u003e [!NOTE]\n\u003e Zephyr is designed to work with OpenWeatherMap's free tier. \n\u003e As long as you stay within the daily limit of 1,000 calls, you won’t need to pay.\n\n\n## Deploy\nThe easiest way to deploy Zephyr is by using Docker. In order to launch it, issue the following\ncommand:\n\n```sh\n$ docker compose up -d\n```\n\nThis will build the container image and then launch it. By default the service will be available\nat `127.0.0.1:3000` but you can easily change this property by editing the associated environment\nvariable(see section above).\n\n## Unit tests\nThe `test/` directory includes unit tests for the statistics module. These tests are executed \nduring the container build process, but you can also run them manually by issuing the following command:\n\n```sh\n$ cabal test\n```\n\n## License\nThis 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/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fceticamarco%2Fzephyr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fceticamarco%2Fzephyr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fceticamarco%2Fzephyr/lists"}