### Install python-aqi and ffmpeg Source: https://github.com/switchdoclabs/sdl_pi_weathersense/blob/main/README.md Install necessary Python libraries and ffmpeg before proceeding with the WeatherSense Monitor setup. These are required for specific functionalities. ```bash sudo pip3 install python-aqi sudo pip3 install ffmpeg ``` -------------------------------- ### Install mysqlclient Source: https://github.com/switchdoclabs/sdl_pi_weathersense/blob/main/README.md Install the mysqlclient Python package, which is required for the WeatherSense Monitor to interact with the MySQL database. Ensure python3-dev and libmysqlclient-dev are installed first. ```bash sudo apt-get install python3-dev libmysqlclient-dev sudo pip3 install mysqlclient ``` -------------------------------- ### Initialize MariaDB database Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Install required dependencies and import the SQL schema to prepare the database for sensor data. ```bash # Install MariaDB (Raspberry Pi) # Follow: https://pimylifeup.com/raspberry-pi-mysql/ # Install Python MySQL client sudo apt-get install python3-dev libmysqlclient-dev sudo pip3 install mysqlclient # Create database and tables sudo mysql -u root -p < WeatherSenseWireless.sql ``` -------------------------------- ### Install Required Python Packages for WeatherSense Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Installs essential Python libraries for the WeatherSense project, including data processing, scheduling, and dashboarding tools. Run these commands before starting the monitor. ```bash # Install required Python packages sudo pip3 install python-aqi sudo pip3 install ffmpeg sudo pip3 install SafecastPy sudo pip3 install paho-mqtt sudo pip3 install apscheduler sudo pip3 install dash dash-bootstrap-components plotly sudo pip3 install gpiozero sudo pip3 install Pillow ``` -------------------------------- ### Start WeatherSense Web Dashboard Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Starts the web dashboard application in the second terminal. This provides a user interface to view weather data. ```bash # Start the web dashboard (terminal 2) cd ~/SDL_Pi_WeatherSenseWireless/dash_app sudo python3 index.py ``` -------------------------------- ### WeatherSense Monitor Expected Output Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Example output shown when the WeatherSense monitor starts successfully, indicating the software version, scheduled jobs, and the initiation of sensor scanning. ```text # Expected output: # ----------------- # WeatherSense Monitoring Software # Software Version V018 # ----------------- # Scheduled Jobs # ----------------- # readSensors # cleanPictures (daily at 3:04 AM) # cleanTimeLapses (daily at 3:10 AM) # buildTimeLapse (daily at 5:30 AM) # ###### # Read Wireless Sensors # ###### # starting 433MHz scanning ``` -------------------------------- ### Start WeatherSense Monitor Daemon Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Starts the main WeatherSense monitoring daemon in the first terminal. This script reads sensor data and manages scheduled tasks. ```bash # Start the sensor monitor (terminal 1) cd ~/SDL_Pi_WeatherSenseWireless sudo python3 WeatherSenseMonitor.py ``` -------------------------------- ### Install rtl_433 SDR Software Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Installs the rtl_433 software, which is necessary for decoding signals from 433 MHz sensors. This involves cloning the repository, navigating into it, and following its build instructions. ```bash # Install rtl_433 SDR software cd ~ git clone https://github.com/switchdoclabs/rtl_433 cd rtl_433 # Follow build instructions in repository ``` -------------------------------- ### Clone rtl_433 Repository Source: https://github.com/switchdoclabs/sdl_pi_weathersense/blob/main/README.md Clone the rtl_433 repository from GitHub to install the necessary SDL libraries for the WeatherSense Monitor. Follow the provided installation instructions within the repository. ```bash cd git clone https://github.com/switchdoclabs/rtl_433 ``` -------------------------------- ### Run WeatherSense Dashboard App Source: https://github.com/switchdoclabs/sdl_pi_weathersense/blob/main/README.md Start the dashboard application for WeatherSense in a separate terminal window. This provides a web interface to view the collected data. ```bash cd SDL_Pi_WeatherSenseWireless cd dash_app sudo python3 index.py ``` -------------------------------- ### Access WeatherSense Dashboard Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Provides the URLs to access the WeatherSense web dashboard after starting the application. It can be accessed via localhost or the Raspberry Pi's IP address. ```text # Access dashboard at: # http://localhost:8050/ # http://:8050/ ``` -------------------------------- ### Initialize WeatherSenseWireless Database Source: https://github.com/switchdoclabs/sdl_pi_weathersense/blob/main/README.md Run the WeatherSenseWireless.sql script to initialize or set up the database for the WeatherSense Monitor. This command requires root privileges and prompts for a password. ```bash sudo mysql -u root -p < WeatherSenseWireless.sql ``` -------------------------------- ### Configure WeatherSense Monitor settings Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Modify the config.py file to set database credentials, MQTT broker details, and sensor recording frequencies. ```python # config.py - Main configuration file # Debug mode SWDEBUG = True # Altitude for barometric pressure calculations (meters) altitude_m = 620 # SkyCam camera rotation settings per device ID DefaultCameraRotation = 90 SkyCamRotationArray = {} SkyCamRotationArray["DE45"] = 90 SkyCamRotationArray["3BAD"] = 270 # MySQL Database Configuration enable_MySQL_Logging = True MySQL_Host = "localhost" MySQL_User = "root" MySQL_Password = "your_password" MySQL_Schema = "WeatherSenseWireless" # Unit system: 0 = English (Fahrenheit, mph), 1 = Metric (Celsius, m/s) English_Metric = 0 # MQTT Configuration enable_MQTT = True MQTThost = "localhost" MQTTport = 1883 MQTTqos = 0 # WeatherRack2 recording frequency # Default 20 = record every ~7.5 minutes (sensors transmit every ~45 seconds) RecordEveryXReadings = 20 # Indoor T/H sensor recording frequency # Default 10 = record every ~10 minutes per sensor IndoorRecordEveryXReadings = 10 ``` -------------------------------- ### Run WeatherSense Monitor Source: https://github.com/switchdoclabs/sdl_pi_weathersense/blob/main/README.md Execute the main Python script for the WeatherSense Monitor. This command should be run from the SDL_Pi_WeatherSenseWireless directory. ```bash cd cd SDL_Pi_WeatherSenseWireless sudo python3 WeatherSenseMonitor.py ``` -------------------------------- ### Build Daily Timelapse Video Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Creates MP4 videos from the previous day's camera pictures. It queries a MySQL database for picture timestamps and names, then copies the relevant images to a temporary directory before using ffmpeg to generate the video. Ensure the database connection details and paths are correctly configured. ```python import datetime import shutil import os DELETE_FILES_OLDER_THAN_DAYS = 14 DELETE_TIME_LAPSES_OLDER_THAN_DAYS = 14 TIME_LAPSE_START_HOUR = 5 def buildTimeLapse(source): """ Build daily timelapse video from SkyCam pictures. Scheduled to run daily at 5:30 AM via APScheduler. Creates MP4 videos at 20fps from previous day's pictures (5am to 5am). Output: static/TimeLapses/{cameraID}/{cameraID}_(YYYY-MM-DD).mp4 """ # Time range: 5am yesterday to 5am today yesterday = datetime.date.today() - datetime.timedelta(days=1) today = datetime.date.today() starttime = datetime.datetime.combine(yesterday, datetime.time(hour=5)) endtime = datetime.datetime.combine(today, datetime.time(hour=5)) my_dir_path = 'static/SkyCam/' devices = os.listdir(my_dir_path) os.makedirs("static/TimeLapses", exist_ok=True) for device in devices: if config.enable_MySQL_Logging: con = mdb.connect(config.MySQL_Host, config.MySQL_User, config.MySQL_Password, config.MySQL_Schema) cur = con.cursor() # Query pictures for the day query = "SELECT timestamp, cameraID, picturename FROM SkyCamPictures " \ "WHERE cameraID = '%s' AND timestamp >= '%s' AND timestamp < '%s' " \ "ORDER BY timestamp" % (device, starttime, endtime) cur.execute(query) filerecords = cur.fetchall() cur.close() con.close() if len(filerecords) == 0: print(f"device {device} has no current pictures") continue # Prepare temporary directory with numbered images dirpath = 'static/BuildTimeLapse' shutil.rmtree(dirpath, ignore_errors=True) os.makedirs(dirpath, exist_ok=True) count = 0 for record in filerecords: recordname = record[2] # Extract date from filename for directory path splitName = recordname.split("_")[2].split("-") dayname = f"{splitName[0]}-{splitName[1]}-{splitName[2]}" fromPath = f"{my_dir_path}{device}/{dayname}/{recordname}" toPath = f"{dirpath}/pic_{count:04d}.jpg" try: shutil.copyfile(fromPath, toPath) count += 1 except: pass # Generate video with ffmpeg cwdir = os.getcwd() inputFiles = f"{cwdir}/{dirpath}/pic_%04d.jpg" outDir = f"{cwdir}/static/TimeLapses/{device}" os.makedirs(outDir, exist_ok=True) outputFile = f"{outDir}/{device}_{dayname}.mp4" # Remove existing file if present try: os.remove(outputFile) except: pass # Create timelapse at 20fps command = f"/usr/bin/ffmpeg -r 20 -i {inputFiles} -c:v libx264 {outputFile}" os.system(f"{command} > /dev/null 2>&1") ``` -------------------------------- ### Publish Sensor Data to MQTT Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Publishes sensor data as a JSON string to a specified MQTT topic. Requires configuration of MQTT broker details. ```python from paho.mqtt import publish def mqtt_publish_single(message, topic): """ Publish sensor data to MQTT broker. Args: message: JSON string of sensor data topic: Sub-topic name (e.g., "WeatherRack3", "WSAQI", "WSLightning") MQTT Topics published: weathersense/WeatherRack3 - Weather station data weathersense/WR3Power - WeatherRack3 solar power data weathersense/FT020T - WeatherRack2 weather data weathersense/F016TH - Indoor T/H sensor data weathersense/WSAQI - Air quality sensor data weathersense/WSLightning - Lightning detector data weathersense/WSAfterShock - Earthquake detector data weathersense/WSRadiation - Radiation detector data weathersense/WSSolarMAX - SolarMAX power monitor data """ full_topic = 'weathersense/{0}'.format(topic) try: publish.single( topic=full_topic, payload=message, hostname=config.MQTThost, port=config.MQTTport, qos=config.MQTTqos ) except Exception: print('MQTT broker not available') ``` -------------------------------- ### Process Indoor Temperature and Humidity Sensors Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Handles F016TH sensor data, including Fahrenheit to Celsius conversion and database logging with throttling. ```python def processF016TH(sLine, ReadingCountArray): """ Process indoor temperature/humidity sensor data. Supports up to 8 sensors on channels 1-8. Args: sLine: Raw JSON string from rtl_433 ReadingCountArray: Array of counters per channel [ch1, ch2, ..., ch8] """ var = json.loads(sLine) # Publish to MQTT if config.enable_MQTT: mqtt_publish_single(sLine, "F016TH") # Throttle database writes per channel chan_array_pos = var['channel'] - 1 if (ReadingCountArray[chan_array_pos] % config.IndoorRecordEveryXReadings) != 0: ReadingCountArray[chan_array_pos] += 1 return "" ReadingCountArray[chan_array_pos] += 1 # Convert Fahrenheit to Celsius IndoorTemperature = round(((var["temperature_F"] - 32.0) / (9.0 / 5.0)), 2) # Store in database if config.enable_MySQL_Logging: con = mdb.connect(config.MySQL_Host, config.MySQL_User, config.MySQL_Password, config.MySQL_Schema) cur = con.cursor() fields = "DeviceID, ChannelID, Temperature, Humidity, BatteryOK, TimeRead" values = '%d, %d, %6.2f, %6.2f, "%s", "%s"' % ( var["device"], var["channel"], IndoorTemperature, var["humidity"], var["battery"], var["time"]) query = "INSERT INTO IndoorTHSensors (%s) VALUES(%s)" % (fields, values) cur.execute(query) con.commit() cur.close() con.close() ``` -------------------------------- ### Create IndoorTHSensors Table Schema Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Defines the schema for the IndoorTHSensors table, which logs indoor temperature and humidity readings from specific devices and channels, including battery status. ```sql -- Indoor temperature/humidity sensors CREATE TABLE `IndoorTHSensors` ( `id` int(11) NOT NULL AUTO_INCREMENT, `TimeStamp` timestamp NOT NULL DEFAULT current_timestamp(), `DeviceID` int(11) NOT NULL, `ChannelID` int(11) NOT NULL, `Temperature` float NOT NULL, `Humidity` int(11) NOT NULL, `BatteryOK` text NOT NULL, PRIMARY KEY (`id`) ); ``` -------------------------------- ### Process WeatherSense Lightning Data Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Handles data from WeatherSense ThunderBoard devices. Logs to MQTT and MySQL if enabled. Requires WeatherSense Protocol 16. ```python def processWeatherSenseTB(sLine): """ Process WeatherSense Lightning Detector (ThunderBoard) data. WeatherSense Protocol 16. Records lightning strike count, distance, and interrupt events. """ state = json.loads(sLine) if config.enable_MQTT: mqtt_publish_single(sLine, "WSLightning") if config.enable_MySQL_Logging: con = mdb.connect(config.MySQL_Host, config.MySQL_User, config.MySQL_Password, config.MySQL_Schema) cur = con.cursor() fields = "deviceid, irqsource, previousinterruptresult, " \ "lightninglastdistance, lightningcount, interruptcount, " \ "batteryvoltage, solarvoltage" values = "%d, %d, %d, %d, %d, %d, %6.2f, %6.2f" % ( state["deviceid"], state['irqsource'], state['previousinterruptresult'], state['lightninglastdistance'], state['lightningcount'], state['interruptcount'], state["batteryvoltage"], state["solarpanelvoltage"]) query = "INSERT INTO TB433MHZ (%s) VALUES(%s)" % (fields, values) cur.execute(query) con.commit() cur.close() con.close() ``` -------------------------------- ### Create AQI433MHZ Table Schema Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Defines the schema for the AQI433MHZ table, storing air quality index data, including PM1.0, PM2.5, PM10 concentrations, and 24-hour AQI. ```sql -- Air Quality Index sensor data CREATE TABLE `AQI433MHZ` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `TimeStamp` timestamp NOT NULL DEFAULT current_timestamp(), `PM1_0S` int(11) NOT NULL, `PM2_5S` int(11) NOT NULL, `PM10S` int(11) NOT NULL, `AQI` int(11) NOT NULL, `AQI24Hour` float NOT NULL, PRIMARY KEY (`ID`) ); ``` -------------------------------- ### Retrieve and Format Current Weather Data Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Fetches the latest record from the WeatherData table and calculates rain totals for the current day and the last 24 hours. It also applies unit conversions if English units are configured. ```python import MySQLdb as mdb import datetime def generateCurrentWeatherJSON(): """ Generate JSON object with current weather readings and calculated values. Returns dictionary with: - OutdoorTemperature, OutdoorHumidity - IndoorTemperature, IndoorHumidity - WindSpeed, WindGust, WindDirection - BarometricPressure, BarometricPressureSeaLevel - SunlightVisible, SunlightUVIndex - AQI, AQI24Average - TotalRain, CalendarDayRain, CalendarMonthRain - 24HourRain, 7DaysRain, 30DayRain - All with corresponding Units suffixes """ con = mdb.connect(config.MySQL_Host, config.MySQL_User, config.MySQL_Password, config.MySQL_Schema) cur = con.cursor() # Get latest weather reading query = "SELECT * FROM `WeatherData` ORDER BY id DESC LIMIT 1" cur.execute(query) records = cur.fetchall() # Build JSON from column names and values query = "SHOW COLUMNS FROM WeatherData" cur.execute(query) names = cur.fetchall() CWJSON = {} for i, name in enumerate(names): if name[0] == "TimeStamp": CWJSON[name[0]] = records[0][i] if records else 0 elif name[0] == "BatteryOK": CWJSON[name[0]] = records[0][i] if records else "LOW" else: CWJSON[name[0]] = float(records[0][i]) if records else 0 # Calculate rain totals # Calendar day rain query = "SELECT id, TotalRain FROM WeatherData WHERE DATE(TimeStamp) = CURDATE()" cur.execute(query) rainrecords = cur.fetchall() if rainrecords: CWJSON["CalendarDayRain"] = round(rainrecords[-1][1] - rainrecords[0][1], 2) # Last 24 hours rain timeDelta = datetime.timedelta(days=1) before = (datetime.datetime.now() - timeDelta).strftime('%Y-%m-%d %H:%M:%S') query = f"SELECT id, TotalRain FROM WeatherData WHERE TimeStamp > '{before}'" cur.execute(query) rainrecords = cur.fetchall() if rainrecords: CWJSON["24HourRain"] = round(rainrecords[-1][1] - rainrecords[0][1], 2) # Apply unit conversions based on config if not config.English_Metric: # English units CWJSON["OutdoorTemperature"] = round((9.0/5.0 * CWJSON["OutdoorTemperature"]) + 32, 1) CWJSON["WindSpeed"] = round(CWJSON["WindSpeed"] * 2.23694, 1) # m/s to mph CWJSON["TotalRain"] = round(CWJSON["TotalRain"] / 25.4, 1) # mm to inches CWJSON["BarometricPressureSeaLevel"] = round( CWJSON["BarometricPressureSeaLevel"] * 0.2953, 2) # hPa to inHg cur.close() con.close() return CWJSON ``` -------------------------------- ### Update WeatherSenseWireless Database Source: https://github.com/switchdoclabs/sdl_pi_weathersense/blob/main/README.md Execute SQL scripts to update the WeatherSenseWireless database to the latest schema version. This is crucial when updating from older database versions. ```bash sudo mysql -u root -p WeatherSenseWireless < updateWeatherSenseWireless.sql sudo mysql -u root -p WeatherSenseWireless < update018WeatherSenseWireless.sql ``` -------------------------------- ### Create TB433MHZ Table Schema Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Defines the schema for the TB433MHZ table, used to store data from a lightning detector, including last detected distance, lightning count, and interrupt count. ```sql -- Lightning detector data CREATE TABLE `TB433MHZ` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `TimeStamp` timestamp NOT NULL DEFAULT current_timestamp(), `lightninglastdistance` int(11) NOT NULL, `lightningcount` int(11) NOT NULL, `interruptcount` int(11) NOT NULL, PRIMARY KEY (`ID`) ); ``` -------------------------------- ### Process WeatherRack3 Sensor Data Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Parses JSON data from WeatherRack3 sensors, calculates various environmental metrics, logs to MySQL, and publishes to MQTT. Skips duplicate readings based on timestamp. ```python import json import aqi import MySQLdb as mdb from paho.mqtt import publish def processWeatherRack3(sLine, lastWeatherRack3TimeStamp, ReadingCount): """ Process WeatherRack3 weather station data from JSON SDR output. Args: sLine: Raw JSON string from rtl_433 lastWeatherRack3TimeStamp: Previous timestamp for duplicate detection ReadingCount: Counter for recording frequency Returns: Updated timestamp string or empty string if duplicate """ var = json.loads(sLine) # Publish to MQTT topic weathersense/WeatherRack3 if config.enable_MQTT: mqtt_publish_single(sLine, "WeatherRack3") # Skip duplicate readings if lastWeatherRack3TimeStamp == var["time"]: return "" # Parse sensor values OutdoorTemperature = var["temperature"] / 10.0 # Celsius OutdoorHumidity = var["humidity"] / 10.0 # Percent WindSpeed = var["windspeed"] / 10.0 # m/s WindDirection = var["winddirectiondegrees"] # Degrees TotalRain = var["rain"] / 10.0 # mm PM2_5 = var["PM2_5"] PM10 = var["PM10"] Noise = var["noise"] / 10.0 # dB BarometricPressure = var["pressure"] # hPa # Calculate AQI from PM values myaqi = aqi.to_aqi([ (aqi.POLLUTANT_PM25, PM2_5), (aqi.POLLUTANT_PM10, PM10) ]) AQI = min(myaqi, 500) # Cap at 500 # Calculate sea level pressure using altitude BarometricPressureSeaLevel = (BarometricPressure * 100000) / \ pow(1.0 - config.altitude_m / 44330.0, 5.255) / 100000 # Store in database if config.enable_MySQL_Logging: con = mdb.connect(config.MySQL_Host, config.MySQL_User, config.MySQL_Password, config.MySQL_Schema) cur = con.cursor() fields = "MessageID, SerialNumber, OutdoorTemperature, OutdoorHumidity, " \ "TotalRain, WindSpeed, WindDirection, Noise, BarometricPressure, " \ "BarometricPressureSeaLevel, AQI, PM2_5, PM10" values = "%d, %d, %6.2f, %6.2f, %6.2f, %6.2f, %6.2f, %6.2f, %6.2f, %6.2f, " \ "%6.2f, %d, %d" % (var["messageid"], var["deviceid"], OutdoorTemperature, OutdoorHumidity, TotalRain, WindSpeed, WindDirection, Noise, BarometricPressure, BarometricPressureSeaLevel, AQI, PM2_5, PM10) query = "INSERT INTO WeatherRack3 (%s) VALUES(%s)" % (fields, values) cur.execute(query) con.commit() cur.close() con.close() return var["time"] ``` -------------------------------- ### Process WeatherRack3 Data Function Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Placeholder comment indicating the location of the function responsible for processing incoming data from WeatherRack3 weather stations. ```python # wirelessSensors.py - processWeatherRack3() ``` -------------------------------- ### Create RAD433MHZ Table Schema Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Defines the schema for the RAD433MHZ table, storing radiation detector data, including counts per minute (CPM) and dose rates (nSVh, uSVh) over different periods. ```sql -- Radiation detector data CREATE TABLE `RAD433MHZ` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `TimeStamp` timestamp NOT NULL DEFAULT current_timestamp(), `CPM` int(11) NOT NULL, `nSVh` int(11) NOT NULL, `uSVh` float NOT NULL, `uSVh24Hour` float NOT NULL, PRIMARY KEY (`ID`) ); ``` -------------------------------- ### Process Air Quality Index Sensors Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Calculates AQI from PM2.5/PM10 values and maintains a 24-hour rolling average for database storage. ```python import aqi def processWeatherSenseAQI(sLine): """ Process WeatherSense Air Quality Index sensor data. WeatherSense Protocol 15. Calculates AQI from PM2.5 and PM10 values and maintains 24-hour rolling average. """ state = json.loads(sLine) if config.enable_MQTT: mqtt_publish_single(sLine, "WSAQI") if config.enable_MySQL_Logging: con = mdb.connect(config.MySQL_Host, config.MySQL_User, config.MySQL_Password, config.MySQL_Schema) cur = con.cursor() # Calculate 24-hour AQI average timeDelta = datetime.timedelta(days=1) before = (datetime.datetime.now() - timeDelta).strftime('%Y-%m-%d %H:%M:%S') query = "SELECT AQI, TimeStamp FROM AQI433MHZ WHERE " \ "(TimeStamp > '%s') ORDER BY TimeStamp" % before cur.execute(query) myAQIRecords = cur.fetchall() if len(myAQIRecords) > 0: myAQITotal = sum(record[0] for record in myAQIRecords) AQI24Hour = (myAQITotal + float(state['AQI'])) / (len(myAQIRecords) + 1) else: AQI24Hour = 0.0 # Recalculate AQI from raw PM values for accuracy myaqi = aqi.to_aqi([ (aqi.POLLUTANT_PM25, state['PM2.5A']), (aqi.POLLUTANT_PM10, state['PM10A']) ]) state['AQI'] = min(myaqi, 500) # Calculate power values batteryPower = float(state["batterycurrent"]) * float(state["batteryvoltage"]) loadPower = float(state["loadcurrent"]) * float(state["loadvoltage"]) solarPower = float(state["solarpanelcurrent"]) * float(state["solarpanelvoltage"]) fields = "PM1_0S, PM2_5S, PM10S, PM1_0A, PM2_5A, PM10A, AQI, AQI24Hour, " \ "batteryvoltage, solarvoltage, batterypower, solarpower" values = "%d, %d, %d, %d, %d, %d, %d, %6.2f, %6.2f, %6.2f, %6.2f, %6.2f" % ( state['PM1.0S'], state['PM2.5S'], state['PM10S'], state['PM1.0A'], state['PM2.5A'], state['PM10A'], state['AQI'], AQI24Hour, state["batteryvoltage"], state["solarpanelvoltage"], batteryPower, solarPower) query = "INSERT INTO AQI433MHZ (%s) VALUES(%s)" % (fields, values) cur.execute(query) con.commit() cur.close() con.close() ``` -------------------------------- ### Create WeatherData Table Schema Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Defines the schema for the WeatherData table, used to store readings from WeatherRack2/FT020T stations, including outdoor temperature, humidity, rain, sunlight, wind, and AQI. ```sql -- Key database tables created by WeatherSenseWireless.sql -- Weather station data (WeatherRack2/FT020T) CREATE TABLE `WeatherData` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `TimeStamp` timestamp NOT NULL DEFAULT current_timestamp(), `OutdoorTemperature` float NOT NULL, `OutdoorHumidity` float NOT NULL, `TotalRain` float NOT NULL, `SunlightVisible` float NOT NULL, `SunlightUVIndex` float NOT NULL, `WindSpeed` float NOT NULL, `WindGust` float NOT NULL, `WindDirection` float NOT NULL, `AQI` float NOT NULL, `BatteryOK` text NOT NULL, PRIMARY KEY (`ID`) ); ``` -------------------------------- ### Create WeatherRack3 Table Schema Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Defines the schema for the WeatherRack3 table, storing advanced weather data such as temperature, humidity, rain, wind, noise, barometric pressure, and air quality (PM2.5, PM10). ```sql -- WeatherRack3 advanced station data CREATE TABLE `WeatherRack3` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `TimeStamp` timestamp NOT NULL DEFAULT current_timestamp(), `OutdoorTemperature` float NOT NULL, `OutdoorHumidity` float NOT NULL, `TotalRain` float NOT NULL, `WindSpeed` float NOT NULL, `WindDirection` float NOT NULL, `Noise` float NOT NULL, `BarometricPressure` float NOT NULL, `AQI` float NOT NULL, `PM2_5` int(11) NOT NULL, `PM10` int(11) NOT NULL, PRIMARY KEY (`ID`) ); ``` -------------------------------- ### Calculate Wind Rose Data Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Aggregates wind speed and direction into a 6x8 matrix for visualization. Requires a datetime.timedelta object to define the query window. ```python def fetchWindData(timeDelta): """ Fetch wind data for wind rose visualization. Args: timeDelta: datetime.timedelta for time range Returns: 6x8 array: [speed_bucket][direction_bucket] with percentages Speed buckets: <1, 1-2.3, 2.3-4.4, 4.4-8.5, 8.5-11, >11 m/s Direction buckets: N, NE, E, SE, S, SW, W, NW """ con = mdb.connect(config.MySQL_Host, config.MySQL_User, config.MySQL_Password, config.MySQL_Schema) cur = con.cursor() before = (datetime.datetime.now() - timeDelta).strftime('%Y-%m-%d %H:%M:%S') query = f"SELECT WindSpeed, WindDirection FROM WeatherData WHERE TimeStamp > '{before}'" cur.execute(query) records = cur.fetchall() # Initialize 6 speed buckets x 8 direction buckets df = [[0]*8 for _ in range(6)] for windSpeed, windDirection in records: # Determine cardinal direction bucket (0-7) if windDirection >= 337.5 or windDirection < 22.5: CB = 0 # N elif windDirection < 67.5: CB = 1 # NE elif windDirection < 112.5: CB = 2 # E elif windDirection < 157.5: CB = 3 # SE elif windDirection < 202.5: CB = 4 # S elif windDirection < 247.5: CB = 5 # SW elif windDirection < 292.5: CB = 6 # W else: CB = 7 # NW # Determine speed bucket (0-5) if windSpeed < 1.0: SB = 0 elif windSpeed < 2.3: SB = 1 elif windSpeed < 4.4: SB = 2 elif windSpeed < 8.5: SB = 3 elif windSpeed < 11.0: SB = 4 else: SB = 5 df[SB][CB] += 1 # Normalize to percentages totalRecords = len(records) if totalRecords > 0: for bucket in df: for i in range(8): bucket[i] = round(100.0 * bucket[i] / totalRecords, 2) cur.close() con.close() return df ``` -------------------------------- ### Calculate Battery Percentage Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Calculates remaining battery percentage from voltage, supporting both 12V lead acid and LiPo battery types. ```python def returnPercentLeftInBattery(currentVoltage, maxVolt): """ Calculate battery percentage from voltage reading. Args: currentVoltage: Current battery voltage maxVolt: Maximum voltage (4.2V for LiPo, 13.2V for 12V lead acid) Returns: Battery percentage (0-100) """ if maxVolt > 11: # 12V lead acid battery (11.0V - 12.9V range) returnPercent = ((currentVoltage - 11.0) / 1.9) * 100.0 return max(0.0, min(100.0, returnPercent)) else: # LiPo battery with non-linear discharge curve scaledVolts = currentVoltage / maxVolt scaledVolts = min(1.0, scaledVolts) # Lookup table for LiPo discharge curve if scaledVolts > 0.9686: return 10 * (1 - (1.0 - scaledVolts) / (1.0 - 0.9686)) + 90 if scaledVolts > 0.9374: return 10 * (1 - (0.9686 - scaledVolts) / (0.9686 - 0.9374)) + 80 if scaledVolts > 0.9063: return 30 * (1 - (0.9374 - scaledVolts) / (0.9374 - 0.9063)) + 50 if scaledVolts > 0.8749: return 20 * (1 - (0.8749 - scaledVolts) / (0.9063 - 0.8749)) + 11 if scaledVolts > 0.8437: return 15 * (1 - (0.8437 - scaledVolts) / (0.8749 - 0.8437)) + 1 if scaledVolts > 0.8126: return 7 * (1 - (0.8126 - scaledVolts) / (0.8437 - 0.8126)) + 2 if scaledVolts > 0.7812: return 4 * (1 - (0.7812 - scaledVolts) / (0.8126 - 0.7812)) + 1 return 0 ``` -------------------------------- ### Process SkyCam Picture Chunks Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Reassembles base64-encoded image chunks into a complete file and applies a timestamp overlay using the PIL library. ```python from PIL import ImageFont, ImageDraw, Image import base64 import datetime import os CameraChunkData = {} def saveChunk(jsonmsg): """ Store incoming picture chunk from SkyCam. Pictures are transmitted in chunks over MQTT and reassembled. Args: jsonmsg: Dictionary with chunk data: - id: Camera ID (e.g., "DE45") - messageid: Picture message ID - chunknumber: Current chunk (0-indexed) - totalchunknumbers: Total chunks in picture - chunk: Base64-encoded chunk data - picturesize: Total file size - resolution: Camera resolution setting """ cameraID = jsonmsg["id"] chunkNumber = jsonmsg["chunknumber"] totalChunks = jsonmsg["totalchunknumbers"] chunk = jsonmsg["chunk"] # Clear data for new picture (chunk 0) if chunkNumber == 0: try: del CameraChunkData[cameraID] except: pass # Store chunk try: currentIDList = CameraChunkData[cameraID] except: currentIDList = [] currentIDList.append({ 'chunknumber': chunkNumber, 'totalchunks': totalChunks, 'picturesize': jsonmsg["picturesize"], 'chunk': chunk, 'resends': jsonmsg["totalchunkresends"], 'resolution': jsonmsg["resolution"] }) CameraChunkData[cameraID] = currentIDList # Process when all chunks received if int(chunkNumber) + 1 == int(totalChunks): processPicture(cameraID, jsonmsg["messageid"], CameraChunkData[cameraID]) del CameraChunkData[cameraID] def processPicture(cameraID, messageID, CameraChunkList): """ Reassemble picture chunks and save with timestamp overlay. Directory structure: static/SkyCam/{cameraID}/{YYYY-MM-DD}/{cameraID}_{messageID}_{timestamp}.jpg static/CurrentPicture/{cameraID}.jpg (latest picture) """ fileDate = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") fileDay = datetime.datetime.now().strftime("%Y-%m-%d") singlefilename = f"{cameraID}_{messageID}_{fileDate}.jpg" dirpathname = f"static/SkyCam/{cameraID}/{fileDay}" os.makedirs(dirpathname, exist_ok=True) filename = f"{dirpathname}/{singlefilename}" # Reassemble chunks into complete image mutable_bytes = bytearray() for myChunk in CameraChunkList: theChunk = myChunk["chunk"].encode() mutable_bytes += base64.b64decode(theChunk) # Write raw image file with open(filename, "wb") as f: f.write(mutable_bytes) # Process image with PIL pil_im = Image.open(filename) # Apply camera rotation from config try: myCameraRotation = config.SkyCamRotationArray[cameraID] except: myCameraRotation = config.DefaultCameraRotation pil_im = pil_im.rotate(myCameraRotation) # Add timestamp overlay draw = ImageDraw.Draw(pil_im) font = ImageFont.truetype("/usr/share/fonts/truetype/freefont/FreeSans.ttf", 25) myText = f"WeatherSense SkyCam-{cameraID} {datetime.datetime.now().strftime('%d-%b-%Y %H:%M:%S')}" # Create text background text_size = font.getsize(myText) button_size = (text_size[0] + 20, text_size[1] + 10) button_img = Image.new('RGBA', button_size, "black") button_draw = ImageDraw.Draw(button_img) button_draw.text((10, 5), myText, fill='rgb(255,255,255)', font=font) pil_im.paste(button_img, (0, 0)) # Save processed image pil_im.save(filename, format='JPEG') pil_im.save(f"static/CurrentPicture/{cameraID}.jpg", format='JPEG') ``` -------------------------------- ### Convert Temperature Units Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Converts temperature between Celsius and Fahrenheit based on the global state.EnglishMetric setting. ```python def returnTemperatureCF(temperature): """Convert Celsius to Fahrenheit based on state.EnglishMetric setting.""" if state.EnglishMetric: return temperature # Metric - return Celsius else: return (9.0/5.0) * temperature + 32.0 # English - return Fahrenheit ``` -------------------------------- ### Convert Wind Direction Degrees Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Maps wind direction in degrees to a cardinal or intercardinal direction string. ```python def returnWindDirection(windDirection): """ Convert wind direction degrees to cardinal/intercardinal direction string. Args: windDirection: Degrees (0-360, where 0/360 = North) Returns: String like "N", "NE", "E", "SE", "S", "SW", "W", "NW", or intercardinals like "NNW", "ESE", etc. """ directions = [ (337.5, "N"), (315.0, "NNW"), (292.5, "NW"), (270.0, "WNW"), (247.5, "W"), (225.0, "WSW"), (202.5, "SW"), (180.0, "SSW"), (157.5, "S"), (135.0, "SSE"), (112.5, "SE"), (90.0, "ESE"), (67.5, "E"), (45.0, "ENE"), (22.5, "NE"), (0.0, "NNE") ] for threshold, direction in directions: if windDirection > threshold + 1.0: return direction return "N" ``` -------------------------------- ### Process WeatherSense Radiation Data Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Processes data from WeatherSense Radiation Detectors using Protocol 19. Calculates 24-hour radiation averages and uploads data to external services. Requires datetime and updateWeb modules. ```python def processWeatherSenseRadiation(sLine): """ Process WeatherSense Radiation Detector data. WeatherSense Protocol 19. Records CPM (counts per minute), µSv/h radiation levels, calculates 24-hour average, and uploads to external services. """ state = json.loads(sLine) if config.enable_MQTT: mqtt_publish_single(sLine, "WSRadiation") if config.enable_MySQL_Logging: con = mdb.connect(config.MySQL_Host, config.MySQL_User, config.MySQL_Password, config.MySQL_Schema) cur = con.cursor() # Calculate 24-hour radiation average timeDelta = datetime.timedelta(days=1) before = (datetime.datetime.now() - timeDelta).strftime('%Y-%m-%d %H:%M:%S') query = "SELECT uSVh FROM RAD433MHZ WHERE (TimeStamp > '%s')" % before cur.execute(query) myRADRecords = cur.fetchall() if len(myRADRecords) > 0: myRADTotal = sum(record[0] for record in myRADRecords) RAD24Hour = (myRADTotal + float(state['uSVh'])) / (len(myRADRecords) + 1) else: RAD24Hour = 0.0 fields = "CPM, nSVh, uSVh, uSVh24Hour, deviceid" values = "%d, %d, %6.2f, %6.2f, %d" % ( state['CPM'], state['nSVh'], state['uSVh'], RAD24Hour, state["deviceid"]) query = "INSERT INTO RAD433MHZ (%s) VALUES(%s)" % (fields, values) cur.execute(query) con.commit() cur.close() con.close() # Upload to external radiation monitoring services CPM = state['CPM'] uSVh = state['uSVh'] updateWeb.update_SafeCast(CPM, uSVh) updateWeb.update_RadMon(CPM) updateWeb.update_GMCMap(CPM, uSVh) ``` -------------------------------- ### Clean Old Image Files Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Removes image files from the 'static/SkyCam/' directory that are older than a specified number of days (default 14). This function helps manage disk space by deleting outdated pictures. ```python def cleanPictures(source): """ Remove pictures older than 14 days. Scheduled to run daily at 3:04 AM. """ dir_path = 'static/SkyCam/' threshold = time.time() - DELETE_FILES_OLDER_THAN_DAYS * 86400 for device in os.listdir(dir_path): device_dir_path = f"{dir_path}{device}/" for day in os.listdir(device_dir_path): day_dir_path = f"{device_dir_path}{day}/" for myFile in os.listdir(day_dir_path): myFilePath = f"{day_dir_path}{myFile}" if os.stat(myFilePath).st_ctime < threshold: os.remove(myFilePath) ``` -------------------------------- ### Clean Old Timelapse Videos Source: https://context7.com/switchdoclabs/sdl_pi_weathersense/llms.txt Removes timelapse video files from the 'static/TimeLapses/' directory that are older than a specified number of days (default 14). This function is essential for managing storage space occupied by generated videos. ```python def cleanTimeLapses(source): """ Remove timelapse videos older than 14 days. Scheduled to run daily at 3:10 AM. """ dir_path = 'static/TimeLapses/' threshold = time.time() - DELETE_TIME_LAPSES_OLDER_THAN_DAYS * 86400 for device in os.listdir(dir_path): device_dir_path = f"{dir_path}{device}/" for myFile in os.listdir(device_dir_path): myFilePath = f"{device_dir_path}{myFile}" if os.stat(myFilePath).st_ctime < threshold: os.remove(myFilePath) ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.