From c5eb5dba4f0c28f39f28cfc69c19815d61377029 Mon Sep 17 00:00:00 2001 From: swanie98635 Date: Sun, 11 Jan 2026 21:37:33 -0800 Subject: [PATCH] Initial release of ASL3 Weather Announcer (v1.0) --- .gitignore | 4 +++ asl3_wx_announce/audio.py | 8 ++--- asl3_wx_announce/main.py | 4 +-- asl3_wx_announce/narrator.py | 24 ++++++++++--- asl3_wx_announce/provider/ec.py | 8 ++--- asl3_wx_announce/provider/nws.py | 4 +-- config.yaml.example | 2 +- debug_ec.py | 8 +++++ deploy.ps1 | 59 ++++++++++++++++++++++++++++++++ requirements.txt | 4 ++- 10 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 debug_ec.py create mode 100644 deploy.ps1 diff --git a/.gitignore b/.gitignore index 4ab03c8..7a0bc17 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ asl3_wx_announce/config.yaml # OS .DS_Store Thumbs.db + +venv/ +.idea/ + diff --git a/asl3_wx_announce/audio.py b/asl3_wx_announce/audio.py index a352134..8f8e06b 100644 --- a/asl3_wx_announce/audio.py +++ b/asl3_wx_announce/audio.py @@ -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 + # 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: diff --git a/asl3_wx_announce/main.py b/asl3_wx_announce/main.py index 202c525..f1582de 100644 --- a/asl3_wx_announce/main.py +++ b/asl3_wx_announce/main.py @@ -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 diff --git a/asl3_wx_announce/narrator.py b/asl3_wx_announce/narrator.py index 965c7d7..a50ab11 100644 --- a/asl3_wx_announce/narrator.py +++ b/asl3_wx_announce/narrator.py @@ -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"): diff --git a/asl3_wx_announce/provider/ec.py b/asl3_wx_announce/provider/ec.py index 49f025f..30653c6 100644 --- a/asl3_wx_announce/provider/ec.py +++ b/asl3_wx_announce/provider/ec.py @@ -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 diff --git a/asl3_wx_announce/provider/nws.py b/asl3_wx_announce/provider/nws.py index 664d8d5..0d2c746 100644 --- a/asl3_wx_announce/provider/nws.py +++ b/asl3_wx_announce/provider/nws.py @@ -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 \ No newline at end of file diff --git a/config.yaml.example b/config.yaml.example index b0748b4..7dc603c 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -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" diff --git a/debug_ec.py b/debug_ec.py new file mode 100644 index 0000000..41c7e41 --- /dev/null +++ b/debug_ec.py @@ -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.") diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100644 index 0000000..3807845 --- /dev/null +++ b/deploy.ps1 @@ -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" diff --git a/requirements.txt b/requirements.txt index 5e39138..a0b2319 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,8 @@ requests astral pydantic pyyaml -env_canada +env_canada==0.5.2 reverse_geocoder numpy + +pytz