Initial release of ASL3 Weather Announcer (v1.0)

main
swanie98635 2 months ago
parent f46e53000d
commit c5eb5dba4f

4
.gitignore vendored

@ -18,3 +18,7 @@ asl3_wx_announce/config.yaml
# OS
.DS_Store
Thumbs.db
venv/
.idea/

@ -9,7 +9,8 @@ class AudioHandler:
self.logger = logging.getLogger(__name__)
# Default to echo if no TTS configured (just for safety)
self.tts_template = config.get('voice', {}).get('tts_command', 'echo "{text}" > {file}')
self.output_dir = "/tmp/asl3_wx"
# Use standard Asterisk sounds directory
self.output_dir = "/var/lib/asterisk/sounds/asl3_wx_announce"
os.makedirs(self.output_dir, exist_ok=True)
def generate_audio(self, text: str, filename: str = "announcement.wav") -> str:
@ -30,9 +31,8 @@ class AudioHandler:
raise e
def play_on_nodes(self, filepath: str, nodes: List[str]):
# Asterisk uses file path WITHOUT extension usually for play commands,
# but rpt localplay might require full path or specific format.
# Usually: rpt localplay <node> <path_no_ext>
# Asterisk uses file path WITHOUT extension usually for play commands.
# KD5FMU script uses absolute path successfully (e.g. /tmp/current-time)
path_no_ext = os.path.splitext(filepath)[0]
for node in nodes:

@ -43,7 +43,7 @@ def do_full_report(config):
# Audio
handler = AudioHandler(config)
wav_file = handler.generate_audio(text, "report.wav")
wav_file = handler.generate_audio(text, "report.gsm")
# Play
nodes = config.get('audio', {}).get('nodes', [])
@ -78,7 +78,7 @@ def monitor_loop(config):
if new_alerts:
logger.info(f"New Alerts detected: {len(new_alerts)}")
text = narrator.announce_alerts(new_alerts)
wav = handler.generate_audio(text, "alert.wav")
wav = handler.generate_audio(text, "alert.gsm")
handler.play_on_nodes(wav, nodes)
# Cleanup expired from known

@ -1,4 +1,5 @@
from datetime import datetime
import pytz
from typing import List
from .models import LocationInfo, CurrentConditions, WeatherForecast, WeatherAlert
@ -6,14 +7,19 @@ class Narrator:
def __init__(self):
pass
def _c_to_f(self, temp_c: float) -> int:
return int((temp_c * 9/5) + 32)
def announce_conditions(self, loc: LocationInfo, current: CurrentConditions) -> str:
wind = ""
if current.wind_speed and current.wind_speed > 5:
wind = f", with winds from the {current.wind_direction} at {int(current.wind_speed)} kilometers per hour"
# Describe wind speed in km/h and mph
mph = int(current.wind_speed * 0.621371)
wind = f", with winds from the {current.wind_direction} at {int(current.wind_speed)} kilometers per hour, or {mph} miles per hour"
return (
f"Current conditions for {loc.city}, {loc.region}. "
f"The temperature is {int(current.temperature)} degrees celsius. "
f"The temperature is {int(current.temperature)} degrees celsius, {self._c_to_f(current.temperature)} degrees fahrenheit. "
f"Conditions are {current.description}{wind}."
)
@ -25,9 +31,9 @@ class Narrator:
for f in forecasts[:3]: # Read first 3 periods
temp = ""
if f.high_temp is not None:
temp = f" with a high of {int(f.high_temp)}"
temp = f" with a high of {int(f.high_temp)} celsius, {self._c_to_f(f.high_temp)} fahrenheit"
elif f.low_temp is not None:
temp = f" with a low of {int(f.low_temp)}"
temp = f" with a low of {int(f.low_temp)} celsius, {self._c_to_f(f.low_temp)} fahrenheit"
text += f"{f.period_name}: {f.summary}{temp}. "
@ -48,8 +54,16 @@ class Narrator:
def build_full_report(self, loc: LocationInfo, current: CurrentConditions, forecast: List[WeatherForecast], alerts: List[WeatherAlert], sun_info: str = "") -> str:
parts = []
# Localize time
try:
tz = pytz.timezone(loc.timezone)
now = datetime.now(tz)
except Exception:
now = datetime.now()
# Time string: "10 30 PM"
now_str = datetime.now().strftime("%I %M %p")
now_str = now.strftime("%I %M %p")
# Remove leading zero from hour if desired, but TTS usually handles "09" fine.
# For cleaner TTS:
if now_str.startswith("0"):

@ -1,6 +1,6 @@
from datetime import datetime
from typing import List
from env_canada import ECData
from env_canada import ECWeather
from ..models import LocationInfo, CurrentConditions, WeatherForecast, WeatherAlert, AlertSeverity
from .base import WeatherProvider
@ -10,8 +10,8 @@ class ECProvider(WeatherProvider):
self.extra_zones = kwargs.get('alerts', {}).get('extra_zones', [])
def _get_ec_data(self, lat, lon):
# ECData auto-selects station based on lat/lon
ec = ECData(coordinates=(lat, lon))
# ECWeather auto-selects station based on lat/lon
ec = ECWeather(coordinates=(lat, lon))
ec.update()
return ec
@ -62,7 +62,7 @@ class ECProvider(WeatherProvider):
for zone_id in self.extra_zones:
if "/" in zone_id: # Basic check if it looks like EC station ID
try:
ec_objects.append(ECData(station_id=zone_id))
ec_objects.append(ECWeather(station_id=zone_id))
except Exception:
pass

@ -59,7 +59,7 @@ class NWSProvider(WeatherProvider):
temp_c = props.get('temperature', {}).get('value')
return CurrentConditions(
temperature=temp_c if temp_c is not None else 0.0,
humidity=props.get('relativeHumidity', {}).get('value'),
humidity=int(round(props.get('relativeHumidity', {}).get('value'))) if props.get('relativeHumidity', {}).get('value') is not None else None,
wind_speed=props.get('windSpeed', {}).get('value'),
wind_direction=str(props.get('windDirection', {}).get('value')),
description=props.get('textDescription', 'Unknown')
@ -142,4 +142,4 @@ class NWSProvider(WeatherProvider):
expires=parse_date(props['expires'])
))
return alerts
return alerts

@ -6,7 +6,7 @@ location:
voice:
# Example utilizing pico2wave (sudo apt install libttspico-utils)
tts_command: 'pico2wave -w {file} "{text}"'
tts_command: 'pico2wave -w /tmp/temp.wav "{text}" && sox /tmp/temp.wav -r 8000 -c 1 {file}'
nodes:
- "YOUR_NODE_NUMBER_HERE"

@ -0,0 +1,8 @@
import env_canada
print("Contents of env_canada:")
print(dir(env_canada))
try:
from env_canada import ECData
print("ECData found!")
except ImportError:
print("ECData NOT found.")

@ -0,0 +1,59 @@
$Server = "192.168.50.150"
$User = Read-Host "Enter SSH Username for $Server (e.g. repeater or root)"
Write-Host "1. Packing files..."
# Create a tarball to avoid copying .git, venv, and permission issues
# Windows tar supports exclusions
tar --exclude ".git" --exclude "venv" --exclude "__pycache__" -cf package.tar .
Write-Host "2. Transferring package..."
scp package.tar "$($User)@$($Server):~"
if ($LASTEXITCODE -ne 0) {
Write-Error "SCP failed. Please check connectivity and credentials."
exit
}
Write-Host "3. Installing System Dependencies and Python Environment..."
# Updated remote script to handle tar unpacking
$RemoteScript = @'
echo '--- unpacking ---'
mkdir -p ~/asl3_wx_announce
mv ~/package.tar ~/asl3_wx_announce/
cd ~/asl3_wx_announce
tar -xf package.tar
rm package.tar
echo '--- Fixing Hostname ---'
sudo bash -c 'grep -q "$(hostname)" /etc/hosts || echo "127.0.0.1 $(hostname)" >> /etc/hosts'
echo '--- Updating Apt ---'
sudo apt update
sudo apt install -y python3-pip python3-venv libttspico-utils gpsd sox libsox-fmt-all
echo '--- Setting up Venv ---'
cd ~/asl3_wx_announce
python3 -m venv venv
source venv/bin/activate
echo '--- Installing Python Libs ---'
pip install -r requirements.txt
echo '--- DEBUG ENV_CANADA ---'
./venv/bin/python3 debug_ec.py
echo '--- Running Test Report ---'
# Run using the venv python
sudo ./venv/bin/python3 -m asl3_wx_announce.main --config config.yaml --report
'@
# Sanitize script for Linux (Remove Carriage Returns)
$RemoteScript = $RemoteScript -replace "`r", ""
# Base64 Encode to avoid quoting hell
$ScriptBytes = [System.Text.Encoding]::UTF8.GetBytes($RemoteScript)
$ScriptBase64 = [Convert]::ToBase64String($ScriptBytes)
# Execute via base64 decode
ssh -t "$($User)@$($Server)" "echo $ScriptBase64 | base64 -d | bash"

@ -2,6 +2,8 @@ requests
astral
pydantic
pyyaml
env_canada
env_canada==0.5.2
reverse_geocoder
numpy
pytz

Loading…
Cancel
Save

Powered by TurnKey Linux.