Vevor 7 In 1 Weather Station - send data to Windy and Home Assistant
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