Vevor 7 In 1 Weather Station - send data to Windy and Home Assistant

Page content

Vevor 7 in 1 Wifi to Windy and HomeAssistant

For a long time i wanted to install a weather station but since
i have to put it on a roof which is easily accessded by outside
people i needed something on the budget side.

So during a recent Ali promotion i got one - Vevor 7 In 1 WiFi
model.

You need the WiFi version, there non-WiFi version which you can work
with too but you need to have RTL SDR dongles and recode radio data send
from the outdoor unit to the sensor. This post is about the WiFi only.

So the WiFi connection is used to send data to two fixed weather data services:

  • weatherunderground
  • weathercloud

But i am a fan of Windy so i wanted to send and use my data there.

To send data to Windy we have to capture the requests to one of the two
services and forward them to Windy instead.

But how to do it without modifing the station itself.

We need to intercept the requests and send captured data to windy instead.
Here to the rescue comes DNS spoofing - it will trick the weather station client
to connect to our web service instead which in turn will process and
send the request to windy.com.

Setup the station to connect to a wifi router which uses our custom DNS,
which we will teach to spoof some addresses.
Since i am Ubuntu user and it has Dnsmasq ready it is a simple config.

Install dnsmasq and resolvconf if not installed already:

sudo apt-get install dnsmasq resolvconf

Comment out dns=dnsmasq in /etc/NetworkManager/NetworkManager.conf
Stop Dnsmasq with sudo killall -9 dnsmasq

After configuring Dnsmasq, restart Network Manager with sudo service network-manager restart

Next prapare dnsmasq.conf and add at the end:

address=/.weathercloud.net/192.168.0.X
address=/.weathercloud.net/fe80::216:3eff:fe26:c31d
address=/.weathercloud.online/192.168.0.X
address=/.weathercloud.online/fe80::216:3eff:fe26:c31d
address=/.wunderground.com/192.168.0.X
address=/.wunderground.com/fe80::216:3eff:fe26:c31d

You can check for more details this guide in case of trouble.
https://www.linux.com/topic/networking/dns-spoofing-dnsmasq/

Setup station to send data do weatherunderground or weathercloud.

Now when the station gets its IP and DNS servers from your wifi router
it must provide the address of the server running dnsmasq with
that configuration.

When it tries to resolve wunderground.com it will get the address of
your web server 192.168.0.X.

Now it will try to send data to 192.168.0.X. This request must be intercepted
by a local webserver and then data captured and forwarded where we want it to go.

Edit: I’ve send the data to my home assistant too - most useful is UV index so i
can save on some light intensity sensors and know when to turn on lights if it is dark.

So let’s make a service:
create /etc/systemd/system/v7i1towindy.service

[Unit]
Description=Vevor 7 in 1 to Windy
After=syslog.target
After=network.target

[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/bin/python3 /home/username/bin/v7i1towindy.py

# Give the script some time to startup
TimeoutSec=300

[Install]
WantedBy=multi-user.target

And here comes the script to forward data, as added bonus i’ve send the data to my home assistant too.
You need to put your keys there:

/home/username/bin/v7i1towindy.py

#!/usr/bin/env python

from http.server import BaseHTTPRequestHandler, HTTPServer
import sys
from urllib.parse import parse_qs, urlencode, urlparse
import requests
import datetime
import datetime as dt
import zoneinfo as zi
import json

GMT = zi.ZoneInfo('GMT')
LTZ = zi.ZoneInfo('Europe/Sofia') # use correct timezone

WINDY_API_KEY = 'your_windy_key'

HASS_LLTOKEN = 'your_hass_token'

HASS_URL_BASE = 'http://127.0.0.1:8123/api/states/'


next_update = datetime.datetime.now()

last_req = None
#ave_wind = 0.0f
#ave_dir = 0.0f

def f2c(f):
    return (f - 32.0) * 5.0/9.0

class S(BaseHTTPRequestHandler):
    hass_map_imp = {
        "baromin" : ["Barometric Pressure", "atmospheric_pressure", "inHg" ],
        "tempf"   : ["Temperature", "temperature", "°F"],
        "humidity": ["Humidity", "humidity", "%" ],
        "dewptf"  : ["Dew Point", "temperature", "°F"],
        "rainin"  : ["Rainfall", "precipitation", "in" ],
        "dailyrainin"  : ["Daily Rainfall", "precipitation", "in" ],
        "winddir" : ["Wind Direction", "none", "°"],
        "windspeedmph" : ["Wind Speed", "wind_speed", "mph"],
        "windgustmph" : ["Wind Gust Speed", "wind_speed", "mph"],
        "uv" : ["UV Index", "none", "index"],
        "solarRadiation" : ["Solar Radiation", "irradiance", "W/m²"],
    }

    hass_map = {
        "baromin" : ["Barometric Pressure", "atmospheric_pressure", "hPa", 33.8639, 0 ],
        "tempf"   : ["Temperature", "temperature", "°C", f2c, 1],
        "humidity": ["Humidity", "humidity", "%" , None, 0],
        "dewptf"  : ["Dew Point", "temperature", "°C", f2c, 1],
        "rainin"  : ["Rainfall", "precipitation", "mm", 25.4, 1 ],
        "dailyrainin"  : ["Daily Rainfall", "precipitation", "mm", 25.4, 1 ],
        "winddir" : ["Wind Direction", "none", "°", None, 0],
        "windspeedmph" : ["Wind Speed", "wind_speed", "kmh", 1.609344, 0],
        "windgustmph" : ["Wind Gust Speed", "wind_speed", "kmh", 1.609344, 0],
        "uv" : ["UV Index", "none", "index", None, 0],
        "solarRadiation" : ["Solar Radiation", "irradiance", "W/m²", None, 0],
    }

    hass_idmap = {
        "baromin" : 0,
        "tempf"   : 1,
        "humidity": 2,
        "dewptf"  : 3,
        "rainin"  : 4,
        "dailyrainin"  : 5,
        "winddir" : 6,
        "windspeedmph" : 7,
        "windgustmph" : 8,
        "uv" : 9,
        "solarRadiation" : 10,
    }

    def __init__(self, a, b, c):
        super(S, self).__init__(a, b, c)

#    def log_message(self, format, *args):
#        return

    def _set_headers(self,msg=None):
        self.send_response(200, message=msg)
        self.send_header('Content-type', 'application/json')
        self.end_headers()

    def should_update(self):
        global next_update
        n = datetime.datetime.now()
        if n > next_update:
            return True
        return False

    def qs2hass(self, qs):
        format_data = "%Y-%m-%d %H:%M:%S"
        hass_format = '%Y-%m-%d %H:%M:%S'
        local_str = ""
        if 'dateutc' in qs:
            date = dt.datetime.strptime(qs['dateutc'], format_data)
            local_date = date.replace(tzinfo=GMT).astimezone(LTZ)
            local_str = local_date.strftime(hass_format)

        headers = {'Content-type': 'application/json', 'Accept': 'text/plain', 'Authorization' : 'Bearer ' + HASS_LLTOKEN }

        for k in qs:
            if k in self.hass_map:
                ret = {}
                el = self.hass_map[k]
                sensor_name = 'sensor.weather_station_1_' + el[0].lower().replace(' ', '_')
                url = HASS_URL_BASE + sensor_name
                val = qs[k]
                if el[3] is not None:
                    r = int(el[4])
                    if callable(el[3]):
                        fn = el[3]
                        val = fn(float(val))
                        val = round(val, r)
                        if r == 0:
                            val = int(val)
                    else:
                        val = float(val) * float(el[3])
                        val = round(val, r)
                        if r == 0:
                            val = int(val)
                ret['state'] = val # qs[k]
                ret['attributes'] = {
                    "friendly_name" : el[0],
                    "unit_of_measurement" : el[2],
                    "device_class" : el[1],
                    "measured on" : local_str,
                    "unique_id" : sensor_name + '_' + str(self.hass_idmap[k])
                }
                r = requests.post(url, data=json.dumps(ret), headers=headers)
                # print(r.text)


    def qs2windy(self, qs):
        url = 'https://stations.windy.com/pws/update/' + WINDY_API_KEY + '?'
        '''
https://stations.windy.com/pws/update/XXX-API-KEY-XXX?winddir=230&windspeedmph=12&windgustmph=12&tempf=70&rainin=0&baromin=29.1&dewptf=68.2&humidity=90

dateutc=2024-12-5+21%3A0%3A9&baromin=30.54&tempf=42.5&humidity=84&dewptf=38.0&rainin=0&dailyrainin=0.25&
winddir=57&windspeedmph=0&windgustmph=0&UV=0&solarRadiation=0
        '''

        # winddir=230&windspeedmph=12&windgustmph=12&tempf=70&rainin=0&baromin=29.1&dewptf=68.2&humidity=90

        # la = pytz.timezone("Europe/Sofia")
        now = datetime.datetime.now(LTZ)
        dstoff = now.dst()
        if dstoff is not None and dstoff > datetime.timedelta(0) and 'dateutc' in qs:
            format_data = "%Y-%m-%d %H:%M:%S"
            ''' There is a bug in vevor that does not apply dst offset , it applies just TZ '''
            print('%s' % qs['dateutc'][0])
            date = dt.datetime.strptime(qs['dateutc'][0], format_data)
            fix_date = date - dstoff
            qs['dateutc'] = [ fix_date.strftime(format_data) ]

        if 'ID' in qs:
            del qs['ID']

        if 'PASSWORD' in qs:
            del qs['PASSWORD']

        for k in qs:
            qs[k] = qs[k][0]

        qs['station'] = '0'

        # bad orientation no wind from north
        # 0 - north, 90 - east, 180 south , 270 west


        qs['uv'] = qs['UV']
        del qs['UV']
        if 'winddir' in qs:
            wd = int(qs['winddir']) # - 5
            if wd >= 359:
                wd = wd - 359
            if wd < 0:
                wd = wd + 359
            qs['winddir'] = wd
        if 'windspeedmph' in qs:
            ws = float(qs['windspeedmph'])
            if ws > 121.0:
                del qs['windspeedmph']
                del qs['windgustmph']
                del qs['winddir']
                print('Invalid wind data')

        if 'humidity' in qs and int(qs['humidity']) < 3:
            # print('Invalid humidity')
            return
        print('%s %s' % (url, qs))
        query_string = urlencode(qs)
        print('%s' % (url + query_string))
        # try:
        r= requests.get(
            url + query_string
        )
        if r.status_code == 200:
            global next_update
            upd = datetime.datetime.now()
            next_update = upd + datetime.timedelta(minutes=5)
#            print('200: upd at %s, next at %s' % (upd, next_update))
        else:
            print(r.status_code)
            print(r.text)
        self.qs2hass(qs)

    def do_GET(self):
        if self.should_update():
            o = urlparse(self.path)
            qs = parse_qs(o.query)
            self.qs2windy(qs)
        self._set_headers(msg="success")
        self.wfile.write(b"")

    def do_HEAD(self):
        self._set_headers()

    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length)
        print(str(post_data))
        self._set_headers()


def run(server_class=HTTPServer, handler_class=S, port=80):
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    print(    'Starting httpd...')
    httpd.serve_forever()


if __name__ == "__main__":
    from sys import argv

    if len(argv) == 2:
        run(port=int(argv[1]))
    else:
        run()

When ready do not forget to:

sudo systemctl enable v7i1towindy.service
sudo systemctl start v7i1towindy

Enjoy and make windy better - i am not associated just like the service :)


have fun,
z