Skip to content

Instantly share code, notes, and snippets.

@tsohr
Last active March 11, 2026 12:44
Show Gist options
  • Select an option

  • Save tsohr/b5277db45f58e97049d69946c1244002 to your computer and use it in GitHub Desktop.

Select an option

Save tsohr/b5277db45f58e97049d69946c1244002 to your computer and use it in GitHub Desktop.
CGS2 de-cloud weather station service py file fork version of https://github.com/ea/cgs2_decloud/blob/main/weather_server/weather_server.py changing weather data source to openweathermap.org
# 192.168.123.234 your server ip
192.168.123.234 mqtt.bj.cleargrass.com
192.168.123.234 gateway.cleargrass.com
192.168.123.234 qing.cleargrass.com
192.168.123.234 cn.ots.io.mi.com
192.168.123.234 cn.ot.io.mi.com
192.168.123.234 baidu.com
192.168.123.234 www.baidu.com
192.168.123.234 qingosapi.dev.cleargrass.com
192.168.123.234 vip3.alidns.com
192.168.123.234 vip4.alidns.com
[root@Qingping-Air-Monitor:/etc]# touch a
touch: cannot touch 'a': Read-only file system
[root@Qingping-Air-Monitor:/]# uname -a
Linux Qingping-Air-Monitor 4.19.232 #10 SMP Sun Jan 4 17:50:17 CST 2026 aarch64 GNU/Linux
cat > /oem/etc/hosts <<EOF
127.0.0.1 localhost
127.0.1.1 Qingping-Air-Monitor
192.168.123.234 mqtt.bj.cleargrass.com
192.168.123.234 gateway.cleargrass.com
192.168.123.234 qing.cleargrass.com
192.168.123.234 cn.ots.io.mi.com
192.168.123.234 cn.ot.io.mi.com
192.168.123.234 baidu.com
192.168.123.234 www.baidu.com
192.168.123.234 qingosapi.dev.cleargrass.com
192.168.123.234 vip3.alidns.com
192.168.123.234 vip4.alidns.com
EOF
echo "mount --bind /oem/etc/hosts /etc/hosts" >> /oem/bin/start.sh
cat > /data/etc/setting.ini <<EOF
[192.168.123.234]
save_history_interval=60
sync_history_interval=60
[application]
current_main_page_index=0
is_initialized=true
is_show_qingping_app_view=false
language=4096
poweroff_timestamp=1501888431
use_http=true
[battery]
is_show_battery_precentage=false
[datetime]
is_automatically=true
is_initialized=false
timezone=Asia/Shanghai
timezone_auto=Asia/Shanghai
[developer]
is_adbd_enabled=true
is_serial_enabled=false
[device]
miio_did=222222222
miio_key=1111111111111111
miio_mac=FF:FF:FF:FF:FF:FF
wifi_mac=58:FF:FF:FF:FF:FF
[host]
client_id="58FFFFFFFFFF|securemode=2,signmethod=hmacsha1|"
host=192.168.123.234
password=ffffffffffffffffffffffffffffffffffffffff
port=1883
pub_topic=/fffffffff/58FFFFFFFFFF/user/update
sub_topic=/fffffffff/58FFFFFFFFFF/user/get
tls=0
username=58FFFFFFFFFF&zTxDnQAGg
[location]
is_auto=true
[qingping]
is_iot_paired=false
is_paired=false
[screen]
auto_brightness=true
brightness_value=350
screen_off_charging_interval=99999
screen_off_discharge_interval=5
screen_saver=2
screen_saver_interval=5
[sensors]
co2_autocalib_enabled=true
co2_type=3
EOF
import ssl
import time
import threading
import json
import socketserver
import requests
from http.server import BaseHTTPRequestHandler, HTTPServer
from http import HTTPStatus
from socketserver import ThreadingMixIn
# --- Configuration ---
HOST = "0.0.0.0"
PORT = 80
LAT = 0#<deleted>
LON = 0#<deleted>
weather_data = None
city_id_placeholder = "<deleted>"
station_name = "<deleted>"
location_data = {
"city_id":"<deleted>","name":station_name,"name_cn":station_name,
"name_en":station_name,"name_cn_tw":station_name,"country":"<deleted>",
"country_cn":"<deleted>","country_en":"<deleted>","country_cn_tw":"<deleted>",
"area_cn_first":"<deleted>","area_cn_second":"<deleted>",
"area_first":"<deleted>","timezone":"<deleted>",
"timezone_gmt":"<deleted>","coordinate":{"longitude":"<deleted>","latitude":"<deleted>"}
}
pollution_data = None
openweather_api_key = "<deleted>"
# ---------------------
#source_json = current_data
def transform_weather_data(source_json, pollution_data):
"""
Transforms NWS-style weather observation JSON into a custom target format.
Args:
source_json (dict): The input dictionary in the NWS GeoJSON/LD format.
Returns:
dict: The transformed dictionary in the custom target format.
"""
# 1. Location Data
# Coordinate extraction: [longitude, latitude]
longitude = str(LON)
latitude = str(LAT)
# 2. Weather Data
# Temperature: value is in degC, convert to float
temperature_c = source_json["current"]['temp']
temperature = float(temperature_c) if temperature_c is not None else 0.0
# Relative Humidity: value is in percent
relative_humidity = source_json["current"]['humidity']
humidity = int(round(relative_humidity)) if relative_humidity is not None else 0
# Wind Speed: value is in km/h, convert to float
wind_speed_kmh = source_json["current"]['wind_speed']
wind_speed = float(wind_speed_kmh) if wind_speed_kmh is not None else 0.0
uv_index = source_json["current"]['uvi']
uv_index = float(uv_index) if uv_index is not None else 0.0
aqi = pollution_data["list"][0]["main"]["aqi"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0
co = pollution_data["list"][0]["components"]["co"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0
no = pollution_data["list"][0]["components"]["no"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0
no2 = pollution_data["list"][0]["components"]["no2"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0
o3 = pollution_data["list"][0]["components"]["o3"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0
so2 = pollution_data["list"][0]["components"]["so2"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0
pm10 = pollution_data["list"][0]["components"]["pm10"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0
pm25 = pollution_data["list"][0]["components"]["pm2_5"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0
nh3 = pollution_data["list"][0]["components"]["nh3"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0
hourly_forecast = source_json["hourly"][0]
pop = hourly_forecast['pop'] if 'pop' in hourly_forecast else 0.0
today_forecast = source_json["daily"][0]
temp_max = today_forecast['temp']['max']
temp_min = today_forecast['temp']['min']
# Wind Direction: value is in degrees, convert to a basic cardinal direction
wind_direction_deg = source_json["current"]['wind_deg']
wind_dir_text = "N/A"
if wind_direction_deg is not None:
# Simple cardinal direction logic (e.g., 0-22.5 is N, 22.5-67.5 is NE, etc.)
dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
index = int((wind_direction_deg % 360) / 22.5)
wind_dir_text = dirs[index]
text_description = source_json["current"]['weather'][0]['main'].lower()
skycon_map = {
"clear": "CLEAR_DAY", "sunny": "CLEAR_DAY", "partly cloudy": "PARTLY_CLOUDY_DAY",
"cloudy": "CLOUDY", "overcast": "CLOUDY", "rain": "RAIN",
"snow": "SNOW", "fog": "FOG"
}
def map_description(text_description,skycon_map):
for k in skycon_map.keys():
if k in text_description:
return skycon_map[k]
return skycon_map["clear"]
skycon = map_description(text_description,skycon_map)
pub_time = source_json["current"]['dt'] if 'dt' in source_json["current"] else int(time.time())
# Placeholder for the arbitrary city ID
timezone_offset_hours = source_json["timezone_offset"] // 3600
timezoneFmt = "UTC" + ("+" if timezone_offset_hours >= 0 else "") + str(timezone_offset_hours)
transformed_data = {
"city": {
"city": station_name,
"cityId": city_id_placeholder,
"cnAddress": {
"city": "",
"cityId": city_id_placeholder,
"country": "KR",
"province": ""
},
"cnCity": "",
"country": "KR", # Assumption based on weather.gov data
"enAddress": {
"city": station_name,
"cityId": city_id_placeholder,
"country": "KR",
"province": "" # Province/State is not directly mapped here
},
"latitude": latitude,
"longitude": longitude,
"name": station_name,
"name_cn": "",
"name_cn_tw": "",
"name_en": station_name,
"province": "",
# Placeholder: Timezone information is not provided in source.
"timezone": source_json["timezone"],
"timezoneFmt": timezoneFmt # Convert offset seconds to hours
},
"city_id": city_id_placeholder,
"weather": {
# --- AQI/Pollution (Placeholders, as this is NOT in the source data) ---
"aqi": aqi,
"aqi_day_max_cn": aqi,
"aqi_day_max_en": aqi,
"aqi_day_min_cn": aqi,
"aqi_day_min_en": aqi,
"aqi_us": aqi,
"co": co,
"co_us": co,
"no2": no2,
"no2_us": no2,
"noAqi": True, # Indicates no AQI data
"o3": o3,
"o3_us": o3,
"pm10": pm10,
"pm25": pm25,
"so2": so2,
"so2_us": so2,
# --- Available Weather Data ---
"humidity": humidity,
"probability": pop, # NWS observation doesn't directly provide rain probability
"pub_time": pub_time,
"skycon": skycon,
# --- Temperature (Temp max/min must be placeholders) ---
"temp_max": temp_max, # Using current temp as a simple placeholder for the day's max
"temp_min": temp_min, # Using current temp as a simple placeholder for the day's min
"temperature": temperature,
"ultraviolet": uv_index, # Placeholder, UV index not in source data
"vehicle_limit": {
"type": "city_unlimited" # Placeholder/default value
},
# --- Wind Data ---
"wind": {
"speed": round(wind_speed, 2), # km/h
"wind_dir": wind_dir_text,
"wind_level": int(wind_speed / 5), # Crude wind level based on speed
}
}
}
# {"city_id":"n980610","name":"Test","name_cn":"\u6ce2\u7279\u5170","name_en":"Test1","name_cn_tw":"\u6ce2\u7279\u5170","country":"U.S.A.","country_cn":"U.S.A.","country_en":"U.S.A.","country_cn_tw":"U.S.A.","area_cn_first":"","area_cn_second":"<h1>Moonbase 1</h1><script>alert(1);</alert>","area_first":"","timezone":"America/Los_Angeles","timezone_gmt":"GMT-7:00","coordinate":{"longitude":"-122.67621","latitude":"45.52345"}
return transformed_data
USER_AGENT = 'airqmonitor/1.0 (https://github.com/ea)'
def get_current_observation():
observation_url = f"https://api.openweathermap.org/data/3.0/onecall?lat={LAT}&lon={LON}&exclude=minutely&appid={openweather_api_key}&units=metric&lang=kr"
headers = {'User-Agent': USER_AGENT}
try:
response = requests.get(observation_url, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return None
def get_current_air_pollution():
pollution_url = f"https://api.openweathermap.org/data/2.5/air_pollution?lat={LAT}&lon={LON}&appid={openweather_api_key}"
headers = {'User-Agent': USER_AGENT}
try:
response = requests.get(pollution_url, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return None
def scheduled_weather_update():
# 1. fetch data from NWS
# 2. transform data to suitable json
# 3. sleep 10 minutes
global weather_data,pollution_data
current_data = get_current_observation()
pollution_data = get_current_air_pollution()
weather_data = transform_weather_data(current_data, pollution_data)
t = threading.Timer(600, scheduled_weather_update)
t.daemon = True
t.start()
class ThreadingTLSServer(ThreadingMixIn, HTTPServer):
pass
class HttpsRequestHandler(BaseHTTPRequestHandler):
"""
A custom handler to process HTTP requests.
"""
def _set_headers(self, status_code=200, content_type='application/json'):
"""Helper function to set common response headers."""
self.send_response(status_code)
self.send_header('Content-type', content_type)
self.end_headers()
def _send_json(self,payload):
body = payload.encode("utf-8")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Connection", "keep-alive")
self.send_header("Vary", "Accept-Encoding")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
"""Handle GET requests."""
global weather_data,location_data
if self.path == '/daily/locate':
payload = '{"data":'+json.dumps(location_data)+',"code":0}'
self._send_json(payload)
return
elif self.path.startswith("/daily/weatherNow"):
payload = '{"code" : 0,"data" : ' + json.dumps(weather_data) + '}'
print(payload)
self._send_json(payload)
return
elif self.path.startswith("/daily/now"):
payload = '{"data":{"timestamp":'+str(int(time.time()))+',"time":"'+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+'"},"code":0}'
# {"data":{"timestamp":1771079693,"time":"2026-02-14 22:34:53"},"code":0}
print(payload)
self._send_json(payload)
return
elif self.path.startswith("/device/pairStatus"):
payload = """
{"desc":"ok","code":10503}
"""
self._send_json(payload)
return
elif self.path.startswith("/cooperation/companies"):
payload = """
{"data":{"cooperation":[]},"code":0}
"""
self._send_json(payload)
return
elif self.path.startswith("/cooperation/getJDKey"):
payload = """{"data":false,"code":0}"""
self._send_json(payload)
return
elif self.path.startswith("/firmware/checkUpdate"):
payload = """{"data":{"upgrade_sign": 0 } , "code" : 0 }"""
self._send_json(payload)
return
else:
# --- Handle other paths (404 Not Found) ---
self._set_headers(404)
error_message = json.dumps({"error": "Not Found", "path": self.path})
self.wfile.write(error_message.encode('utf-8'))
print(f"[{self.client_address[0]}:{self.client_address[1]}] 404 Not Found for {self.path}")
def run_server():
server_address = (HOST, PORT)
scheduled_weather_update()
httpd = HTTPServer(server_address, HttpsRequestHandler)
print("Press Ctrl+C to stop.")
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print("Server stopped.")
if __name__ == '__main__':
run_server()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment