Skip to content

Instantly share code, notes, and snippets.

@jdanders
Forked from sean6541/cwop_upload.yaml
Last active August 2, 2026 02:39
Show Gist options
  • Select an option

  • Save jdanders/56e5e49c185adec4020f2603e4eb8342 to your computer and use it in GitHub Desktop.

Select an option

Save jdanders/56e5e49c185adec4020f2603e4eb8342 to your computer and use it in GitHub Desktop.
Upload data from Home Assistant to the CWOP.
#You should use https://github.com/lancer73/ha-weather-uploader instead
blueprint:
name: 'CWOP Upload'
description: >
Periodically uploads weather station data from Home Assistant to CWOP
(Citizen Weather Observer Program) via APRS-IS, whenever any of the
configured trigger entities change state.
Setup:
Add the following shell_command to configuration.yaml. This pipes the
generated APRS packet to `nc`, which opens a TCP connection to APRS-IS,
sends the login + weather packet, and closes.
shell_command:
cwop_upload: >-
printf "%b" "{{ packet }}" | nc -w 10 {{ host }} {{ port }}
Notes on sensor units:
- Temperature sensors: °F or °C (auto-converted to °F for APRS).
- Wind speed / gust sensors: mph or km/h (auto-converted to mph).
- Rain sensors (hourly/24hr/daily): inches or mm (auto-converted to
hundredths of an inch, per APRS spec).
- Pressure sensor: mbar/hPa or any other unit assumed to be inHg
(auto-converted to tenths of a millibar).
- Luminosity sensor: W/m² or lux (auto-converted to W/m²; values
≥1000 W/m² use the APRS 'l' overflow field per spec).
- Any sensor left blank is reported as APRS "not available" (dots).
domain: automation
source_url: 'https://gist.github.com/jdanders/56e5e49c185adec4020f2603e4eb8342#file-cwop_upload-yaml'
input:
host:
name: 'APRS Internet Service (APRS-IS) Host'
selector:
text:
default: 'cwop.aprs.net'
port:
name: 'Port (DO NOT CHANGE)'
description: 'The standard APRS-IS full-feed port. Only change this if you know what you are doing.'
selector:
text:
default: '14580'
callsign:
name: 'CWOP Callsign'
description: 'Your registered CWOP station ID, e.g. CW1234.'
selector:
text:
default: ''
station:
name: 'Station Type'
description: '2–4 character code identifying the type of weather station hardware.'
selector:
text:
default: ''
lat:
name: 'Station Latitude'
description: 'APRS-compliant format, e.g. 4903.50N.'
selector:
text:
default: ''
lon:
name: 'Station Longitude'
description: 'APRS-compliant format, e.g. 07201.75W.'
selector:
text:
default: ''
trigger_entities:
name: 'Trigger Entities'
description: 'Entities to watch for state changes; any change triggers an upload (subject to the delay below).'
selector:
entity:
multiple: true
default: ''
delay:
name: 'Delay After Uploading'
description: 'Minimum time to wait after an upload before another can be triggered, to avoid flooding APRS-IS.'
selector:
number:
min: 0
max: 300
step: 1
unit_of_measurement: seconds
mode: slider
default: 10
sensor_winddir:
name: 'Wind Direction Sensor'
description: 'Must report direction in degrees (0–360).'
selector:
entity:
domain: sensor
device_class: wind_direction
default: ''
sensor_windspeed:
name: 'Wind Speed Sensor'
description: 'Accepts mph or km/h.'
selector:
entity:
domain: sensor
device_class: wind_speed
default: ''
sensor_windgust:
name: 'Wind Gust Sensor'
description: 'Accepts mph or km/h.'
selector:
entity:
domain: sensor
device_class: wind_speed
default: ''
sensor_temp:
name: 'Temperature Sensor'
description: 'Accepts °F or °C.'
selector:
entity:
domain: sensor
device_class: temperature
default: ''
sensor_hrrain:
name: 'Rainfall Last Hour Sensor'
description: 'Accepts inches or mm.'
selector:
entity:
domain: sensor
default: ''
sensor_twentyfourhrrain:
name: 'Rainfall Last 24 Hours Sensor'
description: 'Accepts inches or mm.'
selector:
entity:
domain: sensor
default: ''
sensor_dayrain:
name: 'Rainfall Since Midnight Sensor'
description: 'Accepts inches or mm.'
selector:
entity:
domain: sensor
default: ''
sensor_humidity:
name: 'Humidity Sensor'
selector:
entity:
domain: sensor
device_class: humidity
default: ''
sensor_pressure:
name: 'Barometric Pressure Sensor'
description: 'Accepts mbar/hPa; any other unit is assumed to be inHg.'
selector:
entity:
domain: sensor
device_class:
- pressure
- atmospheric_pressure
default: ''
sensor_luminosity:
name: 'Luminosity Sensor'
description: 'Accepts W/m² or lux.'
selector:
entity:
domain: sensor
device_class:
- irradiance
- illuminance
default: ''
mode: single
max_exceeded: silent
trigger:
- platform: state
entity_id: !input trigger_entities
variables:
host: !input host
port: !input port
callsign: !input callsign
station: !input station
lat: !input lat
lon: !input lon
delay: !input delay
sensor_winddir: !input sensor_winddir
sensor_windspeed: !input sensor_windspeed
sensor_windgust: !input sensor_windgust
sensor_temp: !input sensor_temp
sensor_hrrain: !input sensor_hrrain
sensor_twentyfourhrrain: !input sensor_twentyfourhrrain
sensor_dayrain: !input sensor_dayrain
sensor_humidity: !input sensor_humidity
sensor_pressure: !input sensor_pressure
sensor_luminosity: !input sensor_luminosity
# APRS timestamp: HHMMSS in UTC
time: >
{% set utc_time = utcnow() %}
{{ '%02d' | format(utc_time.hour | int) }}{{ '%02d' | format(utc_time.minute | int) }}{{ '%02d' | format(utc_time.second | int) }}
# Wind direction in degrees, zero-padded to 3 digits: _DDD
winddir: >
{% if sensor_winddir == '' %}
_...
{% else %}
{% set value = states(sensor_winddir) %}
_{{
'...' if value in ('unavailable', 'unknown')
else '%03d' | format(value | float | round | int)
}}
{% endif %}
# Sustained wind speed in mph, converts from km/h: /SSS
windspeed: >
{% if sensor_windspeed == '' %}
/...
{% else %}
{% set value = states(sensor_windspeed) %}
{% set unit_of_measurement = state_attr(sensor_windspeed, 'unit_of_measurement') %}
/{{
'...' if value in ('unavailable', 'unknown')
else '%03d' | format(((value | float) * (0.6213 if unit_of_measurement == 'km/h' else 1)) | round | int)
}}
{% endif %}
# Wind gust in mph, converts from km/h: gGGG
windgust: >
{% if sensor_windgust == '' %}
g...
{% else %}
{% set value = states(sensor_windgust) %}
{% set unit_of_measurement = state_attr(sensor_windgust, 'unit_of_measurement') %}
g{{
'...' if value in ('unavailable', 'unknown')
else '%03d' | format(((value | float) * (0.6213 if unit_of_measurement == 'km/h' else 1)) | round | int)
}}
{% endif %}
# Temperature in °F, converts from °C: tTTT
temp: >
{% if sensor_temp == '' %}
t...
{% else %}
{% set value = states(sensor_temp) %}
{% set unit_of_measurement = state_attr(sensor_temp, 'unit_of_measurement') %}
t{{
'...' if value in ('unavailable', 'unknown')
else '%03d' | format(value | float | round | int) if unit_of_measurement == '°F'
else '%03d' | format((((value | float) * 1.8) + 32) | round | int)
}}
{% endif %}
# Rainfall in the last hour, hundredths of an inch, converts from mm: rRRR
hrrain: >
{% if sensor_hrrain == '' %}
r...
{% else %}
{% set value = states(sensor_hrrain) %}
{% set unit_of_measurement = state_attr(sensor_hrrain, 'unit_of_measurement') %}
r{{
'...' if value in ('unavailable', 'unknown')
else '%03d' | format((((value | float) / (25.4 if unit_of_measurement == 'mm' else 1)) * 100) | round | int)
}}
{% endif %}
# Rainfall in the last 24 hours, hundredths of an inch, converts from mm: pPPP
twentyfourhrrain: >
{% if sensor_twentyfourhrrain == '' %}
p...
{% else %}
{% set value = states(sensor_twentyfourhrrain) %}
{% set unit_of_measurement = state_attr(sensor_twentyfourhrrain, 'unit_of_measurement') %}
p{{
'...' if value in ('unavailable', 'unknown')
else '%03d' | format((((value | float) / (25.4 if unit_of_measurement == 'mm' else 1)) * 100) | round | int)
}}
{% endif %}
# Rainfall since midnight, hundredths of an inch, converts from mm: PPPP
dayrain: >
{% if sensor_dayrain == '' %}
P...
{% else %}
{% set value = states(sensor_dayrain) %}
{% set unit_of_measurement = state_attr(sensor_dayrain, 'unit_of_measurement') %}
P{{
'...' if value in ('unavailable', 'unknown')
else '%03d' | format((((value | float) / (25.4 if unit_of_measurement == 'mm' else 1)) * 100) | round | int)
}}
{% endif %}
# Relative humidity, 2 digits (100% encoded as "00"): hHH
humidity: >
{% if sensor_humidity == '' %}
h..
{% else %}
{% set value = states(sensor_humidity) %}
{% if value in ('unavailable', 'unknown') %}
h..
{% else %}
{% set filtered_value = value | float | round | int %}
h{{
'..' if filtered_value < 1 else '00' if filtered_value == 100 else '%02d' | format(filtered_value)
}}
{% endif %}
{% endif %}
# Barometric pressure, tenths of a millibar, converts from inHg: bBBBBB
pressure: >
{% if sensor_pressure == '' %}
b.....
{% else %}
{% set value = states(sensor_pressure) %}
{% set unit_of_measurement = state_attr(sensor_pressure, 'unit_of_measurement') %}
b{{
'.....' if value in ('unavailable', 'unknown')
else '%05d' | format(((value | float) * 338.639) | round | int) if unit_of_measurement not in ['mbar', 'hPa']
else '%05d' | format(((value | float) * 10) | round | int)
}}
{% endif %}
# Solar irradiance in W/m², converts from lux; uses 'l' prefix above 1000: LLLL / lLLL
luminosity: >
{% if sensor_luminosity == '' %}
L...
{% else %}
{% set value = states(sensor_luminosity) %}
{% set unit_of_measurement = state_attr(sensor_luminosity, 'unit_of_measurement') %}
{% if value in ('unavailable', 'unknown') %}
L...
{% else %}
{% set converted_value = ((value | float) / (126.7 if unit_of_measurement == 'lux' else 1)) | round | int %}
{% if converted_value < 1000 %}
L{{ '%03d' | format(converted_value) }}
{% else %}
l{{ '%03d' | format(converted_value - 1000) }}
{% endif %}
{% endif %}
{% endif %}
action:
- alias: 'Upload to CWOP'
if: >-
{{ host != '' and port != '' and callsign != '' and lat != '' and lon != '' }}
then:
- action: shell_command.cwop_upload
data:
host: '{{ host }}'
port: '{{ port }}'
packet: >-
user {{ callsign }} pass -1 vers HomeAssistant\n{{ callsign }}>APRS,TCPIP*:/{{ time }}h{{ lat }}/{{ lon }}{{ winddir }}{{ windspeed }}{{ windgust }}{{ temp }}{{ hrrain }}{{ twentyfourhrrain }}{{ dayrain }}{{ humidity }}{{ pressure }}{{ luminosity }}{{ station }}\n
- alias: 'Wait To Prevent Multiple Uploads'
delay:
seconds: '{{ delay | int }}'
@jdanders

jdanders commented Aug 2, 2026

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment