updated cloud detection and mixed in celestial weather
This commit is contained in:
213
sensors/main.py
213
sensors/main.py
@@ -1,5 +1,5 @@
|
||||
import time, math, board, smbus2, serial, psutil, pynmea2, bme680
|
||||
import adafruit_ltr390, threading, os
|
||||
import adafruit_ltr390, threading, os, statistics
|
||||
from datetime import datetime, timezone
|
||||
from collections import deque
|
||||
from influxdb_client import InfluxDBClient, Point
|
||||
@@ -13,30 +13,39 @@ def cfg():
|
||||
load_dotenv(BASE_DIR / '.env', override=True)
|
||||
load_dotenv(BASE_DIR / '.env.local', override=True)
|
||||
return {
|
||||
'LATITUDE': float(os.getenv('LATITUDE', '0.0')),
|
||||
'LONGITUDE': float(os.getenv('LONGITUDE', '0.0')),
|
||||
'ALTITUDE': float(os.getenv('ALTITUDE', '0.0')),
|
||||
'INFLUX_URL': os.getenv('INFLUX_URL', 'http://localhost:8086'),
|
||||
'INFLUX_TOKEN': os.getenv('INFLUX_TOKEN', ''),
|
||||
'INFLUX_ORG': os.getenv('INFLUX_ORG', 'weather'),
|
||||
'INFLUX_BUCKET': os.getenv('INFLUX_BUCKET', 'station'),
|
||||
'GPS_PORT': os.getenv('GPS_PORT', '/dev/ttyS0'),
|
||||
'GPS_BAUD': int( os.getenv('GPS_BAUD', '9600')),
|
||||
'GAS_REFERENCE': int( os.getenv('GAS_REFERENCE', '250000')),
|
||||
'PRESSURE_TREND_WINDOW':int(os.getenv('PRESSURE_TREND_WINDOW', '60')),
|
||||
'LUNA_CAL_SAMPLES': int( os.getenv('LUNA_CAL_SAMPLES', '20')),
|
||||
'LUNA_MAX_DIST_CM': int( os.getenv('LUNA_MAX_DIST_CM', '800')),
|
||||
'LUNA_MIN_DEPTH_CM': int( os.getenv('LUNA_MIN_DEPTH_CM', '2')),
|
||||
'AS3935_ADDR': int( os.getenv('AS3935_ADDR', '0x03'), 16),
|
||||
'AS3935_NOISE_FLOOR': int( os.getenv('AS3935_NOISE_FLOOR', '5')),
|
||||
'AS3935_WATCHDOG': int( os.getenv('AS3935_WATCHDOG', '3')),
|
||||
'AS3935_SPIKE_REJ': int( os.getenv('AS3935_SPIKE_REJ', '7')),
|
||||
'SKIN_TYPE_2_MED': int( os.getenv('SKIN_TYPE_2_MED', '200')),
|
||||
'LOOP_INTERVAL': float(os.getenv('LOOP_INTERVAL', '1.0')),
|
||||
'FLOOD_TEMP_THRESHOLD':float(os.getenv('FLOOD_TEMP_THRESHOLD', '10.0')),
|
||||
'SLUSH_TEMP_THRESHOLD':float(os.getenv('SLUSH_TEMP_THRESHOLD', '0.0')),
|
||||
'SNOW_STRENGTH_MIN': int( os.getenv('SNOW_STRENGTH_MIN', '700')),
|
||||
'SEISMIC_NOISE_FLOOR':float(os.getenv('SEISMIC_NOISE_FLOOR', '0.02')),
|
||||
'LATITUDE': float(os.getenv('LATITUDE', '0.0')),
|
||||
'LONGITUDE': float(os.getenv('LONGITUDE', '0.0')),
|
||||
'ALTITUDE': float(os.getenv('ALTITUDE', '0.0')),
|
||||
'INFLUX_URL': os.getenv('INFLUX_URL', 'http://localhost:8086'),
|
||||
'INFLUX_TOKEN': os.getenv('INFLUX_TOKEN', ''),
|
||||
'INFLUX_ORG': os.getenv('INFLUX_ORG', 'weather'),
|
||||
'INFLUX_BUCKET': os.getenv('INFLUX_BUCKET', 'station'),
|
||||
'GPS_PORT': os.getenv('GPS_PORT', '/dev/ttyS0'),
|
||||
'GPS_BAUD': int( os.getenv('GPS_BAUD', '9600')),
|
||||
'GAS_REFERENCE': int( os.getenv('GAS_REFERENCE', '250000')),
|
||||
'PRESSURE_TREND_WINDOW': int( os.getenv('PRESSURE_TREND_WINDOW', '60')),
|
||||
'LUNA_CAL_SAMPLES': int( os.getenv('LUNA_CAL_SAMPLES', '20')),
|
||||
'LUNA_MAX_DIST_CM': int( os.getenv('LUNA_MAX_DIST_CM', '800')),
|
||||
'LUNA_MIN_DEPTH_CM': int( os.getenv('LUNA_MIN_DEPTH_CM', '2')),
|
||||
'AS3935_ADDR': int( os.getenv('AS3935_ADDR', '0x03'), 16),
|
||||
'AS3935_NOISE_FLOOR': int( os.getenv('AS3935_NOISE_FLOOR', '5')),
|
||||
'AS3935_WATCHDOG': int( os.getenv('AS3935_WATCHDOG', '3')),
|
||||
'AS3935_SPIKE_REJ': int( os.getenv('AS3935_SPIKE_REJ', '7')),
|
||||
'SKIN_TYPE_2_MED': int( os.getenv('SKIN_TYPE_2_MED', '200')),
|
||||
'LOOP_INTERVAL': float( os.getenv('LOOP_INTERVAL', '1.0')),
|
||||
'FLOOD_TEMP_THRESHOLD': float( os.getenv('FLOOD_TEMP_THRESHOLD', '10.0')),
|
||||
'SLUSH_TEMP_THRESHOLD': float( os.getenv('SLUSH_TEMP_THRESHOLD', '0.0')),
|
||||
'SNOW_STRENGTH_MIN': int( os.getenv('SNOW_STRENGTH_MIN', '700')),
|
||||
'SEISMIC_NOISE_FLOOR': float( os.getenv('SEISMIC_NOISE_FLOOR', '0.02')),
|
||||
'CLOUD_BUCKET_SIZE': int( os.getenv('CLOUD_BUCKET_SIZE', '5')),
|
||||
'CLOUD_HISTORY': int( os.getenv('CLOUD_HISTORY', '20')),
|
||||
'CLOUD_BASELINE_PCT': float( os.getenv('CLOUD_BASELINE_PCT', '0.9')),
|
||||
'CLOUD_VOLATILITY_THRESH':float( os.getenv('CLOUD_VOLATILITY_THRESH', '0.3')),
|
||||
'CLOUD_CLEAR_MAX': float( os.getenv('CLOUD_CLEAR_MAX', '15.0')),
|
||||
'CLOUD_MOSTLY_CLEAR_MAX': float( os.getenv('CLOUD_MOSTLY_CLEAR_MAX', '40.0')),
|
||||
'CLOUD_CLOUDY_MAX': float( os.getenv('CLOUD_CLOUDY_MAX', '80.0')),
|
||||
'CLOUD_MIN_SAMPLES': int( os.getenv('CLOUD_MIN_SAMPLES', '5')),
|
||||
'CLOUD_LUX_WINDOW': int( os.getenv('CLOUD_LUX_WINDOW', '10')),
|
||||
}
|
||||
|
||||
# ── InfluxDB ──────────────────────────────────────────────────────────────────
|
||||
@@ -229,10 +238,10 @@ def mpu_loop():
|
||||
with _mpu_lock:
|
||||
if _mpu_peak is None or mag > _mpu_peak['magnitude']:
|
||||
_mpu_peak = {
|
||||
'seismic_x': round(ax, 3),
|
||||
'seismic_y': round(ay, 3),
|
||||
'seismic_z': round(az, 3),
|
||||
'magnitude': round(mag, 4),
|
||||
'seismic_x': round(ax, 3),
|
||||
'seismic_y': round(ay, 3),
|
||||
'seismic_z': round(az, 3),
|
||||
'magnitude': round(mag, 4),
|
||||
}
|
||||
time.sleep(0.01)
|
||||
|
||||
@@ -277,6 +286,72 @@ last_lux_time = time.time()
|
||||
daylight_start = None
|
||||
DAYLIGHT_LUX_THRESHOLD = 50
|
||||
|
||||
_cloud_baseline = {}
|
||||
_cloud_lux_window = deque(maxlen=10)
|
||||
|
||||
def _el_bucket(solar_el, bucket_size):
|
||||
if solar_el is None: return None
|
||||
return round(solar_el / bucket_size) * bucket_size
|
||||
|
||||
def moon_phase_factor():
|
||||
known_new = datetime(2024, 1, 11, tzinfo=timezone.utc)
|
||||
lunar_cycle = 29.53058867
|
||||
days = (datetime.now(timezone.utc) - known_new).total_seconds() / 86400
|
||||
phase = (days % lunar_cycle) / lunar_cycle
|
||||
return round(math.cos(math.pi * (phase - 0.5)) * 0.5 + 0.5, 3)
|
||||
|
||||
def update_cloud_baseline(c, solar_el, lux):
|
||||
bucket = _el_bucket(solar_el, c['CLOUD_BUCKET_SIZE'])
|
||||
if bucket is None: return
|
||||
if bucket not in _cloud_baseline:
|
||||
_cloud_baseline[bucket] = deque(maxlen=c['CLOUD_HISTORY'])
|
||||
_cloud_baseline[bucket].append(lux)
|
||||
|
||||
def get_expected_lux(c, solar_el):
|
||||
bucket = _el_bucket(solar_el, c['CLOUD_BUCKET_SIZE'])
|
||||
if bucket is None: return None
|
||||
history = _cloud_baseline.get(bucket)
|
||||
if not history or len(history) < c['CLOUD_MIN_SAMPLES']: return None
|
||||
sorted_vals = sorted(history)
|
||||
idx = max(0, int(len(sorted_vals) * c['CLOUD_BASELINE_PCT']) - 1)
|
||||
baseline = sorted_vals[idx]
|
||||
if solar_el is not None and solar_el < 0:
|
||||
baseline = baseline * moon_phase_factor()
|
||||
return round(baseline, 2)
|
||||
|
||||
def classify_clouds(c, lux, solar_el):
|
||||
# Resize window if config changed
|
||||
if _cloud_lux_window.maxlen != c['CLOUD_LUX_WINDOW']:
|
||||
_cloud_lux_window.__init__(maxlen=c['CLOUD_LUX_WINDOW'])
|
||||
_cloud_lux_window.append(lux)
|
||||
|
||||
expected = get_expected_lux(c, solar_el)
|
||||
if expected is None or expected < 0.1:
|
||||
return None, None, None
|
||||
|
||||
ratio = min(lux / expected, 1.0)
|
||||
cloud_pct = round((1 - ratio) * 100, 1)
|
||||
|
||||
volatility = None
|
||||
if len(_cloud_lux_window) >= 3:
|
||||
mean = statistics.mean(_cloud_lux_window)
|
||||
if mean > 0:
|
||||
volatility = round(statistics.stdev(_cloud_lux_window) / mean, 3)
|
||||
|
||||
vt = c['CLOUD_VOLATILITY_THRESH']
|
||||
if volatility is not None and volatility > vt and c['CLOUD_CLEAR_MAX'] < cloud_pct < c['CLOUD_CLOUDY_MAX']:
|
||||
label = 'Partly Cloudy'
|
||||
elif cloud_pct < c['CLOUD_CLEAR_MAX']:
|
||||
label = 'Clear'
|
||||
elif cloud_pct < c['CLOUD_MOSTLY_CLEAR_MAX']:
|
||||
label = 'Mostly Clear'
|
||||
elif cloud_pct < c['CLOUD_CLOUDY_MAX']:
|
||||
label = 'Cloudy'
|
||||
else:
|
||||
label = 'Overcast'
|
||||
|
||||
return cloud_pct, label, volatility
|
||||
|
||||
def visibility_estimate(lux, humidity):
|
||||
base_km = 50.0
|
||||
hum_factor = max(0, 1 - ((humidity - 40) / 60)) if humidity > 40 else 1.0
|
||||
@@ -290,7 +365,7 @@ def read_ltr(c, sensor, solar_el, humidity=None):
|
||||
null = {
|
||||
'lux': None, 'uv_index': None, 'solar_wm2': None,
|
||||
'uv_dose': None, 'clouds': None, 'daily_light_integral': None,
|
||||
'daylight': None, 'visibility': None,
|
||||
'daylight': None, 'visibility': None, 'cloud_volatility': None,
|
||||
}
|
||||
|
||||
if not sensor: write(c, 'light', null); return
|
||||
@@ -323,31 +398,20 @@ def read_ltr(c, sensor, solar_el, humidity=None):
|
||||
daylight_start = now
|
||||
daylight_hours = round((now - daylight_start) / 3600, 2) if daylight_start else 0.0
|
||||
|
||||
burn_min = round(c['SKIN_TYPE_2_MED'] / (uvi * 25 / 1000), 1) if uvi > 0 else None
|
||||
cloud_pct, cloud_label, volatility = classify_clouds(c, lux, solar_el)
|
||||
|
||||
cloud_pct, cloud_label = None, None
|
||||
if solar_el is not None and solar_el > 0:
|
||||
cos_z = math.cos(math.radians(90 - solar_el))
|
||||
theoretical = 1361 * cos_z * 0.75
|
||||
if theoretical > 0:
|
||||
ratio = min((lux / 120) / theoretical, 1.0)
|
||||
cloud_pct = round((1 - ratio) * 100, 1)
|
||||
cloud_label = (
|
||||
'Clear' if cloud_pct < 20 else
|
||||
'Partly Cloudy' if cloud_pct < 50 else
|
||||
'Mostly Cloudy' if cloud_pct < 85 else
|
||||
'Overcast'
|
||||
)
|
||||
if cloud_label == 'Clear':
|
||||
update_cloud_baseline(c, solar_el, lux)
|
||||
|
||||
write(c, 'light', {
|
||||
'lux': lux,
|
||||
'uv_index': uvi,
|
||||
'solar_wm2': round(lux / 120, 2),
|
||||
'uv_dose': round(uv_dose_mj, 2),
|
||||
'clouds': cloud_pct,
|
||||
'daily_light_integral': dli,
|
||||
'daylight':daylight_hours,
|
||||
'visibility': visibility_estimate(lux, humidity) if humidity else None,
|
||||
'lux': lux,
|
||||
'uv_index': uvi,
|
||||
'solar_wm2': round(lux / 120, 2),
|
||||
'uv_dose': round(uv_dose_mj, 2),
|
||||
'clouds': cloud_pct,
|
||||
'daily_light_integral': dli,
|
||||
'daylight': daylight_hours,
|
||||
'visibility': visibility_estimate(lux, humidity) if humidity else None,
|
||||
}, tags={'clouds_label': cloud_label or ''})
|
||||
|
||||
# ── TF-Luna ───────────────────────────────────────────────────────────────────
|
||||
@@ -394,9 +458,9 @@ def read_luna(c, temp_c):
|
||||
'slush' if t is not None and t > c['SLUSH_TEMP_THRESHOLD'] else
|
||||
'snow' if strength > c['SNOW_STRENGTH_MIN'] else 'ice'
|
||||
)
|
||||
fields['accumulation'] = depth
|
||||
fields['accumulation'] = depth
|
||||
else:
|
||||
fields['accumulation'] = 0
|
||||
fields['accumulation'] = 0
|
||||
write(c, 'accumulation', fields, tags={'accumulation_type': kind})
|
||||
except Exception as e:
|
||||
print(f"[TF-Luna] {e}")
|
||||
@@ -447,8 +511,8 @@ def read_as3935(c):
|
||||
if avg_late < avg_early - 2: trend = 'Approaching'
|
||||
elif avg_late > avg_early + 2: trend = 'Retreating'
|
||||
write(c, 'lightning', {
|
||||
'lightning_distance': dist,
|
||||
'lightning_energy': energy,
|
||||
'lightning_distance': dist,
|
||||
'lightning_energy': energy,
|
||||
'lightning_rate': strike_rate,
|
||||
}, tags={'storm_direction': trend})
|
||||
except Exception as e:
|
||||
@@ -466,16 +530,16 @@ def read_gps(c):
|
||||
if 'GGA' in line:
|
||||
msg = pynmea2.parse(line)
|
||||
gps_cache.update({
|
||||
'latitude': msg.latitude,
|
||||
'longitude': msg.longitude,
|
||||
'altitude': msg.altitude,
|
||||
'latitude': msg.latitude,
|
||||
'longitude': msg.longitude,
|
||||
'altitude': msg.altitude,
|
||||
'gps_satellites': int(msg.num_sats),
|
||||
})
|
||||
if gps_cache:
|
||||
write(c, 'gps', {k: v for k, v in gps_cache.items() if v is not None})
|
||||
except Exception as e:
|
||||
print(f"[GPS] {e}")
|
||||
write(c, 'gps', {'latitude': None, 'longitude': None, 'altitude': None,'gps_satellites': None})
|
||||
write(c, 'gps', {'latitude': None, 'longitude': None, 'altitude': None, 'gps_satellites': None})
|
||||
return gps_cache
|
||||
|
||||
def solar_elevation(lat, lon):
|
||||
@@ -494,9 +558,7 @@ def solar_elevation(lat, lon):
|
||||
|
||||
def read_rain(c):
|
||||
try:
|
||||
write(c, 'rain', {
|
||||
'precipitation': 0,
|
||||
}, tags={'raining': False})
|
||||
write(c, 'rain', {'precipitation': 0}, tags={'raining': False})
|
||||
except Exception as e:
|
||||
print(f"[Rain] {e}")
|
||||
|
||||
@@ -504,11 +566,7 @@ def read_rain(c):
|
||||
|
||||
def read_wind(c):
|
||||
try:
|
||||
write(c, 'wind', {
|
||||
'wind_direction': 0,
|
||||
'wind_speed': 0,
|
||||
'wind_gusts': 0,
|
||||
})
|
||||
write(c, 'wind', {'wind_direction': 0, 'wind_speed': 0, 'wind_gusts': 0})
|
||||
except Exception as e:
|
||||
print(f"[Wind] {e}")
|
||||
|
||||
@@ -516,7 +574,6 @@ def read_wind(c):
|
||||
|
||||
def read_system(c):
|
||||
try:
|
||||
# CPU temp (Linux hwmon/thermal)
|
||||
cpu_temp = None
|
||||
temps = psutil.sensors_temperatures()
|
||||
for key in ('cpu_thermal', 'coretemp', 'k10temp', 'acpitz'):
|
||||
@@ -529,15 +586,15 @@ def read_system(c):
|
||||
disk = psutil.disk_usage('/')
|
||||
|
||||
write(c, 'system', {
|
||||
'cpu_temp': cpu_temp,
|
||||
'cpu_usage': round(psutil.cpu_percent(interval=None), 1),
|
||||
'cpu_freq_mhz': round(cpu_freq.current, 1) if cpu_freq else None,
|
||||
'mem_used_mb': round(vm.used / 1024**2, 1),
|
||||
'mem_total_mb': round(vm.total / 1024**2, 1),
|
||||
'mem_percent': vm.percent,
|
||||
'disk_used_gb': round(disk.used / 1024**3, 2),
|
||||
'disk_total_gb': round(disk.total / 1024**3, 2),
|
||||
'disk_percent': disk.percent,
|
||||
'cpu_temp': cpu_temp,
|
||||
'cpu_usage': round(psutil.cpu_percent(interval=None), 1),
|
||||
'cpu_freq_mhz': round(cpu_freq.current, 1) if cpu_freq else None,
|
||||
'mem_used_mb': round(vm.used / 1024**2, 1),
|
||||
'mem_total_mb': round(vm.total / 1024**2, 1),
|
||||
'mem_percent': vm.percent,
|
||||
'disk_used_gb': round(disk.used / 1024**3, 2),
|
||||
'disk_total_gb': round(disk.total / 1024**3, 2),
|
||||
'disk_percent': disk.percent,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[System] {e}")
|
||||
|
||||
Reference in New Issue
Block a user