An interesting math problem. I’d like to have a template sensor which averages the result of 3 incoming angle sensors, all reporting in degrees from 0-359.9. The circular mean is a special case since of course the correct answer for the average of a 350 degree reading and a 10 degree reading and a 0 degree reading is 0, but (350+10+0)/3 = 120… wrong.

There’s a formula described here on Wikipedia, but I’m not adept enough with template sensors to implement this. (average the sine, average the cosine, take the arctan of sin-avg/cos-avg) Anyone have an idea?

Actually my availability template has an issue, it is possible that r < 0 could still be valid for positive angles. This should work for all angles, negative and positive:

template:
- sensor:
- name: "Average Angle"
unit_of_measurement: "°"
state_class: "measurement"
state: >
{% set a1 = states('sensor.your_1st_sensor_here')|float('unknown') %}
{% set a2 = states('sensor.your_2nd_sensor_here')|float('unknown') %}
{% set a3 = states('sensor.your_3rd_sensor_here')|float('unknown') %}
{% if 'unknown' not in [a1,a2,a3] %}
{% set s = ( sin(a1,'unknown') + sin(a2,'unknown') + sin(a3,'unknown') ) / 3 %}
{% set c = ( cos(a1,'unknown') + cos(a2,'unknown') + cos(a3,'unknown') ) / 3 %}
{% set r = atan(s/c,'unknown') %}
{% if 'unknown' in [c,s,r] %}
unknown
{% elif s > 0 and c > 0 %}
{{ r }}
{% elif c < 0 %}
{{ r + 180 }}
{% elif s < 0 and c > 0 %}
{{ r + 360 }}
{% else %}
unknown
{% endif %}
{% else %}
unknown
{% endif %}
availability: "{{ 'unknown' not in [a1,a2,a3,c,s,r] }}"

A small but important correction, the formula only works in radians. So each variable has to be converted to radian, then the answer back to degrees. Rounded to whole digit degree answer.

- name: Circular Mean 3 sensors
unique_id: circular_mean_of_3_sensors
unit_of_measurement: '°'
icon: mdi:compass-rose
state_class: "measurement"
state: >-
{% set pi = 3.14159265359 %}
{% set a1 = (states('sensor.direction1')|float('Unknown')*pi/180) %}
{% set a2 = (states('sensor.direction2')|float('Unknown')*pi/180) %}
{% set a3 = (states('sensor.direction3')|float('Unknown')*pi/180) %}
{% if 'Unknown' not in [a1,a2,a3] %}
{% set s = ( sin(a1,'Unknown') + sin(a2,'Unknown') + sin(a3,'Unknown') ) / 3 %}
{% set c = ( cos(a1,'Unknown') + cos(a2,'Unknown') + cos(a3,'Unknown') ) / 3 %}
{% set r = (atan(s/c,'Unknown')*180/pi)|round(0, default='Unknown') %}
{% if 'Unknown' in [c,s,r] %}
Unknown
{% elif s > 0 and c > 0 %}
{{ r }}
{% elif c < 0 %}
{{ r + 180 }}
{% elif s < 0 and c > 0 %}
{{ r + 360 }}
{% else %}
Unknown
{% endif %}
{% else %}
Unknown
{% endif %}
availability: "{{ 'Unknown' not in [a1,a2,a3,c,s,r] }}"