Files
thermostat-hw/main/api/ha_template.h

79 lines
4.8 KiB
C

/*
* The Jinja template POSTed to Home Assistant's /api/template.
*
* Why a template rather than plain REST calls:
*
* - One request instead of a dozen. /api/states returns all ~204 entities
* (far too much to buffer); fetching entities one at a time would mean a
* dozen round-trips inside the window where the UI is frozen. This is a
* single ~430 byte reply, measured at ~85ms on the LAN.
* - HA formats the times. sunrise/sunset/clock/date come back as local-time
* strings, so the device needs neither SNTP nor a timezone database, and
* DST is somebody else's problem. The board's clock starts at the epoch
* and that no longer matters.
* - Unavailable sensors arrive as JSON null, not a sentinel number. A
* sentinel would be indistinguishable from a real reading -- -1 mph of
* wind is impossible, but -1 degF of dew point is not.
*
* The reply is compact JSON; see ha_parse_json().
*/
#ifndef HA_TEMPLATE_H
#define HA_TEMPLATE_H
#include "ha_config.h"
/* Emits the entity list for the lights loop, in HA_LIGHT_LIST order. The
* trailing comma is deliberate -- Jinja accepts it, and it avoids a
* last-element special case. The icon argument is the UI layer's. */
#define HA_TMPL_LIGHT_X(id, label, dimmable, icon) "'" id "',"
/*
* num() -- a state as a number, or null if it is not one ("unavailable",
* "unknown", or a non-numeric state).
* attrnum() -- ditto for an attribute; brightness is None when a light is off.
*/
#define HA_STATE_TEMPLATE \
"{%- macro num(e) -%}{%- set v = states(e) -%}" \
"{{ v | float if is_number(v) else 'null' }}{%- endmacro -%}" \
"{%- macro attrnum(e, a) -%}{%- set v = state_attr(e, a) -%}" \
"{{ v if v is number else 'null' }}{%- endmacro -%}" \
"{" \
"\"out_t\":" "{{ num('" HA_ENT_OUTDOOR_TEMP "') }}," \
"\"out_h\":" "{{ num('" HA_ENT_OUTDOOR_HUMIDITY "') }}," \
"\"in_t\":" "{{ num('" HA_ENT_INDOOR_TEMP "') }}," \
"\"in_h\":" "{{ num('" HA_ENT_INDOOR_HUMIDITY "') }}," \
"\"dew\":" "{{ num('" HA_ENT_DEW_POINT "') }}," \
"\"press\":" "{{ num('" HA_ENT_PRESSURE "') }}," \
"\"rain_d\":" "{{ num('" HA_ENT_RAIN_TODAY "') }}," \
"\"rain_r\":" "{{ num('" HA_ENT_RAIN_RATE "') }}," \
"\"wind\":" "{{ num('" HA_ENT_WIND_SPEED "') }}," \
"\"gust\":" "{{ num('" HA_ENT_WIND_GUST "') }}," \
"\"wdir\":" "{{ num('" HA_ENT_WIND_DIR "') }}," \
"\"cond\":\"" "{{ states('" HA_ENT_WEATHER "') }}\"," \
"\"sunrise\":\"{{ (state_attr('" HA_ENT_SUN "','next_rising')" \
" | as_datetime | as_local).strftime('%-I:%M %p') }}\"," \
"\"sunset\":\"{{ (state_attr('" HA_ENT_SUN "','next_setting')" \
" | as_datetime | as_local).strftime('%-I:%M %p') }}\"," \
"\"sun_up\":{{ (states('" HA_ENT_SUN "') == 'above_horizon') | lower }}," \
"\"clock\":\"{{ now().strftime('%-I:%M %p') }}\"," \
"\"date\":\"{{ now().strftime('%a %b %-d') }}\"," \
"\"lights\":[" \
"{%- for e in [" HA_LIGHT_LIST(HA_TMPL_LIGHT_X) "] -%}" \
"{\"s\":\"{{ states(e) }}\",\"b\":{{ attrnum(e,'brightness') }}}" \
"{{ \",\" if not loop.last }}" \
"{%- endfor -%}" \
"]}"
/*
* Forecast body for POST /api/services/weather/get_forecasts?return_response.
*
* The forecast is deliberately NOT part of the template above: HA removed the
* `forecast` attribute from weather entities in 2024.4, and Jinja templates
* cannot call services. It also changes far more slowly than the sensors, so
* it is polled on its own longer interval.
*/
#define HA_FORECAST_BODY \
"{\"entity_id\":\"" HA_ENT_WEATHER "\",\"type\":\"daily\"}"
#endif