Merge branch 'master' into patch-1

This commit is contained in:
Igor Chubin 2020-10-15 06:20:14 +02:00 committed by GitHub
commit 3717e30837
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2282 additions and 851 deletions

View File

@ -1,54 +1,70 @@
FROM ubuntu:18.04
RUN apt-get update && \
apt-get install -y curl \
git \
python \
python-pip \
python-dev \
autoconf \
libtool \
gawk
RUN curl -O https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
RUN tar xvf go1.10.3.linux-amd64.tar.gz
# Build stage
FROM golang:1-alpine as builder
WORKDIR /app
COPY ./share/we-lang/we-lang.go /app
RUN apk add --no-cache git
RUN go get -u github.com/mattn/go-colorable && \
go get -u github.com/klauspost/lctime && \
go get -u github.com/mattn/go-runewidth && \
CGO_ENABLED=0 go build /app/we-lang.go
# Results in /app/we-lang
FROM alpine:3
WORKDIR /app
COPY ./requirements.txt /app
ENV LLVM_CONFIG=/usr/bin/llvm9-config
RUN apk add --no-cache --virtual .build \
autoconf \
automake \
g++ \
gcc \
jpeg-dev \
llvm9-dev\
make \
zlib-dev \
&& apk add --no-cache \
python3 \
py3-pip \
py3-scipy \
py3-wheel \
py3-gevent \
zlib \
jpeg \
llvm9 \
libtool \
supervisor \
py3-numpy-dev \
python3-dev && \
mkdir -p /app/cache && \
mkdir -p /var/log/supervisor && \
mkdir -p /etc/supervisor/conf.d && \
chmod -R o+rw /var/log/supervisor && \
chmod -R o+rw /var/run && \
pip install -r requirements.txt --no-cache-dir && \
apk del --no-cache -r .build
COPY --from=builder /app/we-lang /app/bin/we-lang
COPY ./bin /app/bin
COPY ./lib /app/lib
COPY ./share /app/share
COPY ./requirements.txt /app
COPY ./src/we-lang/we-lang.go /app
# There are several files that must be fetched/created manually
# before building the image
COPY ./.wegorc /root
COPY ./.ip2location.key /root
COPY ./airports.dat /app
COPY ./GeoLite2-City.mmdb /app
RUN export PATH=$PATH:/go/bin && \
go get -u github.com/mattn/go-colorable && \
go get -u github.com/klauspost/lctime && \
go get -u github.com/mattn/go-runewidth && \
export GOBIN="/root/go/bin" && \
go install /app/we-lang.go
RUN pip install -r requirements.txt
RUN mkdir /app/cache
RUN mkdir -p /var/log/supervisor && \
mkdir -p /etc/supervisor/conf.d
RUN chmod -R o+rw /var/log/supervisor && \
chmod -R o+rw /var/run
COPY share/docker/supervisord.conf /etc/supervisor/supervisord.conf
ENV WTTR_MYDIR="/app"
ENV WTTR_GEOLITE="/app/GeoLite2-City.mmdb"
ENV WTTR_WEGO="/root/go/bin/we-lang"
ENV WTTR_WEGO="/app/bin/we-lang"
ENV WTTR_LISTEN_HOST="0.0.0.0"
ENV WTTR_LISTEN_PORT="8002"
EXPOSE 8002
CMD ["/usr/local/bin/supervisord"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

223
README.md
View File

@ -32,13 +32,16 @@ but it's still a live weather report in your language.)
Or in PowerShell:
(Invoke-WebRequest http://wttr.in).Content
```PowerShell
Invoke-RestMethod http://wttr.in
```
Want to get the weather information for a specific location? You can add the desired location to the URL in your
request like this:
$ curl wttr.in/London
$ curl wttr.in/Moscow
$ curl wttr.in/Salt+Lake+City
If you omit the location name, you will get the report for your current location based on your IP address.
@ -88,7 +91,8 @@ wttr.in currently supports five output formats:
* Plain-text for the terminal and scripts;
* HTML for the browser;
* PNG for the graphical viewers;
* JSON for scripts and APIs.
* JSON for scripts and APIs;
* Prometheus metrics for scripts and APIs.
The ANSI and HTML formats are selected basing on the User-Agent string.
The PNG format can be forced by adding `.png` to the end of the query:
@ -154,13 +158,13 @@ To specify your own custom output format, use the special `%`-notation:
c Weather condition,
C Weather condition textual name,
h Humidity,
t Temperature,
t Temperature (Actual),
f Temperature (Feels Like),
w Wind,
l Location,
m Moonphase 🌑🌒🌓🌔🌕🌖🌗🌘,
M Moonday,
p precipitation (mm),
o Probability of Precipitation,
P pressure (hPa),
D Dawn*,
@ -169,7 +173,7 @@ To specify your own custom output format, use the special `%`-notation:
s Sunset*,
d Dusk*.
(times are shown in the local timezone)
(*times are shown in the local timezone)
```
So, these two calls are the same:
@ -177,7 +181,7 @@ So, these two calls are the same:
```
$ curl wttr.in/London?format=3
London: ⛅️ +7⁰C
$ curl wttr.in/London?format="%l:+%c+%t"
$ curl wttr.in/London?format="%l:+%c+%t\n"
London: ⛅️ +7⁰C
```
Keep in mind, that when using in `tmux.conf`, you have to escape `%` with `%`, i.e. write there `%%` instead of `%`.
@ -203,7 +207,7 @@ both of them support all necessary emoji glyphs.
Font configuration:
```
```xml
$ cat ~/.config/fontconfig/fonts.conf
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
@ -253,6 +257,12 @@ or
$ curl wttr.in/München?format=v2
```
or, if you prefer Nerd Fonts instead of Emoji, `v2d` (day) or `v2n` (night):
```
$ curl v2d.wttr.in/München
```
![data-reach output format](https://wttr.in/files/example-wttr-v2.png)
@ -265,7 +275,7 @@ Currently, you need some tweaks for some terminals, to get the best possible vis
### URXVT
Depending on your configuration you might be taking all steps, or only a few. URXVT currenly doesn't support emoji related fonts, but we can get almost the same effect using *Font-Symbola*. So add to your `.Xresources` file the following line:
Depending on your configuration you might be taking all steps, or only a few. URXVT currently doesn't support emoji related fonts, but we can get almost the same effect using *Font-Symbola*. So add to your `.Xresources` file the following line:
```
xft:symbola:size=10:minspace=False
```
@ -277,9 +287,11 @@ The result, should look like:
![URXVT Emoji line](https://user-images.githubusercontent.com/24360204/63842949-1d36d480-c975-11e9-81dd-998d1329bd8a.png)
## JSON output
## Different output formats
The JSON format is a feature providing access to wttr.in data through an easy-to-parse format, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
### JSON output
The JSON format is a feature providing access to *wttr.in* data through an easy-to-parse format, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
To fetch information in JSON format, use the following syntax:
@ -288,42 +300,74 @@ To fetch information in JSON format, use the following syntax:
This will fetch information on the Detroit region in JSON format. The j1 format code is used to allow for the use of other layouts for the JSON output.
The result will look something like the following:
{
"current_condition": [
{
"FeelsLikeC": "25",
"FeelsLikeF": "76",
"cloudcover": "100",
"humidity": "76",
"observation_time": "04:08 PM",
"precipMM": "0.2",
"pressure": "1019",
"temp_C": "22",
"temp_F": "72",
"uvIndex": 5,
"visibility": "16",
"weatherCode": "122",
"weatherDesc": [
{
"value": "Overcast"
}
],
"weatherIconUrl": [
{
"value": ""
}
],
"winddir16Point": "NNE",
"winddirDegree": "20",
"windspeedKmph": "7",
"windspeedMiles": "4"
}
],
...
```json
{
"current_condition": [
{
"FeelsLikeC": "25",
"FeelsLikeF": "76",
"cloudcover": "100",
"humidity": "76",
"observation_time": "04:08 PM",
"precipMM": "0.2",
"pressure": "1019",
"temp_C": "22",
"temp_F": "72",
"uvIndex": 5,
"visibility": "16",
"weatherCode": "122",
"weatherDesc": [
{
"value": "Overcast"
}
],
"weatherIconUrl": [
{
"value": ""
}
],
"winddir16Point": "NNE",
"winddirDegree": "20",
"windspeedKmph": "7",
"windspeedMiles": "4"
}
],
...
```
Most of these values are self-explanatory, aside from `weatherCode`. The `weatherCode` is an enumeration which you can find at either [the WorldWeatherOnline website](https://www.worldweatheronline.com/developer/api/docs/weather-icons.aspx) or [in the wttr.in source code](https://github.com/chubin/wttr.in/blob/master/lib/constants.py).
### Prometheus Metrics Output
The [Prometheus](https://github.com/prometheus/prometheus) Metrics format is a feature providing access to *wttr.in* data through an easy-to-parse format for monitoring systems, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
To fetch information in Prometheus format, use the following syntax:
$ curl wttr.in/Detroit?format=p1
This will fetch information on the Detroit region in Prometheus Metrics format. The `p1` format code is used to allow for the use of other layouts for the Prometheus Metrics output.
A possible configuration for Prometheus could look like this:
```yaml
- job_name: 'wttr_in_detroit'
static_configs:
- targets: ['wttr.in']
metrics_path: '/Detroit'
params:
format: ['p1']
```
The result will look something like the following:
# HELP temperature_feels_like_celsius Feels Like Temperature in Celsius
temperature_feels_like_celsius{forecast="current"} 7
# HELP temperature_feels_like_fahrenheit Feels Like Temperature in Fahrenheit
temperature_feels_like_fahrenheit{forecast="current"} 45
[truncated]
...
## Moon phases
@ -343,10 +387,10 @@ To get the moon phase information in the online mode, use `%m`:
$ curl wttr.in/London?format=%m
🌖
Keep in mid that the Unicode representation of moonphases suffers 2 caveats:
Keep in mind that the Unicode representation of moonphases suffers 2 caveats:
- With some fonts, the representation `🌘` is ambiguous, for it either seem
almost-shadowed or almost-lit, depedending on whether your terminal is in
almost-shadowed or almost-lit, depending on whether your terminal is in
light mode or dark mode. Relying on colored fonts like `noto-fonts` works
around this problem.
@ -480,6 +524,19 @@ $ echo 'YOUR_IP2LOCATION_KEY' > ~/.ip2location.key
If you don't have this file, the service will be silently skipped (it is not a big problem,
because the MaxMind database is pretty good).
### Installation with Docker
* Install Docker
* Build Docker Image
* These files should be mounted by the user at runtime:
```
/root/.wegorc
/root/.ip2location.key (optional)
/app/airports.dat
/app/GeoLite2-City.mmdb
```
### Get a WorldWeatherOnline key and configure wego
To get a WorldWeatherOnline API key, you must register here:
@ -491,14 +548,16 @@ WWO key file: `~/.wwo.key`
Also, you have to specify the key in the `wego` configuration:
$ cat ~/.wegorc
{
"APIKey": "00XXXXXXXXXXXXXXXXXXXXXXXXXXX",
"City": "London",
"Numdays": 3,
"Imperial": false,
"Lang": "en"
}
```json
$ cat ~/.wegorc
{
"APIKey": "00XXXXXXXXXXXXXXXXXXXXXXXXXXX",
"City": "London",
"Numdays": 3,
"Imperial": false,
"Lang": "en"
}
```
The `City` parameter in `~/.wegorc` is ignored.
@ -507,42 +566,46 @@ The `City` parameter in `~/.wegorc` is ignored.
Configure the following environment variables that define the path to the local `wttr.in`
installation, to the GeoLite database, and to the `wego` installation. For example:
export WTTR_MYDIR="/home/igor/wttr.in"
export WTTR_GEOLITE="/home/igor/wttr.in/GeoLite2-City.mmdb"
export WTTR_WEGO="/home/igor/go/bin/wego"
export WTTR_LISTEN_HOST="0.0.0.0"
export WTTR_LISTEN_PORT="8002"
```bash
export WTTR_MYDIR="/home/igor/wttr.in"
export WTTR_GEOLITE="/home/igor/wttr.in/GeoLite2-City.mmdb"
export WTTR_WEGO="/home/igor/go/bin/wego"
export WTTR_LISTEN_HOST="0.0.0.0"
export WTTR_LISTEN_PORT="8002"
```
### Configure the HTTP-frontend service
It's recommended that you also configure the web server that will be used to access the service:
server {
listen [::]:80;
server_name wttr.in *.wttr.in;
access_log /var/log/nginx/wttr.in-access.log main;
error_log /var/log/nginx/wttr.in-error.log;
```nginx
server {
listen [::]:80;
server_name wttr.in *.wttr.in;
access_log /var/log/nginx/wttr.in-access.log main;
error_log /var/log/nginx/wttr.in-error.log;
location / {
proxy_pass http://127.0.0.1:8002;
location / {
proxy_pass http://127.0.0.1:8002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
client_max_body_size 10m;
client_body_buffer_size 128k;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
expires off;
}
}
expires off;
}
}
```

75
bin/proxy.py Normal file → Executable file
View File

@ -36,7 +36,8 @@ MYDIR = os.path.abspath(
os.path.dirname(os.path.dirname('__file__')))
sys.path.append("%s/lib/" % MYDIR)
from globals import PROXY_CACHEDIR, PROXY_HOST, PROXY_PORT
from globals import PROXY_CACHEDIR, PROXY_HOST, PROXY_PORT, USE_METNO, USER_AGENT
from metno import create_standard_json_from_metno, metno_request
from translations import PROXY_LANGS
# pylint: enable=wrong-import-position
@ -71,7 +72,12 @@ def load_translations():
return translations
TRANSLATIONS = load_translations()
def _is_metno():
return USE_METNO
def _find_srv_for_query(path, query): # pylint: disable=unused-argument
if _is_metno():
return 'https://api.met.no'
return 'http://api.worldweatheronline.com'
def _cache_file(path, query):
@ -84,7 +90,7 @@ def _cache_file(path, query):
digest = hashlib.sha1(("%s %s" % (path, query)).encode("utf-8")).hexdigest()
digest_number = ord(digest[0].upper())
expiry_interval = 60*(digest_number+10)
expiry_interval = 60*(digest_number+40)
timestamp = "%010d" % (int(time.time())//expiry_interval*expiry_interval)
filename = os.path.join(PROXY_CACHEDIR, timestamp, path, query)
@ -142,7 +148,7 @@ def add_translations(content, lang):
returned by the data source
"""
if content is "{}":
if content == "{}":
return {}
languages_to_translate = TRANSLATIONS.keys()
@ -155,7 +161,10 @@ def add_translations(content, lang):
return {}
try:
weather_condition = d['data']['current_condition'][0]['weatherDesc'][0]['value']
weather_condition = d['data']['current_condition'
][0]['weatherDesc'][0]['value'].capitalize()
d['data']['current_condition'][0]['weatherDesc'][0]['value'] = \
weather_condition
if lang in languages_to_translate:
d['data']['current_condition'][0]['lang_%s' % lang] = \
[{'value': translate(weather_condition, lang)}]
@ -201,18 +210,7 @@ def add_translations(content, lang):
print(exception)
return content
@APP.route("/<path:path>")
def proxy(path):
"""
Main proxy function. Handles incoming HTTP queries.
"""
lang = request.args.get('lang', 'en')
query_string = request.query_string.decode("utf-8")
query_string = query_string.replace('sr-lat', 'sr')
query_string = query_string.replace('lang=None', 'lang=en')
query_string += "&extra=localObsTime"
query_string += "&includelocation=yes"
def _fetch_content_and_headers(path, query_string, **kwargs):
content, headers = _load_content_and_headers(path, query_string)
if content is None:
@ -223,7 +221,7 @@ def proxy(path):
response = None
while attempts:
try:
response = requests.get(url, timeout=2)
response = requests.get(url, timeout=2, **kwargs)
except requests.ReadTimeout:
attempts -= 1
continue
@ -243,6 +241,34 @@ def proxy(path):
content = "{}"
else:
print("cache found")
return content, headers
@APP.route("/<path:path>")
def proxy(path):
"""
Main proxy function. Handles incoming HTTP queries.
"""
lang = request.args.get('lang', 'en')
query_string = request.query_string.decode("utf-8")
query_string = query_string.replace('sr-lat', 'sr')
query_string = query_string.replace('lang=None', 'lang=en')
content = ""
headers = ""
if _is_metno():
path, query, days = metno_request(path, query_string)
if USER_AGENT == '':
raise ValueError('User agent must be set to adhere to metno ToS: https://api.met.no/doc/TermsOfService')
content, headers = _fetch_content_and_headers(path, query, headers={
'User-Agent': USER_AGENT
})
content = create_standard_json_from_metno(content, days)
else:
# WWO tweaks
query_string += "&extra=localObsTime"
query_string += "&includelocation=yes"
content, headers = _fetch_content_and_headers(path, query)
content = add_translations(content, lang)
@ -251,6 +277,15 @@ def proxy(path):
if __name__ == "__main__":
#app.run(host='0.0.0.0', port=5001, debug=False)
#app.debug = True
bind_addr = "0.0.0.0"
SERVER = WSGIServer((bind_addr, PROXY_PORT), APP)
SERVER.serve_forever()
if len(sys.argv) == 1:
bind_addr = "0.0.0.0"
SERVER = WSGIServer((bind_addr, PROXY_PORT), APP)
SERVER.serve_forever()
else:
print('running single request from command line arg')
APP.testing = True
with APP.test_client() as c:
resp = c.get(sys.argv[1])
print('Status: ' + resp.status)
# print('Headers: ' + dumps(resp.headers))
print(resp.data.decode('utf-8'))

79
cmd/peakHandling.go Normal file
View File

@ -0,0 +1,79 @@
package main
import (
"log"
"net/http"
"sync"
"time"
"github.com/robfig/cron"
)
var peakRequest30 sync.Map
var peakRequest60 sync.Map
func initPeakHandling() {
c := cron.New()
// cronTime := fmt.Sprintf("%d,%d * * * *", 30-prefetchInterval/60, 60-prefetchInterval/60)
c.AddFunc("24 * * * *", prefetchPeakRequests30)
c.AddFunc("54 * * * *", prefetchPeakRequests60)
c.Start()
}
func savePeakRequest(cacheDigest string, r *http.Request) {
_, min, _ := time.Now().Clock()
if min == 30 {
peakRequest30.Store(cacheDigest, *r)
} else if min == 0 {
peakRequest60.Store(cacheDigest, *r)
}
}
func prefetchRequest(r *http.Request) {
processRequest(r)
}
func syncMapLen(sm *sync.Map) int {
count := 0
f := func(key, value interface{}) bool {
// Not really certain about this part, don't know for sure
// if this is a good check for an entry's existence
if key == "" {
return false
}
count++
return true
}
sm.Range(f)
return count
}
func prefetchPeakRequests(peakRequestMap *sync.Map) {
peakRequestLen := syncMapLen(peakRequestMap)
log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen)
if peakRequestLen == 0 {
return
}
sleepBetweenRequests := time.Duration(prefetchInterval*1000/peakRequestLen) * time.Millisecond
peakRequestMap.Range(func(key interface{}, value interface{}) bool {
go func(r http.Request) {
prefetchRequest(&r)
}(value.(http.Request))
peakRequestMap.Delete(key)
time.Sleep(sleepBetweenRequests)
return true
})
}
func prefetchPeakRequests30() {
prefetchPeakRequests(&peakRequest30)
}
func prefetchPeakRequests60() {
prefetchPeakRequests(&peakRequest60)
}

147
cmd/processRequest.go Normal file
View File

@ -0,0 +1,147 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"strings"
"time"
)
func processRequest(r *http.Request) responseWithHeader {
var response responseWithHeader
if dontCache(r) {
return get(r)
}
cacheDigest := getCacheDigest(r)
foundInCache := false
savePeakRequest(cacheDigest, r)
cacheBody, ok := lruCache.Get(cacheDigest)
if ok {
cacheEntry := cacheBody.(responseWithHeader)
// if after all attempts we still have no answer,
// we try to make the query on our own
for attempts := 0; attempts < 300; attempts++ {
if !ok || !cacheEntry.InProgress {
break
}
time.Sleep(30 * time.Millisecond)
cacheBody, ok = lruCache.Get(cacheDigest)
cacheEntry = cacheBody.(responseWithHeader)
}
if cacheEntry.InProgress {
log.Printf("TIMEOUT: %s\n", cacheDigest)
}
if ok && !cacheEntry.InProgress && cacheEntry.Expires.After(time.Now()) {
response = cacheEntry
foundInCache = true
}
}
if !foundInCache {
lruCache.Add(cacheDigest, responseWithHeader{InProgress: true})
response = get(r)
if response.StatusCode == 200 || response.StatusCode == 304 {
lruCache.Add(cacheDigest, response)
} else {
log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest)
lruCache.Remove(cacheDigest)
}
}
return response
}
func get(req *http.Request) responseWithHeader {
client := &http.Client{}
queryURL := fmt.Sprintf("http://%s%s", req.Host, req.RequestURI)
proxyReq, err := http.NewRequest(req.Method, queryURL, req.Body)
if err != nil {
log.Printf("Request: %s\n", err)
}
// proxyReq.Header.Set("Host", req.Host)
// proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)
for header, values := range req.Header {
for _, value := range values {
proxyReq.Header.Add(header, value)
}
}
res, err := client.Do(proxyReq)
if err != nil {
panic(err)
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Println(err)
}
return responseWithHeader{
InProgress: false,
Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
Body: body,
Header: res.Header,
StatusCode: res.StatusCode,
}
}
// implementation of the cache.get_signature of original wttr.in
func getCacheDigest(req *http.Request) string {
userAgent := req.Header.Get("User-Agent")
queryHost := req.Host
queryString := req.RequestURI
clientIPAddress := readUserIP(req)
lang := req.Header.Get("Accept-Language")
return fmt.Sprintf("%s:%s%s:%s:%s", userAgent, queryHost, queryString, clientIPAddress, lang)
}
// return true if request should not be cached
func dontCache(req *http.Request) bool {
// dont cache cyclic requests
loc := strings.Split(req.RequestURI, "?")[0]
if strings.Contains(loc, ":") {
return true
}
return false
}
func readUserIP(r *http.Request) string {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = r.RemoteAddr
var err error
IPAddress, _, err = net.SplitHostPort(IPAddress)
if err != nil {
log.Printf("ERROR: userip: %q is not IP:port\n", IPAddress)
}
}
return IPAddress
}
func randInt(min int, max int) int {
return min + rand.Intn(max-min)
}

View File

@ -2,113 +2,48 @@ package main
import (
"context"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"time"
"github.com/hashicorp/golang-lru"
lru "github.com/hashicorp/golang-lru"
)
const uplinkSrvAddr = "127.0.0.1:9002"
const uplinkTimeout = 30
const prefetchInterval = 300
const lruCacheSize = 12800
var lruCache *lru.Cache
type ResponseWithHeader struct {
type responseWithHeader struct {
InProgress bool // true if the request is being processed
Expires time.Time // expiration time of the cache entry
Body []byte
Header http.Header
StatusCode int // e.g. 200
}
func init() {
var err error
lruCache, err = lru.New(12800)
lruCache, err = lru.New(lruCacheSize)
if err != nil {
panic(err)
}
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
Timeout: uplinkTimeout * time.Second,
KeepAlive: uplinkTimeout * time.Second,
DualStack: true,
}
http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
addr = "127.0.0.1:8002"
return dialer.DialContext(ctx, network, addr)
http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, network, uplinkSrvAddr)
}
}
func readUserIP(r *http.Request) string {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = r.RemoteAddr
var err error
IPAddress, _, err = net.SplitHostPort(IPAddress)
if err != nil {
fmt.Printf("userip: %q is not IP:port\n", IPAddress)
}
}
return IPAddress
}
// implementation of the cache.get_signature of original wttr.in
func findCacheDigest(req *http.Request) string {
userAgent := req.Header.Get("User-Agent")
queryHost := req.Host
queryString := req.RequestURI
clientIpAddress := readUserIP(req)
lang := req.Header.Get("Accept-Language")
now := time.Now()
secs := now.Unix()
timestamp := secs / 1000
return fmt.Sprintf("%s:%s%s:%s:%s:%d", userAgent, queryHost, queryString, clientIpAddress, lang, timestamp)
}
func get(req *http.Request) ResponseWithHeader {
client := &http.Client{}
queryURL := fmt.Sprintf("http://%s%s", req.Host, req.RequestURI)
proxyReq, err := http.NewRequest(req.Method, queryURL, req.Body)
if err != nil {
// handle error
}
// proxyReq.Header.Set("Host", req.Host)
// proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)
for header, values := range req.Header {
for _, value := range values {
proxyReq.Header.Add(header, value)
}
}
res, err := client.Do(proxyReq)
if err != nil {
panic(err)
}
body, err := ioutil.ReadAll(res.Body)
return ResponseWithHeader{
Body: body,
Header: res.Header,
StatusCode: res.StatusCode,
}
initPeakHandling()
}
func copyHeader(dst, src http.Header) {
@ -120,26 +55,15 @@ func copyHeader(dst, src http.Header) {
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var response ResponseWithHeader
// printStat()
response := processRequest(r)
cacheDigest := findCacheDigest(r)
cacheBody, ok := lruCache.Get(cacheDigest)
if ok {
response = cacheBody.(ResponseWithHeader)
} else {
fmt.Println(cacheDigest)
response = get(r)
if response.StatusCode == 200 {
lruCache.Add(cacheDigest, response)
}
}
copyHeader(w.Header(), response.Header)
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(response.StatusCode)
w.Write(response.Body)
})
log.Fatal(http.ListenAndServe(":8081", nil))
log.Fatal(http.ListenAndServe(":8082", nil))
}

40
cmd/stat.go Normal file
View File

@ -0,0 +1,40 @@
package main
import (
"log"
"sync"
"time"
)
type safeCounter struct {
v map[int]int
mux sync.Mutex
}
func (c *safeCounter) inc(key int) {
c.mux.Lock()
c.v[key]++
c.mux.Unlock()
}
// func (c *safeCounter) val(key int) int {
// c.mux.Lock()
// defer c.mux.Unlock()
// return c.v[key]
// }
//
// func (c *safeCounter) reset(key int) int {
// c.mux.Lock()
// defer c.mux.Unlock()
// result := c.v[key]
// c.v[key] = 0
// return result
// }
var queriesPerMinute safeCounter
func printStat() {
_, min, _ := time.Now().Clock()
queriesPerMinute.inc(min)
log.Printf("Processed %d requests\n", min)
}

View File

@ -34,8 +34,20 @@ def get_signature(user_agent, query_string, client_ip_address, lang):
"""
Get cache signature based on `user_agent`, `url_string`,
`lang`, and `client_ip_address`
Return `None` if query should not be cached.
"""
if "?" in query_string:
location = query_string.split("?", 1)[0]
else:
location = query_string
if location.startswith("http://"):
location = location[7:]
elif location.startswith("https://"):
location = location[8:]
if ":" in location:
return None
signature = "%s:%s:%s:%s" % \
(user_agent, query_string, client_ip_address, lang)
print(signature)
@ -48,6 +60,9 @@ def get(signature):
the `_update_answer` function.
"""
if not signature:
return None
value_record = CACHE.get(signature)
if not value_record:
return None
@ -69,6 +84,8 @@ def store(signature, value):
"""
Store in cache `value` for `signature`
"""
if not signature:
return _update_answer(value)
if len(value) >= MIN_SIZE_FOR_FILECACHE:
value_to_store = _store_in_file(signature, value)

View File

@ -103,6 +103,68 @@ MOON_PHASES = (
"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"
)
WEATHER_SYMBOL_WI_DAY = {
"Unknown": "",
"Cloudy": "",
"Fog": "",
"HeavyRain": "",
"HeavyShowers": "",
"HeavySnow": "",
"HeavySnowShowers": "",
"LightRain": "",
"LightShowers": "",
"LightSleet": "",
"LightSleetShowers": "",
"LightSnow": "",
"LightSnowShowers": "",
"PartlyCloudy": "",
"Sunny": "",
"ThunderyHeavyRain": "",
"ThunderyShowers": "",
"ThunderySnowShowers": "",
"VeryCloudy": "",
}
WEATHER_SYMBOL_WI_NIGHT = {
"Unknown": "",
"Cloudy": "",
"Fog": "",
"HeavyRain": "",
"HeavyShowers": "",
"HeavySnow": "",
"HeavySnowShowers": "",
"LightRain": "",
"LightShowers": "",
"LightSleet": "",
"LightSleetShowers": "",
"LightSnow": "",
"LightSnowShowers": "",
"PartlyCloudy": "",
"Sunny": "",
"ThunderyHeavyRain": "",
"ThunderyShowers": "",
"ThunderySnowShowers": "",
"VeryCloudy": "",
}
WEATHER_SYMBOL_WIDTH_VTE_WI = {
}
WIND_DIRECTION_WI = [
"", "", "", "", "", "", "", "",
]
WIND_SCALE_WI = [
"", "", "", "", "", "", "", "", "", "", "", "", "",
]
MOON_PHASES_WI = (
"", "", "", "", "", "", "",
"", "", "", "", "", "", "",
"", "", "", "", "", "", "",
"", "", "", "", "", "", "",
)
WEATHER_SYMBOL_WEGO = {
"Unknown": [
" .-. ",
@ -168,7 +230,7 @@ WEATHER_SYMBOL_WEGO = {
"\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m",
"\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m",
"\033[38;5;226m /\033[38;5;250m(___(__) \033[0m",
"\033[38;5;228;5m ⚡\033[38;5;111;25m\033[38;5;228;5m⚡\033[38;5;111;25m \033[0m",
"\033[38;5;228;5m ⚡\033[38;5;111;25m \033[38;5;228;5m⚡\033[38;5;111;25m \033[0m",
"\033[38;5;111m \033[0m"],
"ThunderyHeavyRain": [
"\033[38;5;240;1m .-. \033[0m",

105
lib/fields.py Normal file
View File

@ -0,0 +1,105 @@
"""
Human readable description of the available data fields
describing current weather, weather forecast, and astronomical data
"""
DESCRIPTION = {
# current condition fields
"FeelsLikeC": (
"Feels Like Temperature in Celsius",
"temperature_feels_like_celsius"),
"FeelsLikeF": (
"Feels Like Temperature in Fahrenheit",
"temperature_feels_like_fahrenheit"),
"cloudcover": (
"Cloud Coverage in Percent",
"cloudcover_percentage"),
"humidity": (
"Humidity in Percent",
"humidity_percentage"),
"precipMM": (
"Precipitation (Rainfall) in mm",
"precipitation_mm"),
"pressure": (
"Air pressure in hPa",
"pressure_hpa"),
"temp_C": (
"Temperature in Celsius",
"temperature_celsius"),
"temp_F": (
"Temperature in Fahrenheit",
"temperature_fahrenheit"),
"uvIndex": (
"Ultaviolet Radiation Index",
"uv_index"),
"visibility": (
"Visible Distance in Kilometres",
"visibility"),
"weatherCode": (
"Code to describe Weather Condition",
"weather_code"),
"winddirDegree": (
"Wind Direction in Degree",
"winddir_degree"),
"windspeedKmph": (
"Wind Speed in Kilometres per Hour",
"windspeed_kmph"),
"windspeedMiles": (
"Wind Speed in Miles per Hour",
"windspeed_mph"),
"observation_time": (
"Minutes since start of the day the observation happened",
"observation_time"),
# fields with `description`
"weatherDesc": (
"Weather Description",
"weather_desc"),
"winddir16Point": (
"Wind Direction on a 16-wind compass rose",
"winddir_16_point"),
# forecast fields
"maxtempC": (
"Maximum Temperature in Celsius",
"temperature_celsius_maximum"),
"maxtempF": (
"Maximum Temperature in Fahrenheit",
"temperature_fahrenheit_maximum"),
"mintempC": (
"Minimum Temperature in Celsius",
"temperature_celsius_minimum"),
"mintempF": (
"Minimum Temperature in Fahrenheit",
"temperature_fahrenheit_minimum"),
"sunHour":(
"Hours of sunlight",
"sun_hour"),
"totalSnow_cm":(
"Total snowfall in cm",
"snowfall_cm"),
# astronomy fields
"moon_illumination": (
"Percentage of the moon illuminated",
"astronomy_moon_illumination"),
# astronomy fields with description
"moon_phase": (
"Phase of the moon",
"astronomy_moon_phase"),
# astronomy fields with time
"moonrise": (
"Minutes since start of the day untill the moon appears above the horizon",
"astronomy_moonrise_min"),
"moonset": (
"Minutes since start of the day untill the moon disappears below the horizon",
"astronomy_moonset_min"),
"sunrise": (
"Minutes since start of the day untill the sun appears above the horizon",
"astronomy_sunrise_min"),
"sunset": (
"Minutes since start of the day untill the moon disappears below the horizon",
"astronomy_sunset_min"),
}

View File

@ -8,6 +8,7 @@ External environment variables:
WTTR_WEGO
WTTR_LISTEN_HOST
WTTR_LISTEN_PORT
WTTR_USER_AGENT
"""
from __future__ import print_function
@ -24,7 +25,7 @@ else:
GEOLITE = os.path.join(MYDIR, 'data', "GeoLite2-City.mmdb")
WEGO = os.environ.get("WTTR_WEGO", "/home/igor/go/bin/we-lang")
PYPHOON = "/home/igor/pyphoon/bin/pyphoon-lolcat"
PYPHOON = "pyphoon-lolcat"
_DATADIR = "/wttr.in"
_LOGDIR = "/wttr.in/log"
@ -83,15 +84,34 @@ PLAIN_TEXT_AGENTS = [
PLAIN_TEXT_PAGES = [':help', ':bash.function', ':translation', ':iterm2']
_IP2LOCATION_KEY_FILE = os.environ['HOME'] + '/.ip2location.key'
_IPLOCATION_ORDER = os.environ.get(
"WTTR_IPLOCATION_ORDER",
'geoip,ip2location,ipinfo')
IPLOCATION_ORDER = _IPLOCATION_ORDER.split(',')
_IP2LOCATION_KEY_FILE = os.environ.get(
"WTTR_IP2LOCATION_KEY_FILE",
os.environ['HOME'] + '/.ip2location.key')
IP2LOCATION_KEY = None
if os.path.exists(_IP2LOCATION_KEY_FILE):
IP2LOCATION_KEY = open(_IP2LOCATION_KEY_FILE, 'r').read().strip()
_WWO_KEY_FILE = os.environ['HOME'] + '/.wwo.key'
_IPINFO_KEY_FILE = os.environ.get(
"WTTR_IPINFO_KEY_FILE",
os.environ['HOME'] + '/.ipinfo.key')
IPINFO_TOKEN = None
if os.path.exists(_IPINFO_KEY_FILE):
IPINFO_TOKEN = open(_IPINFO_KEY_FILE, 'r').read().strip()
_WWO_KEY_FILE = os.environ.get(
"WTTR_WWO_KEY_FILE",
os.environ['HOME'] + '/.wwo.key')
WWO_KEY = "key-is-not-specified"
USE_METNO = True
USER_AGENT = os.environ.get("WTTR_USER_AGENT", "")
if os.path.exists(_WWO_KEY_FILE):
WWO_KEY = open(_WWO_KEY_FILE, 'r').read().strip()
USE_METNO = False
def error(text):
"log error `text` and raise a RuntimeError exception"

View File

@ -17,7 +17,7 @@ import requests
import geoip2.database
from globals import GEOLITE, GEOLOCATOR_SERVICE, IP2LCACHE, IP2LOCATION_KEY, NOT_FOUND_LOCATION, \
ALIASES, BLACKLIST, IATA_CODES_FILE
ALIASES, BLACKLIST, IATA_CODES_FILE, IPLOCATION_ORDER, IPINFO_TOKEN
GEOIP_READER = geoip2.database.Reader(GEOLITE)
@ -88,9 +88,15 @@ def geolocator(location):
return None
def ip2location(ip_addr):
"Convert IP address `ip_addr` to a location name"
def ipcachewrite(ip_addr, location):
cached = os.path.join(IP2LCACHE, ip_addr)
if not os.path.exists(IP2LCACHE):
os.makedirs(IP2LCACHE)
with open(cached, 'w') as file:
file.write(location[0] + ';' + location[1])
def ipcache(ip_addr):
cached = os.path.join(IP2LCACHE, ip_addr)
if not os.path.exists(IP2LCACHE):
os.makedirs(IP2LCACHE)
@ -98,33 +104,57 @@ def ip2location(ip_addr):
location = None
if os.path.exists(cached):
location = open(cached, 'r').read()
else:
# if IP2LOCATION_KEY is not set, do not the query,
# because the query wont be processed anyway
if IP2LOCATION_KEY:
try:
ip2location_response = requests\
.get('http://api.ip2location.com/?ip=%s&key=%s&package=WS3' \
% (ip_addr, IP2LOCATION_KEY)).text
if ';' in ip2location_response:
open(cached, 'w').write(ip2location_response)
location = ip2location_response
except requests.exceptions.ConnectionError:
pass
location = open(cached, 'r').read().split(';')
if len(location) > 3:
return location[3], location[1]
elif len(location) > 1:
return location[0], location[1]
else:
return location[0], None
return None, None
def ip2location(ip_addr):
"Convert IP address `ip_addr` to a location name"
location = ipcache(ip_addr)
if location:
return location
# if IP2LOCATION_KEY is not set, do not the query,
# because the query wont be processed anyway
if IP2LOCATION_KEY:
try:
location = requests\
.get('http://api.ip2location.com/?ip=%s&key=%s&package=WS3' \
% (ip_addr, IP2LOCATION_KEY)).text
except requests.exceptions.ConnectionError:
pass
if location and ';' in location:
ipcachewrite(ip_addr, location)
location = location.split(';')[3], location.split(';')[1]
else:
location = location, None
return location
def get_location(ip_addr):
"""
Return location pair (CITY, COUNTRY) for `ip_addr`
"""
def ipinfo(ip_addr):
location = ipcache(ip_addr)
if location:
return location
if IPINFO_TOKEN:
r = requests.get('https://ipinfo.io/%s/json?token=%s' %
(ip_addr, IPINFO_TOKEN))
if r.status_code == 200:
location = r.json()["city"], r.json()["country"]
if location:
ipcachewrite(ip_addr, location)
return location
def geoip(ip_addr):
try:
response = GEOIP_READER.city(ip_addr)
country = response.country.name
@ -132,7 +162,34 @@ def get_location(ip_addr):
except geoip2.errors.AddressNotFoundError:
country = None
city = None
return city, country
def workaround(city, country):
# workaround for the strange bug with the country name
# maybe some other countries has this problem too
#
# Having these in a separate function will help if this gets to
# be a problem
if country == 'Russian Federation':
country = 'Russia'
return city, country
def get_location(ip_addr):
"""
Return location pair (CITY, COUNTRY) for `ip_addr`
"""
for method in IPLOCATION_ORDER:
if method == 'geoip':
city, country = geoip(ip_addr)
elif method == 'ip2location':
city, country = ip2location(ip_addr)
elif method == 'ipinfo':
city, country = ipinfo(ip_addr)
else:
print("ERROR: invalid iplocation method speficied: %s" % method)
if city is not None:
city, country = workaround(city, country)
return city, country
#
# temporary disabled it because of geoip services capcacity
#
@ -143,18 +200,9 @@ def get_location(ip_addr):
# city = location.raw.get('address', {}).get('city')
# except:
# pass
if city is None:
city, country = ip2location(ip_addr)
# workaround for the strange bug with the country name
# maybe some other countries has this problem too
if country == 'Russian Federation':
country = 'Russia'
if city:
return city, country
else:
return NOT_FOUND_LOCATION, None
# No methods resulted in a location - return default
return NOT_FOUND_LOCATION, None
def location_canonical_name(location):
@ -202,6 +250,19 @@ def is_location_blocked(location):
return location is not None and location.lower() in LOCATION_BLACK_LIST
def get_hemisphere(location):
"""
Return hemisphere of the location (True = North, False = South).
Assume North and return True if location can't be found.
"""
location_string = location[0]
if location[1] is not None:
location_string += ",%s" % location[1]
geolocation = geolocator(location_string)
if geolocation is None:
return True
return geolocation["latitude"] > 0
def location_processing(location, ip_addr):
"""
"""
@ -233,6 +294,12 @@ def location_processing(location, ip_addr):
query_source_location = get_location(ip_addr)
# For moon queries, hemisphere must be found
# True for North, False for South
hemisphere = False
if location is not None and (location.lower()+"@").startswith("moon@"):
hemisphere = get_hemisphere(query_source_location)
country = None
if not location or location == 'MyLocation':
location = ip_addr
@ -283,4 +350,5 @@ def location_processing(location, ip_addr):
override_location_name, \
full_address, \
country, \
query_source_location
query_source_location, \
hemisphere

462
lib/metno.py Executable file
View File

@ -0,0 +1,462 @@
#!/bin/env python
# vim: fileencoding=utf-8
from datetime import datetime, timedelta
import json
import logging
import os
import re
import sys
import timezonefinder
from pytz import timezone
from constants import WWO_CODE
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
logger = logging.getLogger(__name__)
def metno_request(path, query_string):
# We'll need to sanitize the inbound request - ideally the
# premium/v1/weather.ashx portion would have always been here, though
# it seems as though the proxy was built after the majority of the app
# and not refactored. For WAPI we'll strip this and the API key out,
# then manage it on our own.
logger.debug('Original path: ' + path)
logger.debug('Original query: ' + query_string)
path = path.replace('premium/v1/weather.ashx',
'weatherapi/locationforecast/2.0/complete')
query_string = re.sub(r'key=[^&]*&', '', query_string)
query_string = re.sub(r'format=[^&]*&', '', query_string)
days = int(re.search(r'num_of_days=([0-9]+)&', query_string).group(1))
query_string = re.sub(r'num_of_days=[0-9]+&', '', query_string)
# query_string = query_string.replace('key=', '?key=' + WAPI_KEY)
# TP is for hourly forecasting, which isn't available in the free api.
query_string = re.sub(r'tp=[0-9]*&', '', query_string)
# This assumes lang=... is at the end. Also note that the API doesn't
# localize, and we're not either. TODO: add language support
query_string = re.sub(r'lang=[^&]*$', '', query_string)
query_string = re.sub(r'&$', '', query_string)
logger.debug('qs: ' + query_string)
# Deal with coordinates. Need to be rounded to 4 decimals for metno ToC
# and in a different query string format
coord_match = re.search(r'q=[^&]*', query_string)
coords_str = coord_match.group(0)
coords = re.findall(r'[-0-9.]+', coords_str)
lat = str(round(float(coords[0]), 4))
lng = str(round(float(coords[1]), 4))
logger.debug('lat: ' + lat)
logger.debug('lng: ' + lng)
query_string = re.sub(r'q=[^&]*', 'lat=' + lat + '&lon=' + lng + '&',
query_string)
logger.debug('Return path: ' + path)
logger.debug('Return query: ' + query_string)
return path, query_string, days
def celsius_to_f(celsius):
return round((1.8 * celsius) + 32, 1)
def to_weather_code(symbol_code):
logger.debug(symbol_code)
code = re.sub(r'_.*', '', symbol_code)
logger.debug(code)
# symbol codes: https://api.met.no/weatherapi/weathericon/2.0/documentation
# they also have _day, _night and _polartwilight variants
# See json from https://api.met.no/weatherapi/weathericon/2.0/legends
# WWO codes: https://github.com/chubin/wttr.in/blob/master/lib/constants.py
# http://www.worldweatheronline.com/feed/wwoConditionCodes.txt
weather_code_map = {
"clearsky": 113,
"cloudy": 119,
"fair": 116,
"fog": 143,
"heavyrain": 302,
"heavyrainandthunder": 389,
"heavyrainshowers": 305,
"heavyrainshowersandthunder": 386,
"heavysleet": 314, # There's a ton of 'LightSleet' in WWO_CODE...
"heavysleetandthunder": 377,
"heavysleetshowers": 362,
"heavysleetshowersandthunder": 374,
"heavysnow": 230,
"heavysnowandthunder": 392,
"heavysnowshowers": 371,
"heavysnowshowersandthunder": 392,
"lightrain": 266,
"lightrainandthunder": 200,
"lightrainshowers": 176,
"lightrainshowersandthunder": 386,
"lightsleet": 281,
"lightsleetandthunder": 377,
"lightsleetshowers": 284,
"lightsnow": 320,
"lightsnowandthunder": 392,
"lightsnowshowers": 368,
"lightssleetshowersandthunder": 365,
"lightssnowshowersandthunder": 392,
"partlycloudy": 116,
"rain": 293,
"rainandthunder": 389,
"rainshowers": 299,
"rainshowersandthunder": 386,
"sleet": 185,
"sleetandthunder": 392,
"sleetshowers": 263,
"sleetshowersandthunder": 392,
"snow": 329,
"snowandthunder": 392,
"snowshowers": 230,
"snowshowersandthunder": 392,
}
if code not in weather_code_map:
logger.debug('not found')
return -1 # not found
logger.debug(weather_code_map[code])
return weather_code_map[code]
def to_description(symbol_code):
desc = WWO_CODE[str(to_weather_code(symbol_code))]
logger.debug(desc)
return desc
def to_16_point(degrees):
# 360 degrees / 16 = 22.5 degrees of arc or 11.25 degrees around the point
if degrees > (360 - 11.25) or degrees <= 11.25:
return 'N'
if degrees > 11.25 and degrees <= (11.25 + 22.5):
return 'NNE'
if degrees > (11.25 + (22.5 * 1)) and degrees <= (11.25 + (22.5 * 2)):
return 'NE'
if degrees > (11.25 + (22.5 * 2)) and degrees <= (11.25 + (22.5 * 3)):
return 'ENE'
if degrees > (11.25 + (22.5 * 3)) and degrees <= (11.25 + (22.5 * 4)):
return 'E'
if degrees > (11.25 + (22.5 * 4)) and degrees <= (11.25 + (22.5 * 5)):
return 'ESE'
if degrees > (11.25 + (22.5 * 5)) and degrees <= (11.25 + (22.5 * 6)):
return 'SE'
if degrees > (11.25 + (22.5 * 6)) and degrees <= (11.25 + (22.5 * 7)):
return 'SSE'
if degrees > (11.25 + (22.5 * 7)) and degrees <= (11.25 + (22.5 * 8)):
return 'S'
if degrees > (11.25 + (22.5 * 8)) and degrees <= (11.25 + (22.5 * 9)):
return 'SSW'
if degrees > (11.25 + (22.5 * 9)) and degrees <= (11.25 + (22.5 * 10)):
return 'SW'
if degrees > (11.25 + (22.5 * 10)) and degrees <= (11.25 + (22.5 * 11)):
return 'WSW'
if degrees > (11.25 + (22.5 * 11)) and degrees <= (11.25 + (22.5 * 12)):
return 'W'
if degrees > (11.25 + (22.5 * 12)) and degrees <= (11.25 + (22.5 * 13)):
return 'WNW'
if degrees > (11.25 + (22.5 * 13)) and degrees <= (11.25 + (22.5 * 14)):
return 'NW'
if degrees > (11.25 + (22.5 * 14)) and degrees <= (11.25 + (22.5 * 15)):
return 'NNW'
def meters_to_miles(meters):
return round(meters * 0.00062137, 2)
def mm_to_inches(mm):
return round(mm / 25.4, 2)
def hpa_to_mb(hpa):
return hpa
def hpa_to_in(hpa):
return round(hpa * 0.02953, 2)
def group_hours_to_days(lat, lng, hourlies, days_to_return):
tf = timezonefinder.TimezoneFinder()
timezone_str = tf.certain_timezone_at(lat=lat, lng=lng)
logger.debug('got TZ: ' + timezone_str)
tz = timezone(timezone_str)
start_day_gmt = datetime.fromisoformat(hourlies[0]['time']
.replace('Z', '+00:00'))
start_day_local = start_day_gmt.astimezone(tz)
end_day_local = (start_day_local + timedelta(days=days_to_return - 1)).date()
logger.debug('series starts at gmt time: ' + str(start_day_gmt))
logger.debug('series starts at local time: ' + str(start_day_local))
logger.debug('series ends on day: ' + str(end_day_local))
days = {}
for hour in hourlies:
current_day_gmt = datetime.fromisoformat(hour['time']
.replace('Z', '+00:00'))
current_local = current_day_gmt.astimezone(tz)
current_day_local = current_local.date()
if current_day_local > end_day_local:
continue
if current_day_local not in days:
days[current_day_local] = {'hourly': []}
hour['localtime'] = current_local.time()
days[current_day_local]['hourly'].append(hour)
# Need a second pass to build the min/max/avg data
for date, day in days.items():
minTempC = -999
maxTempC = 1000
avgTempC = None
n = 0
maxUvIndex = 0
for hour in day['hourly']:
temp = hour['data']['instant']['details']['air_temperature']
if temp > minTempC:
minTempC = temp
if temp < maxTempC:
maxTempC = temp
if avgTempC is None:
avgTempC = temp
n = 1
else:
avgTempC = ((avgTempC * n) + temp) / (n + 1)
n = n + 1
uv = hour['data']['instant']['details']
if 'ultraviolet_index_clear_sky' in uv:
if uv['ultraviolet_index_clear_sky'] > maxUvIndex:
maxUvIndex = uv['ultraviolet_index_clear_sky']
day["maxtempC"] = str(maxTempC)
day["maxtempF"] = str(celsius_to_f(maxTempC))
day["mintempC"] = str(minTempC)
day["mintempF"] = str(celsius_to_f(minTempC))
day["avgtempC"] = str(round(avgTempC, 1))
day["avgtempF"] = str(celsius_to_f(avgTempC))
# day["totalSnow_cm": "not implemented",
# day["sunHour": "12", # This would come from astonomy data
day["uvIndex"] = str(maxUvIndex)
return days
def _convert_hour(hour):
# Whatever is upstream is expecting data in the shape of WWO. This method will
# morph from metno to hourly WWO response format.
# Note that WWO is providing data every 3 hours. Metno provides every hour
# {
# "time": "0",
# "tempC": "19",
# "tempF": "66",
# "windspeedMiles": "6",
# "windspeedKmph": "9",
# "winddirDegree": "276",
# "winddir16Point": "W",
# "weatherCode": "119",
# "weatherIconUrl": [
# {
# "value": "http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0003_white_cloud.png"
# }
# ],
# "weatherDesc": [
# {
# "value": "Cloudy"
# }
# ],
# "precipMM": "0.0",
# "precipInches": "0.0",
# "humidity": "62",
# "visibility": "10",
# "visibilityMiles": "6",
# "pressure": "1017",
# "pressureInches": "31",
# "cloudcover": "66",
# "HeatIndexC": "19",
# "HeatIndexF": "66",
# "DewPointC": "12",
# "DewPointF": "53",
# "WindChillC": "19",
# "WindChillF": "66",
# "WindGustMiles": "8",
# "WindGustKmph": "13",
# "FeelsLikeC": "19",
# "FeelsLikeF": "66",
# "chanceofrain": "0",
# "chanceofremdry": "93",
# "chanceofwindy": "0",
# "chanceofovercast": "89",
# "chanceofsunshine": "18",
# "chanceoffrost": "0",
# "chanceofhightemp": "0",
# "chanceoffog": "0",
# "chanceofsnow": "0",
# "chanceofthunder": "0",
# "uvIndex": "1"
details = hour['data']['instant']['details']
if 'next_1_hours' in hour['data']:
next_hour = hour['data']['next_1_hours']
elif 'next_6_hours' in hour['data']:
next_hour = hour['data']['next_6_hours']
elif 'next_12_hours' in hour['data']:
next_hour = hour['data']['next_12_hours']
else:
next_hour = {}
# Need to dig out symbol_code and precipitation_amount
symbol_code = 'clearsky_day' # Default to sunny
if 'summary' in next_hour and 'symbol_code' in next_hour['summary']:
symbol_code = next_hour['summary']['symbol_code']
precipitation_amount = 0 # Default to no rain
if 'details' in next_hour and 'precipitation_amount' in next_hour['details']:
precipitation_amount = next_hour['details']['precipitation_amount']
uvIndex = 0 # default to 0 index
if 'ultraviolet_index_clear_sky' in details:
uvIndex = details['ultraviolet_index_clear_sky']
localtime = ''
if 'localtime' in hour:
localtime = "{h:02.0f}".format(h=hour['localtime'].hour) + \
"{m:02.0f}".format(m=hour['localtime'].minute)
logger.debug(str(hour['localtime']))
# time property is local time, 4 digit 24 hour, with no :, e.g. 2100
return {
'time': localtime,
'observation_time': hour['time'], # Need to figure out WWO TZ
# temp_C is used in we-lang.go calcs in such a way
# as to expect a whole number
'temp_C': str(int(round(details['air_temperature'], 0))),
# temp_F can be more precise - not used in we-lang.go calcs
'temp_F': str(celsius_to_f(details['air_temperature'])),
'weatherCode': str(to_weather_code(symbol_code)),
'weatherIconUrl': [{
'value': 'not yet implemented',
}],
'weatherDesc': [{
'value': to_description(symbol_code),
}],
# similiarly, windspeedMiles is not used by we-lang.go, but kmph is
"windspeedMiles": str(meters_to_miles(details['wind_speed'])),
"windspeedKmph": str(int(round(details['wind_speed'], 0))),
"winddirDegree": str(details['wind_from_direction']),
"winddir16Point": to_16_point(details['wind_from_direction']),
"precipMM": str(precipitation_amount),
"precipInches": str(mm_to_inches(precipitation_amount)),
"humidity": str(details['relative_humidity']),
"visibility": 'not yet implemented', # str(details['vis_km']),
"visibilityMiles": 'not yet implemented', # str(details['vis_miles']),
"pressure": str(hpa_to_mb(details['air_pressure_at_sea_level'])),
"pressureInches": str(hpa_to_in(details['air_pressure_at_sea_level'])),
"cloudcover": 'not yet implemented', # Convert from cloud_area_fraction?? str(details['cloud']),
# metno doesn't have FeelsLikeC, but we-lang.go is using it in calcs,
# so we shall set it to temp_C
"FeelsLikeC": str(int(round(details['air_temperature'], 0))),
"FeelsLikeF": 'not yet implemented', # str(details['feelslike_f']),
"uvIndex": str(uvIndex),
}
def _convert_hourly(hours):
converted_hours = []
for hour in hours:
converted_hours.append(_convert_hour(hour))
return converted_hours
# Whatever is upstream is expecting data in the shape of WWO. This method will
# morph from metno to WWO response format.
def create_standard_json_from_metno(content, days_to_return):
try:
forecast = json.loads(content) # pylint: disable=invalid-name
except (ValueError, TypeError) as exception:
logger.error("---")
logger.error(exception)
logger.error("---")
return {}, ''
hourlies = forecast['properties']['timeseries']
current = hourlies[0]
# We are assuming these units:
# "units": {
# "air_pressure_at_sea_level": "hPa",
# "air_temperature": "celsius",
# "air_temperature_max": "celsius",
# "air_temperature_min": "celsius",
# "cloud_area_fraction": "%",
# "cloud_area_fraction_high": "%",
# "cloud_area_fraction_low": "%",
# "cloud_area_fraction_medium": "%",
# "dew_point_temperature": "celsius",
# "fog_area_fraction": "%",
# "precipitation_amount": "mm",
# "relative_humidity": "%",
# "ultraviolet_index_clear_sky": "1",
# "wind_from_direction": "degrees",
# "wind_speed": "m/s"
# }
content = {
'data': {
'request': [{
'type': 'feature',
'query': str(forecast['geometry']['coordinates'][1]) + ',' +
str(forecast['geometry']['coordinates'][0])
}],
'current_condition': [
_convert_hour(current)
],
'weather': []
}
}
days = group_hours_to_days(forecast['geometry']['coordinates'][1],
forecast['geometry']['coordinates'][0],
hourlies, days_to_return)
# TODO: Astronomy needs to come from this:
# https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2020-10-07&offset=-05:00
# and obviously can be cached for a while
# https://api.met.no/weatherapi/sunrise/2.0/documentation
# Note that full moon/new moon/first quarter/last quarter aren't returned
# and the moonphase value should match these from WWO:
# New Moon
# Waxing Crescent
# First Quarter
# Waxing Gibbous
# Full Moon
# Waning Gibbous
# Last Quarter
# Waning Crescent
for date, day in days.items():
content['data']['weather'].append({
"date": str(date),
"astronomy": [],
"maxtempC": day['maxtempC'],
"maxtempF": day['maxtempF'],
"mintempC": day['mintempC'],
"mintempF": day['mintempF'],
"avgtempC": day['avgtempC'],
"avgtempF": day['avgtempF'],
"totalSnow_cm": "not implemented",
"sunHour": "12", # This would come from astonomy data
"uvIndex": day['uvIndex'],
'hourly': _convert_hourly(day['hourly']),
})
# for day in forecast.
return json.dumps(content)
if __name__ == "__main__":
# if len(sys.argv) == 1:
# for deg in range(0, 360):
# print('deg: ' + str(deg) + '; 16point: ' + to_16_point(deg))
if len(sys.argv) == 2:
req = sys.argv[1].split('?')
# to_description(sys.argv[1])
metno_request(req[0], req[1])
elif len(sys.argv) == 3:
with open(sys.argv[1], 'r') as contentf:
content = create_standard_json_from_metno(contentf.read(),
int(sys.argv[2]))
print(content)
else:
print('usage: metno <content file> <days>')

View File

@ -6,8 +6,8 @@ Translation of almost everything.
FULL_TRANSLATION = [
"ar", "af", "be", "ca", "da", "de", "el", "et",
"fr", "fa", "hu", "ia", "id", "it",
"nb", "nl", "pl", "pt-br", "ro",
"fr", "fa", "hi", "hu", "ia", "id", "it",
"nb", "nl", "oc", "pl", "pt-br", "ro",
"ru", "tr", "th", "uk", "vi", "zh-cn", "zh-tw"
]
@ -23,10 +23,10 @@ PARTIAL_TRANSLATION = [
PROXY_LANGS = [
"af", "ar", "az", "be", "bs", "ca",
"cy", "de", "el", "eo", "et", "fa", "fr",
"fy", "he", "hr", "hu", "hy",
"cy", "de", "el", "eo", "et", "eu", "fa", "fr",
"fy", "ga", "he", "hr", "hu", "hy",
"ia", "id", "is", "it", "ja", "kk",
"lv", "mk", "nb", "nn", "ro",
"lv", "mk", "nb", "nn", "oc", "ro",
"ru", "sl", "th", "pt-br", "uk", "uz",
"vi", "zh-cn", "zh-tw",
]
@ -120,6 +120,16 @@ Nous n'avons pas pu déterminer votre position,
Nous vous avons donc amenés à Oïmiakon,
l'un des endroits les plus froids habités en permanence sur la planète.
Nous espérons qu'il fait meilleur chez vous !
""",
'ga': u"""
rabhamar ábalta do cheantar a aimsiú
mar sin thugamar go dtí Oymyakon,
ceann do na ceantair bhuanáitrithe is fuaire ar domhan.
""",
'hi': u"""
हम आपक जन असमर ,
इसलि हम आपक ओयमय पर आए ,
रह सबस एक |
""",
'hu': u"""
Nem sikerült megtalálni a pozíciódat,
@ -186,6 +196,12 @@ dus hebben we u naar Ojmjakon gebracht,
Wy koenen jo lokaasje net fêststelle
dus wy ha jo nei Ojmjakon brocht,
ien fan de kâldste permanent bewenbere plakken op ierde.
""",
'oc': u"""
Avèm pas pogut determinar vòstra posicion,
Vos avèm doncas menat a Oïmiakon,
un dels endreches mai freds abitat permanéncia del monde.
Esperam que fa melhor en çò vòstre !
""",
'pt': u"""
Não conseguimos encontrar a sua localização,
@ -256,7 +272,7 @@ shuning uchun sizga sayyoramizning eng sovuq aholi punkti - Oymyakondagi ob-havo
Umid qilamizki, sizda bugungi ob-havo bundan yaxshiroq!
""",
'zh': u"""
我们无法找到您的位置,
我们无法找到您的位置,
当前显示奥伊米亚康(Oymyakon)这个星球上最冷的人类定居点
""",
'da': u"""
@ -296,9 +312,12 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'el': u'Άνγωστη τοποθεσία',
'es': u'Ubicación desconocida',
'et': u'Tundmatu asukoht',
'eu': u'Kokapen ezezaguna',
'fa': u'مکان نامعلوم',
'fi': u'Tuntematon sijainti',
'fr': u'Emplacement inconnu',
'ga': u'Ceantar anaithnid',
'hi': u'अज्ञात स्थान',
'hu': u'Ismeretlen lokáció',
'hy': u'Անհայտ գտնվելու վայր',
'id': u'Lokasi tidak diketahui',
@ -312,6 +331,7 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'mk': u'Непозната локација',
'nb': u'Ukjent sted',
'nl': u'Onbekende locatie',
'oc': u'Emplaçament desconegut',
'fy': u'Ûnbekende lokaasje',
'pl': u'Nieznana lokalizacja',
'pt': u'Localização desconhecida',
@ -347,9 +367,12 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'el': u'Τοποθεσία',
'es': u'Ubicación',
'et': u'Asukoht',
'eu': u'Kokaena',
'fa': u'مکان',
'fi': u'Tuntematon sijainti',
'fr': u'Emplacement',
'ga': u'Ceantar',
'hi': u'स्थान',
'hu': u'Lokáció',
'hy': u'Դիրք',
'ia': u'Location',
@ -363,6 +386,7 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'mk': u'Локација',
'nb': u'Sted',
'nl': u'Locatie',
'oc': u'Emplaçament',
'fy': u'Lokaasje',
'pl': u'Lokalizacja',
'pt': u'Localização',
@ -385,7 +409,7 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
'CAPACITY_LIMIT_REACHED': {
'en': u"""
Sorry, we are running out of queries to the weather service at the moment.
Here is the weather report for the default city (just to show you, how it looks like).
Here is the weather report for the default city (just to show you what it looks like).
We will get new queries as soon as possible.
You can follow https://twitter.com/igor_chubin for the updates.
======================================================================================
@ -465,6 +489,20 @@ Voici un bulletin météo de l'emplacement par défaut (pour vous donner un aper
Nous serons très bientôt en mesure de faire de nouvelles requêtes.
Vous pouvez suivre https://twitter.com/igor_chubin pour rester informé.
======================================================================================
""",
'ga': u"""
brón orainn, níl mórán iarratas le fail chuig seirbhís na haimsire faoi láthair.
Seo duit réamhaisnéis na haimsire don chathair réamhshocraithe (chun é a thaispeaint duit).
Gheobhaimid iarratais nua chomh luath agus is feidir.
Lean orainn ar https://twitter.com/igor_chubin don eolas is déanaí.
======================================================================================
""",
'hi': u"""
षम कर, इस समय हम सम पर नह कर रह
यह िि शहर ि सम नक (आपक यह ि ि ि यह िखत )
हम जल जल सम पर करन नई ि कर
आप अपड ि https://twitter.com/igor_chubin अनसरण कर सकत
======================================================================================
""",
'hu': u"""
Sajnáljuk, kifogytunk az időjárási szolgáltatásra fordított erőforrásokból.
@ -542,6 +580,13 @@ Hier is het weerbericht voor de standaard stad(zodat u weet hoe het er uitziet)
Wij lossen dit probleem zo snel mogelijk op.
voor updates kunt u ons op https://twitter.com/igor_chubin volgen.
======================================================================================
""",
'oc': u"""
O planhèm, avèm pas mai de requèstas cap al servici metèo.
Vaquí las prevision metèo de l'emplaçament per defaut (per vos donar un apercebut).
Poirem lèu ne far de novèlas.
Podètz seguir https://twitter.com/igor_chubin per demorar informat.
======================================================================================
""",
'fy': u"""
Excuses, wy kinne op dit moment 't waarberjocht net sjin litte.
@ -657,6 +702,7 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
'fy': u'Nije funksje: twatalige lokaasje nammen \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) en lokaasje sykjen \033[92mwttr.in/~Kilimanjaro\033[0m (set er gewoan in ~ foar)',
'cy': u'Nodwedd newydd: enwau lleoliadau amlieithog \033[92mwttr.in/станция+Восток\033[0m (yn UTF-8) a chwilio am leoliad \033[92mwttr.in/~Kilimanjaro\033[0m (ychwanegwch ~ yn gyntaf)',
'de': u'Neue Funktion: mehrsprachige Ortsnamen \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) und Ortssuche \033[92mwttr.in/~Kilimanjaro\033[0m (fügen Sie ein ~ vor dem Ort ein)',
'hi': u'नई सुविधा: बहुभाषी स्थान के नाम \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) और स्थान खोज \033[92mwttr.in/~Kilimanjaro\033[0m (बस ~ आगे लगाये)',
'hu': u'Új funkcinalitás: többnyelvű helynevek \033[92mwttr.in/станция+Восток\033[0m (UTF-8-ban) és pozíció keresés \033[92mwttr.in/~Kilimanjaro\033[0m (csak adj egy ~ jelet elé)',
'hy': u'Փորձարկեք: տեղամասերի անունները կամայական լեզվով \033[92mwttr.in/Դիլիջան\033[0m (в UTF-8) և տեղանքի որոնում \033[92mwttr.in/~Kilimanjaro\033[0m (հարկավոր է ~ ավելացնել դիմացից)',
'ia': u'Nove functione: location nomine multilingue \033[92mwttr.in/станция+Восток\033[0m (a UTF-8) e recerca de location\033[92mwttr.in/~Kilimanjaro\033[0m (solo adde ~ ante)',
@ -666,6 +712,7 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
'kk': u'',
'lv': u'Jaunums: Daudzvalodu atrašanās vietu nosaukumi \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) un dabas objektu meklēšana \033[92mwttr.in/~Kilimanjaro\033[0m (tikai priekšā pievieno ~)',
'mk': u'Нова функција: повеќе јазично локациски имиња \033[92mwttr.in/станция+Восток\033[0m (во UTF-8) и локациско пребарување \033[92mwttr.in/~Kilimanjaro\033[0m (just add ~ before)',
'oc': u'Novèla foncionalitat : nom de lòc multilenga \033[92mwttr.in/станция+Восток\033[0m (en UTF-8) e recèrca de lòc \033[92mwttr.in/~Kilimanjaro\033[0m (solament ajustatz ~ abans)',
'pl': u'Nowa funkcjonalność: wielojęzyczne nazwy lokalizacji \033[92mwttr.in/станция+Восток\033[0m (w UTF-8) i szukanie lokalizacji \033[92mwttr.in/~Kilimanjaro\033[0m (poprzedź zapytanie ~ - znakiem tyldy)',
'pt': u'Nova funcionalidade: nomes de localidades em várias línguas \033[92mwttr.in/станция+Восток\033[0m (em UTF-8) e procura por localidades \033[92mwttr.in/~Kilimanjaro\033[0m (é só colocar ~ antes)',
'pt-br': u'Nova funcionalidade: nomes de localidades em várias línguas \033[92mwttr.in/станция+Восток\033[0m (em UTF-8) e procura por localidades \033[92mwttr.in/~Kilimanjaro\033[0m (é só colocar ~ antes)',
@ -691,10 +738,13 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
'bs': u'XXXXXX \033[46m\033[30m@igor_chubin\033[0m XXXXXXXXXXXXXXXXXXX',
'ca': u'Segueix \033[46m\033[30m@igor_chubin\033[0m per actualitzacions de wttr.in',
'es': u'Sigue a \033[46m\033[30m@igor_chubin\033[0m para enterarte de las novedades de wttr.in',
'eu': u'\033[46m\033[30m@igor_chubin\033[0m jarraitu wttr.in berriak jasotzeko',
'cy': u'Dilyner \033[46m\033[30m@igor_Chubin\033[0m am diweddariadau wttr.in',
'fa': u'برای دنبال کردن خبرهای wttr.in شناسه \033[46m\033[30m@igor_chubin\033[0m رو فالو کنید.',
'fr': u'Suivez \033[46m\033[30m@igor_Chubin\033[0m pour rester informé sur wttr.in',
'de': u'Folgen Sie \033[46m\033[30mhttps://twitter.com/igor_chubin\033[0m für wttr.in Updates',
'ga': u'Lean \033[46m\033[30m@igor_chubin\033[0m don wttr.in eolas is deanaí',
'hi': u'अपडेट के लिए फॉलो करें \033[46m\033[30m@igor_chubin\033[0m',
'hu': u'Kövesd \033[46m\033[30m@igor_chubin\033[0m-t további wttr.in információkért',
'hy': u'Նոր ֆիչռների համար հետևեք՝ \033[46m\033[30m@igor_chubin\033[0m',
'ia': u'Seque \033[46m\033[30m@igor_chubin\033[0m por nove information de wttr.in',
@ -706,6 +756,7 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
'mk': u'Следете \033[46m\033[30m@igor_chubin\033[0m за wttr.in новости',
'nb': u'Følg \033[46m\033[30m@igor_chubin\033[0m for wttr.in oppdateringer',
'nl': u'Volg \033[46m\033[30m@igor_chubin\033[0m voor wttr.in updates',
'oc': u'Seguissètz \033[46m\033[30m@igor_Chubin\033[0m per demorar informat sus wttr.in',
'fy': u'Folgje \033[46m\033[30m@igor_chubin\033[0m foar wttr.in updates',
'pl': u'Śledź \033[46m\033[30m@igor_chubin\033[0m aby być na bieżąco z nowościami dotyczącymi wttr.in',
'pt': u'Seguir \033[46m\033[30m@igor_chubin\033[0m para as novidades de wttr.in',
@ -740,12 +791,14 @@ CAPTION = {
"eo": u"Veterprognozo por:",
"es": u"Pronóstico del tiempo en:",
"et": u"Ilmaprognoos:",
"eu": u"Eguraldiaren iragarpena:",
"fa": u"گزارش آب و هئا برای شما:",
"fi": u"Säätiedotus:",
"fr": u"Prévisions météo pour:",
"fy": u"Waarberjocht foar:",
"ga": u"Réamhaisnéis na haimsire do:",
"he": u":ריוואה גזמ תיזחת",
"hi": u"मौसम की जानकारी",
"hr": u"Vremenska prognoza za:",
"hu": u"Időjárás előrejelzés:",
"hy": u"Եղանակի տեսություն:",
@ -766,6 +819,7 @@ CAPTION = {
"nb": u"Værmelding for:",
"nl": u"Weerbericht voor:",
"nn": u"Vêrmelding for:",
"oc": u"Previsions metèo per :",
"pl": u"Pogoda w:",
"pt": u"Previsão do tempo para:",
"pt-br": u"Previsão do tempo para:",

View File

@ -22,24 +22,26 @@ V2_TRANSLATION = {
"eo": ("Veterprognozo por:", "Vetero", "Horzono", "Nun", "Tagiĝo", "Sunleviĝo", "Zenito", "Sunsubiro", "Krepusko"),
"es": ("El tiempo en:", "Clima", "Zona horaria", "Ahora", "Alborada", "Amanecer", "Cenit", "Atardecer", "Anochecer"),
"et": ("Ilmaprognoos:", "Ilm", "Ajatsoon", "Hetkel", "Koit", "Päikesetõus", "Seniit", "Päikeseloojang", "Eha"),
"eu": ("Eguraldia:", "Ordu-eremua", "Orain", "Egunsentia", "Eguzkia", "Zenit", "Ilunabarra", "Ilunabarra"),
"fa": ("اوه و بآ تیعضو شرازگ", "", "", "", "", "", "", "", ""),
"fi": ("Säätiedotus:", "", "", "", "", "", "", "", ""),
"fr": ("Prévisions météo pour :", "Météo", "Fuseau Horaire", "Heure", "Aube", "Lever du Soleil", "Zénith", "Coucher du Soleil", "Crépuscule"),
"fy": ("Waarberjocht foar:", "", "", "", "", "", "", "", ""),
"ga": ("Réamhaisnéis na haimsire do:", "", "", "", "", "", "", "", ""),
"he": (":ריוואה גזמ תיזחת", "", "", "", "", "", "", "", ""),
"hi": ("मौसम की जानकारी", "मौसम", "समय मण्डल", "अभी", "उदय", "सूर्योदय", "चरम बिन्दु", "सूर्यास्त", "संध्याकाल"),
"hr": ("Vremenska prognoza za:", "", "", "", "", "", "", "", ""),
"hu": ("Időjárás előrejelzés:", "", "", "", "", "", "", "", ""),
"hu": ("Időjárás előrejelzés:", "Időjárás", "időzóna", "aktuális", "hajnal", "napkelte", "dél", "naplemente", "szürkület"),
"hy": ("Եղանակի տեսություն:", "", "", "", "", "", "", "", ""),
"ia": ("Reporto tempore pro:", "Tempore", "Fuso Horari", "Alora", "Alba", "Aurora", "Zenit", "Poner del sol", "Crepusculo"),
"id": ("Prakiraan cuaca:", "", "", "", "", "", "", "", ""),
"it": ("Previsioni meteo:", "", "", "", "", "", "", "", ""),
"it": ("Previsioni meteo:", "Tempo", "Fuso orario", "Ora", "Alba", "Sorgere del Sole", "Zenit", "Tramonto", "Crepuscolo"),
"is": ("Veðurskýrsla fyrir:", "", "", "", "", "", "", "", ""),
"ja": ("天気予報:", "天気", "タイムゾーン", "", "夜明け", "日の出", "天頂", "日の入", "日暮れ"),
"jv": ("Weather forecast for:", "", "", "", "", "", "", "", ""),
"ka": ("ამინდის პროგნოზი:", "", "", "", "", "", "", "", ""),
"kk": ("Ауа райы:", "", "", "", "", "", "", "", ""),
"ko": ("일기 예보:", "", "", "", "", "", "", "", ""),
"ko": ("일기 예보:", "날씨", "시간대", "현재", "새벽", "일출", "정오", "일몰", "황혼"),
"ky": ("Аба ырайы:", "", "", "", "", "", "", "", ""),
"lt": ("Orų prognozė:", "", "", "", "", "", "", "", ""),
"lv": ("Laika ziņas:", "", "", "", "", "", "", "", ""),
@ -48,6 +50,7 @@ V2_TRANSLATION = {
"nb": ("Værmelding for:", "", "", "", "", "", "", "", ""),
"nl": ("Weerbericht voor:", "Weer", "Tijdzone", "Nu", "Dageraad", "Zonsopkomst", "Zenit", "Zonsondergang", "Schemering"),
"nn": ("Vêrmelding for:", "", "", "", "", "", "", "", ""),
"oc": ("Previsions metèo per:" "Metèo", "Zòna orària", "Ara", "Auròra", "Alba", "Zenit", "A solelh colc", "Entrelutz"),
"pl": ("Pogoda w:", "", "", "", "", "", "", "", ""),
"pt": ("Previsão do tempo para:", "Tempo", "Fuso horário", "Agora", "Alvorada", "Nascer do sol", "Zénite", "Pôr do sol", "Crepúsculo"),
"pt-br": ("Previsão do tempo para:", "Tempo", "Fuso horário", "Agora", "Alvorada", "Nascer do sol", "Zénite", "Pôr do sol", "Crepúsculo"),
@ -69,5 +72,3 @@ V2_TRANSLATION = {
"zh-tw": ("天氣預報:", "天氣", "時區", "目前", "黎明", "日出", "日正當中", "日落", "黃昏"),
"zu": ("Isimo sezulu:", "", "", "", "", "", "", "", ""),
}

View File

@ -28,12 +28,13 @@ import pytz
from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION, WEATHER_SYMBOL_WIDTH_VTE
from weather_data import get_weather_data
from . import v2
from . import prometheus
PRECONFIGURED_FORMAT = {
'1': u'%c %t',
'2': u'%c 🌡️%t 🌬️%w',
'3': u'%l: %c %t',
'4': u'%l: %c 🌡️%t 🌬️%w',
'1': r'%c %t\n',
'2': r'%c 🌡️%t 🌬️%w\n',
'3': r'%l: %c %t\n',
'4': r'%l: %c 🌡️%t 🌬️%w\n',
}
MOON_PHASES = (
@ -60,6 +61,21 @@ def render_temperature(data, query):
return temperature
def render_feel_like_temperature(data, query):
"""
feel like temperature (f)
"""
if query.get('use_imperial', False):
temperature = u'%s°F' % data['FeelsLikeF']
else:
temperature = u'%s°C' % data['FeelsLikeC']
if temperature[0] != '-':
temperature = '+' + temperature
return temperature
def render_condition(data, query):
"""Emoji encoded weather condition (c)
"""
@ -145,7 +161,7 @@ def render_wind(data, query):
degree = ""
if degree:
wind_direction = WIND_DIRECTION[((degree+22)%360)//45]
wind_direction = WIND_DIRECTION[int(((degree+22.5)%360)/45.0)]
else:
wind_direction = ""
@ -224,6 +240,7 @@ FORMAT_SYMBOL = {
'C': render_condition_fullname,
'h': render_humidity,
't': render_temperature,
'f': render_feel_like_temperature,
'w': render_wind,
'l': render_location,
'm': render_moonphase,
@ -288,7 +305,7 @@ def render_line(line, data, query):
return ''
template_regexp = r'%[^%]*[a-zA-Z]'
template_regexp = r'%[a-zA-Z]'
for template_code in re.findall(template_regexp, line):
if template_code.lstrip("%") in FORMAT_SYMBOL_ASTRO:
local_time_of = get_local_time_of()
@ -297,7 +314,7 @@ def render_line(line, data, query):
return re.sub(template_regexp, render_symbol, line)
def render_json(data):
output = json.dumps(data, indent=4, sort_keys=True)
output = json.dumps(data, indent=4, sort_keys=True, ensure_ascii=False)
output = "\n".join(
re.sub('"[^"]*worldweatheronline[^"]*"', '""', line) if "worldweatheronline" in line else line
@ -320,6 +337,8 @@ def format_weather_data(query, parsed_query, data):
if format_line == "j1":
return render_json(data['data'])
if format_line == "p1":
return prometheus.render_prometheus(data['data'])
if format_line[:2] == "v2":
return v2.main(query, parsed_query, data)
@ -339,7 +358,7 @@ def wttr_line(query, parsed_query):
data = get_weather_data(location, lang)
output = format_weather_data(query, parsed_query, data)
return output.rstrip("\n")+"\n"
return output.rstrip("\n").replace(r"\n", "\n")
def main():
"""

View File

@ -15,6 +15,7 @@ def get_moon(parsed_query):
location = parsed_query['orig_location']
html = parsed_query['html_output']
lang = parsed_query['lang']
hemisphere = parsed_query['hemisphere']
date = None
if '@' in location:
@ -25,6 +26,9 @@ def get_moon(parsed_query):
if lang:
cmd += ["-l", lang]
if not hemisphere:
cmd += ["-s", "south"]
if date:
try:
dateutil.parser.parse(date)

69
lib/view/prometheus.py Normal file
View File

@ -0,0 +1,69 @@
"""
Rendering weather data in the Prometheus format.
"""
from datetime import datetime
from fields import DESCRIPTION
def _render_current(data, for_day="current", already_seen=[]):
"Converts data into prometheus style format"
output = []
for field_name, val in DESCRIPTION.items():
help, name = val
try:
value = data[field_name]
if field_name == "weatherDesc":
value = value[0]["value"]
except (IndexError, KeyError):
try:
value = data["astronomy"][0][field_name]
if value.endswith(" AM") or value.endswith(" PM"):
value = _convert_time_to_minutes(value)
except (IndexError, KeyError, ValueError):
continue
try:
if name == "observation_time":
value = _convert_time_to_minutes(value)
except ValueError:
continue
description = ""
try:
float(value)
except ValueError:
description = f", description=\"{value}\""
value = "1"
if name not in already_seen:
output.append(f"# HELP {name} {help}")
already_seen.append(name)
output.append(f"{name}{{forecast=\"{for_day}\"{description}}} {value}")
return "\n".join(output)+"\n"
def _convert_time_to_minutes(time_str):
"Convert time from midnight to minutes"
return int((datetime.strptime(time_str, "%I:%M %p")
- datetime.strptime("12:00 AM", "%I:%M %p")).total_seconds())//60
def render_prometheus(data):
"""
Convert `data` into Prometheus format
and return it as string.
"""
already_seen = []
answer = _render_current(
data["current_condition"][0], already_seen=already_seen)
for i in range(3):
answer += _render_current(
data["weather"][i], for_day="%sd" % i, already_seen=already_seen)
return answer

View File

@ -38,7 +38,7 @@ from astral import moon, sun
from scipy.interpolate import interp1d
from babel.dates import format_datetime
from globals import WWO_KEY
from globals import WWO_KEY, remove_ansi
import constants
import translations
import parse_query
@ -239,7 +239,7 @@ def draw_time(geo_data):
# }}}
# draw_astronomical {{{
def draw_astronomical(city_name, geo_data):
def draw_astronomical(city_name, geo_data, config):
datetime_day_start = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
city = LocationInfo()
@ -289,12 +289,18 @@ def draw_astronomical(city_name, geo_data):
answer += char
if config.get("view") in ["v2n", "v2d"]:
moon_phases = constants.MOON_PHASES_WI
moon_phases = [" %s" % x for x in moon_phases]
else:
moon_phases = constants.MOON_PHASES
# moon
if time_interval in [0,23,47,69]: # time_interval % 3 == 0:
moon_phase = moon.phase(
date=datetime_day_start + datetime.timedelta(hours=time_interval))
moon_phase_emoji = constants.MOON_PHASES[
int(math.floor(moon_phase*1.0/28.0*8+0.5)) % len(constants.MOON_PHASES)]
moon_phase_emoji = moon_phases[
int(math.floor(moon_phase*1.0/28.0*8+0.5)) % len(moon_phases)]
# if time_interval in [0, 24, 48, 69]:
moon_line += moon_phase_emoji # + " "
elif time_interval % 3 == 0:
@ -309,19 +315,29 @@ def draw_astronomical(city_name, geo_data):
return answer
# }}}
# draw_emoji {{{
def draw_emoji(data):
def draw_emoji(data, config):
answer = ""
if config.get("view") == "v2n":
weather_symbol = constants.WEATHER_SYMBOL_WI_NIGHT
weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE_WI
elif config.get("view") == "v2d":
weather_symbol = constants.WEATHER_SYMBOL_WI_NIGHT
weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE_WI
else:
weather_symbol = constants.WEATHER_SYMBOL
weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE
for i in data:
emoji = constants.WEATHER_SYMBOL.get(
emoji = weather_symbol.get(
constants.WWO_CODE.get(
str(int(i)), "Unknown"))
space = " "*(3-constants.WEATHER_SYMBOL_WIDTH_VTE.get(emoji))
answer += emoji + space
space = " "*(3-weather_symbol_width_vte.get(emoji, 1))
answer += space[:1] + emoji + space[1:]
answer += "\n"
return answer
# }}}
# draw_wind {{{
def draw_wind(data, color_data):
def draw_wind(data, color_data, config):
def _color_code_for_wind_speed(wind_speed):
@ -346,11 +362,16 @@ def draw_wind(data, color_data):
answer = ""
answer_line2 = ""
if config.get("view") in ["v2n", "v2d"]:
wind_direction_list = constants.WIND_DIRECTION_WI
else:
wind_direction_list = constants.WIND_DIRECTION
for j, degree in enumerate(data):
degree = int(degree)
if degree:
wind_direction = constants.WIND_DIRECTION[((degree+22)%360)//45]
wind_direction = wind_direction_list[int(((degree+22.5)%360)/45.0)]
else:
wind_direction = ""
@ -430,14 +451,14 @@ def generate_panel(data_parsed, geo_data, config):
output += "\n"
data = jq_query(weather_code_query, data_parsed)
output += draw_emoji(data)
output += draw_emoji(data, config)
data = jq_query(wind_direction_query, data_parsed)
color_data = jq_query(wind_speed_query, data_parsed)
output += draw_wind(data, color_data)
output += draw_wind(data, color_data, config)
output += "\n"
output += draw_astronomical(config["location"], geo_data)
output += draw_astronomical(config["location"], geo_data, config)
output += "\n"
output = add_frame(output, max_width, config)
@ -604,6 +625,8 @@ def main(query, parsed_query, data):
output = generate_panel(data_parsed, geo_data, parsed_query)
if query.get("text") != "no" and parsed_query.get("text") != "no":
output += textual_information(data_parsed, geo_data, parsed_query)
if parsed_query.get('no-terminal', False):
output = remove_ansi(output)
return output
if __name__ == '__main__':

View File

@ -40,7 +40,10 @@ def get_wetter(parsed_query):
location_not_found = True
stdout += get_message('NOT_FOUND_MESSAGE', lang)
first_line, stdout = _wego_postprocessing(location, parsed_query, stdout)
if "\n" in stdout:
first_line, stdout = _wego_postprocessing(location, parsed_query, stdout)
else:
first_line = ""
if html:

View File

@ -17,7 +17,7 @@ import fmt.png
import parse_query
from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS
from buttons import add_buttons
from globals import get_help_file, \
from globals import get_help_file, remove_ansi, \
BASH_FUNCTION_FILE, TRANSLATION_FILE, LOG_FILE, \
NOT_FOUND_LOCATION, \
MALFORMED_RESPONSE_HTML_PAGE, \
@ -221,7 +221,7 @@ def _response(parsed_query, query, fast_mode=False):
#
# output = fmt.png.render_ansi(
# output, options=parsed_query)
result = TASKS.spawn(fmt.png.render_ansi, output, options=parsed_query)
result = TASKS.spawn(fmt.png.render_ansi, cache._update_answer(output), options=parsed_query)
output = result.get()
else:
if query.get('days', '3') != '0' \
@ -230,7 +230,10 @@ def _response(parsed_query, query, fast_mode=False):
if parsed_query['html_output']:
output = add_buttons(output)
else:
output += '\n' + get_message('FOLLOW_ME', parsed_query['lang']) + '\n'
message = get_message('FOLLOW_ME', parsed_query['lang'])
if parsed_query.get('no-terminal', False):
message = remove_ansi(message)
output += '\n' + message + '\n'
return cache.store(cache_signature, output)
@ -289,9 +292,10 @@ def parse_request(location, request, query, fast_mode=False):
parsed_query["lang"] = parsed_query.get("lang", lang)
parsed_query["html_output"] = get_output_format(query, parsed_query)
parsed_query["json_output"] = (parsed_query.get("view", "") or "").startswith("j")
if not fast_mode: # not png_filename and not fast_mode:
location, override_location_name, full_address, country, query_source_location = \
location, override_location_name, full_address, country, query_source_location, hemisphere = \
location_processing(parsed_query["location"], parsed_query["ip_addr"])
us_ip = query_source_location[1] == 'United States' \
@ -306,7 +310,8 @@ def parse_request(location, request, query, fast_mode=False):
'override_location_name': override_location_name,
'full_address': full_address,
'country': country,
'query_source_location': query_source_location})
'query_source_location': query_source_location,
'hemisphere': hemisphere})
parsed_query.update(query)
return parsed_query
@ -318,7 +323,7 @@ def wttr(location, request):
it returns output in HTML, ANSI or other format.
"""
def _wrap_response(response_text, html_output, png_filename=None):
def _wrap_response(response_text, html_output, json_output, png_filename=None):
if not isinstance(response_text, str) and \
not isinstance(response_text, bytes):
return response_text
@ -337,16 +342,21 @@ def wttr(location, request):
response.headers[key] = value
else:
response = make_response(response_text)
response.mimetype = 'text/html' if html_output else 'text/plain'
if html_output:
response.mimetype = "text/html"
elif json_output:
response.mimetype = "application/json"
else:
response.mimetype = "text/plain"
return response
if is_location_blocked(location):
return ""
return ("", 403) # Forbidden
try:
LIMITS.check_ip(_client_ip_address(request))
except RuntimeError as exception:
return str(exception)
return (str(exception), 429) # Too many requests
query = parse_query.parse_query(request.args)
@ -356,6 +366,8 @@ def wttr(location, request):
# use the full track
parsed_query = parse_request(location, request, query, fast_mode=True)
response = _response(parsed_query, query, fast_mode=True)
http_code = 200
try:
if not response:
parsed_query = parse_request(location, request, query)
@ -365,15 +377,17 @@ def wttr(location, request):
logging.error("Exception has occured", exc_info=1)
if parsed_query['html_output']:
response = MALFORMED_RESPONSE_HTML_PAGE
http_code = 500 # Internal Server Error
else:
response = get_message('CAPACITY_LIMIT_REACHED', parsed_query['lang'])
http_code = 503 # Service Unavailable
# if exception is occured, we return not a png file but text
if "png_filename" in parsed_query:
del parsed_query["png_filename"]
return _wrap_response(
response, parsed_query['html_output'],
png_filename=parsed_query.get('png_filename'))
return (_wrap_response(
response, parsed_query['html_output'], parsed_query['json_output'],
png_filename=parsed_query.get('png_filename')), http_code)
if __name__ == "__main__":
import doctest

View File

@ -8,6 +8,7 @@ pylint
cyrtranslit
astral
timezonefinder==2.1.2
pytz
pyte
python-dateutil
diagram

View File

@ -29,7 +29,7 @@ nürnberg : Nuremberg
norway : Oslo
ville de bruxelles - stad brussel: Brussels
frankfurt am main:Frankfurt, Hessen
frankfurt :Frankfurt, Brandenburg
frankfurt :Frankfurt, Hessen
frankfurt oder : Frankfurt, Brandenburg
frankfurt (oder): Frankfurt, Brandenburg
tel-aviv : Tel Aviv

View File

@ -4,19 +4,19 @@ logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:srv]
command=python /app/bin/srv.py
command=python3 /app/bin/srv.py
stderr_logfile=/var/log/supervisor/srv-stderr.log
stdout_logfile=/var/log/supervisor/srv-stdout.log
[program:proxy]
command=python /app/bin/proxy.py
command=python3 /app/bin/proxy.py
stderr_logfile=/var/log/supervisor/proxy-stderr.log
stdout_logfile=/var/log/supervisor/proxy-stdout.log
[program:geoproxy]
command=python /app/bin/geo-proxy.py
command=python3 /app/bin/geo-proxy.py
stderr_logfile=/var/log/supervisor/geoproxy-stderr.log
stdout_logfile=/var/log/supervisor/geoproxy-stdout.log
[include]
files=/etc/supervisor/conf.d/*.conf
files=/etc/supervisor/conf.d/*.conf

View File

@ -6,7 +6,7 @@ Usage:
Supported location types:
/paris # city name
/~Eiffel+tower # any location
/~Eiffel+tower # any location (+ for spaces)
/Москва # Unicode name of any location in any language
/muc # airport code (3 letters)
/@stackoverflow.com # domain name

View File

@ -31,10 +31,12 @@ Translated/improved/corrected by:
* German: Igor Chubin, @MAGICC (https://kthx.at)
* Greek: Panayotis Vryonis and @Petewg (on github)
* Hebrew: E.R.
* Hindi: Aakash Gajjar @skyme5 (gh)
* Hungarian: Mark Petruska
* Icelandic: Óli G. @dvergur, Skúli Arnlaugsson @Arnlaugsson
* Indonesian: Andria Arisal @andria009
* Interlingua: Dustin Redmond @dustinkredmond (on github)
* Irish: Robert Devereux @RobertDev (gh), Conor O'Callaghan @ivernus (gh)
* Italian: Diego Maniacco
* Japanese: @ryunix
* Kazakh: Akku Tutkusheva, Oleg Tropinin
@ -43,6 +45,7 @@ Translated/improved/corrected by:
* Macedonian: Matej Plavevski @MatejMecka
* Norwegian: Fredrik Fjeld @fredrikfjeld
* Nynorsk: Kevin Brubeck Unhammer (https://unhammer.org/k/)
* Occitan: Quentin PAGÈS @Quenty-tolosan (gh)
* Persian: Javad @threadripper_
* Polish: Wojtek Łukasiewicz @wojtuch (on github)
* Portuguese: Fernando Bitti Loureiro @fbitti (on github)

View File

@ -1,14 +1,14 @@
Argibideak:
$ curl wttr.in # eguraldia zure kokapenean
$ curl wttr.in/muc # eguraldia Municheko aireportuan
$ curl wttr.in/bio # eguraldia Bilboko aireportuan
Onartzen diren kokapen motak:
/paris # hiri baten izena
/~Eiffel+tower # leku famatu baten izena
/bilbao # hiri baten izena
/~Bilbao+Guggenheim+museum # leku famatu baten izena
/Москва # Edozein lekuko edozein hizkuntzako Unicode izena
/muc # aeroportu baten kodea (3 letra)
/bio # aeroportu baten kodea (3 letra)
/@stackoverflow.com # web domeinu baten izena
/94107 # area kode bat
/-78.46,106.79 # GPS koordenadak

81
share/translations/ga.txt Normal file
View File

@ -0,0 +1,81 @@
113: Geal : Clear
113: Grianmhar : Sunny
116: Breacscamallach : Partly cloudy
119: Scamallach : Cloudy
122: Scamallach : Overcast
143: Ceochán : Mist
176: Ceathanna : Patchy rain possible
179: Ceathanna sneachta : Patchy snow possible
182: Ceathanna flichshneachta : Patchy sleet possible
185: Ceathanna cloichshneachta : Patchy freezing drizzle possible
200: Ceathanna toirní : Thundery outbreaks possible
227: Stealladh sneachta : Blowing snow
230: Síobadh sneachta : Blizzard
248: Ceo : Fog
260: Ceo reoite : Freezing fog
263: Ceobhrán : Patchy light drizzle
266: Ceobhrán éadrom : Light drizzle
281: Ceobhrán reoite : Freezing drizzle
284: Ceobhrán reoite trom : Heavy freezing drizzle
293: Breachbháisteach éadrom : Patchy light rain
296: Báisteach éadrom : Light rain
299: Báisteach mheasartha uaireanta : Moderate rain at times
302: Báisteach mheasartha : Moderate rain
305: Báisteach throm uairanta : Heavy rain at times
308: Báisteach throm : Heavy rain
311: Báisteach reoite éadrom : Light freezing rain
314: Báisteach mheasartha nó reoite throm : Moderate or heavy freezing rain
317: Flichshneachta éadrom : Light sleet
320: Flichshneachta measartha nó trom : Moderate or heavy sleet
323: Breacsneachta éadrom : Patchy light snow
326: Sneachta éadrom : Light snow
329: Breacsneachta measartha : Patchy moderate snow
332: Sneachta measartha : Moderate snow
335: Sneachta trom uaireanta : Patchy heavy snow
338: Sneachta trom : Heavy snow
350: Chloichshneachta : Ice pellets
353: Cith éadrom : Light rain shower
356: Cith measartha no trom : Moderate or heavy rain shower
359: Duartan : Torrential rain shower
362: Ceathanna flichshneachta : Light sleet showers
365: Ceathanna flichshneachta measartha nó troma : Moderate or heavy sleet showers
368: Ceathanna sneachta éadroma : Light snow showers
371: Ceathanna sneachta measartha nó troma : Moderate or heavy snow showers
386: Ceathanna éadroma le toirneach : Patchy light rain with thunder
389: Báisteach measartha nó throm le tóirneach : Moderate or heavy rain with thunder
392: Sneachta éadrom le toirneach : Patchy light snow with thunder
395: Sneachta measartha nó trom le toirneach : Moderate or heavy snow with thunder

View File

@ -1,3 +1,4 @@
: Kiša : Rain
113 : Vedro : Clear
113 : Sunčano : Sunny
116 : Djelomično oblačno : Partly cloudy

View File

@ -0,0 +1,66 @@
Usatge:
$ curl wttr.in # emplaçament actual
$ curl wttr.in/cdg # metèo a l'aeropòrt de Paris - Charles de Gaulle
Types d'emplacements acceptés:
/toulouse # nom de la vila
/~Eiffel+tower # emplaçament qual que siá
/Москва # nom Unicode o emplaçament qual que siá dins quala que siá lenga
/muc # còde aeropòrt (3 letras)
/@stackoverflow.com # nom de domeni
/94107 # còde postal (sonque pels Estats Units)
/-78.46,106.79 # coordenadas GPS
Emplacements particuliers:
/moon # passas de la luna (ajustar ,+US o ,+France per accedir a las vilas del meteis nom)
/moon@2016-10-25 # passas de la luna per aquesta data (@2016-10-25)
Unitats:
?m # sistèma metric (per defaut pertot levat als Estats Units d d'America)
?u # USCS (per defaut pels Estats-Units d'America)
?M # afichar la velocitat del vent en m/s
Opcion d'afichatge:
?0 # uèi solament
?1 # uèi + deman
?2 # uèi + 2 jorns
?n # version corta (sonque pel jorn e la nuèch)
?q # version silenciosa (cap d'entèsta "Previsions metèo per")
?Q # version super-silencieuse (pas d'en-tête "Prévisions météo pour", pas de nom de la ville)
?T # sequéncias d'ecapament pels terminals desactivadas (cap de colors)
Opcions PNG:
/paris.png # gnèra un fichièr PNG
?p # ajuta un quadre altorn de la sortida
?t # transparéncia 150 (transparence 150)
transparency=... # transparéncia de 0 fins a 255 (255 = cap de transparéncia)
Combinar las opcions:
/Toulouse?0pq
/Toulouse?0pq&lang=oc
/Toulouse_0pq.png # dins lo mòde PNG las opcions son especificadas après _
/Rome_0pq_lang=it.png # las opcions longas son separadas per de underscores _
Localizacion:
$ curl fr.wttr.in/Toulouse
$ curl wttr.in/toulouse?lang=oc
$ curl -H "Accept-Language: oc" wttr.in/toulouse
Lengas suportadas:
FULL_TRANSLATION (Support complèt)
PARTIAL_TRANSLATION (Support incomplèt)
URLs particularas:
/:help # mostra aquesta pagina
/:bash.function # foncion bash recomandada wttr()
/:translation # mostra las informacions sus la traduccion de wttr.in

50
share/translations/oc.txt Normal file
View File

@ -0,0 +1,50 @@
: Pluèja : Rain
: Plugeta, Raissas : Light Rain, Rain Shower
: Raissas : Rain Shower
113 : Temps clar : Clear
113 : Solelh : Sunny
116 : Nivolós en partida : Partly cloudy
119 : Nivolós : Cloudy
122 : Ennivolat : Overcast
143 : Brumós : Mist
176 : Possiblas raissas esparpalhadas : Patchy rain possible
179 : Possiblas nevadas esparpalhadas : Patchy snow possible
182 : Possiblas nèus fondudas esparpalhadas : Patchy sleet possible
185 : Possiblas plovinas gelibrantas esparpalhadas : Patchy freezing drizzle possible
200 : Auratges possibles : Thundery outbreaks possible
227 : Nèu e vent : Blowing snow
230 : Blisard : Blizzard
248 : Fums : Fog
260 : Nèbla gelibranta : Freezing fog
263 : Plovina leugièra esparpalhada : Patchy light drizzle
266 : Plovina leugièra : Light drizzle
281 : Plovina gelibranta : Freezing drizzle
284 : Fòrta plovina gelibranta : Heavy freezing drizzle
293 : Plugeta fina : Patchy light rain
296 : Plovina : Light rain
299 : Pluèja moderada intermitenta : Moderate rain at times
302 : Pluèja moderada : Moderate rain
305 : Forte pluie intermittente : Heavy rain at times
308 : Raissa : Heavy rain
311 : Pluie verglaçante légère : Light freezing rain
314 : Pluie verglaçante modérée à forte : Moderate or heavy freezing rain
317 : Aiganèu leugièra : Light sleet
320 : Aiganèu moderada o fòrta : Moderate or heavy sleet
323 : Nevada esparpalhada e leugièra : Patchy light snow
326 : Nevada leugièra : Light snow
329 : Nevada esparpalhada parciala : Patchy moderate snow
332 : Nevada moderada : Moderate snow
335 : Nevada fòrta parciala : Patchy heavy snow
338 : Nevada fòrta : Heavy snow
350 : Pèiras de glaça : Ice pellets
353 : Pluèja leugièra : Light rain shower
356 : Pluèja moderada a fòrta : Moderate or heavy rain shower
359 : Pluèja torrenciala : Torrential rain shower
362 : Granissa leugièra : Light sleet showers
365 : Granissa moderada a fòrta : Moderate or heavy sleet showers
368 : Nevadas leugièra : Light snow showers
371 : Granissa moderada a violenta : Moderate or heavy snow showers
386 : Pluèja auratjosa leugièra esparpalhada : Patchy light rain with thunder
389 : Pluèja auratjosa moderada a violenta : Moderate or heavy rain with thunder
392 : Nevada auratjosa esparpalhada e leugièra : Patchy light snow with thunder
395 : Nevada auratjosa moderada a violenta : Moderate or heavy snow with thunder

View File

@ -1,3 +1,4 @@
: Проливной дождь : Rain shower :
113: Ясно : Clear :
113: Солнечно : Sunny :
116: Переменная облачность : Partly cloudy :

File diff suppressed because it is too large Load Diff