Files
Flipdot-display/src/esp32/net/ha_api.cpp
T

209 lines
5.5 KiB
C++
Raw Normal View History

#include "net/ha_api.h"
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <WiFi.h>
#include <vector>
#include "config.h"
#include "credentials.h"
struct Entry
{
String entity;
String attribute;
String value;
bool known;
bool page_watched; // used by the shown page: read every HA_POLL_INTERVAL
uint32_t rule_interval; // used by trigger rules: read this often (ms), 0 = not
uint32_t next_poll;
bool watched() const { return page_watched || rule_interval; }
uint32_t interval() const
{
uint32_t page = page_watched ? HA_POLL_INTERVAL : UINT32_MAX;
return rule_interval && rule_interval < page ? rule_interval : page;
}
};
static String base_url;
static String token;
static std::vector<Entry> entries;
// Kept for the editor's previews, besides the watched ones
static const size_t MAX_ENTRIES = 24;
void haApiSetSettings(const HomeAssistantSettings &settings)
{
if (settings.api_url[0])
base_url = settings.api_url;
else
base_url = String("http://") + (settings.host[0] ? String(settings.host) : BROKER_ADDR.toString()) + ":8123";
while (base_url.endsWith("/"))
base_url.remove(base_url.length() - 1);
token = settings.token;
for (Entry &e : entries)
e.next_poll = millis(); // read again with the new settings
}
static Entry *find(const char *entity, const char *attribute)
{
for (Entry &e : entries)
if (e.entity == entity && e.attribute == attribute)
return &e;
return nullptr;
}
static Entry &add(const char *entity, const char *attribute)
{
Entry *e = find(entity, attribute);
if (e)
return *e;
if (entries.size() >= MAX_ENTRIES)
for (auto it = entries.begin(); it != entries.end(); ++it)
if (!it->watched())
{
entries.erase(it);
break;
}
entries.push_back({entity, attribute, "", false, false, 0, millis()});
return entries.back();
}
void haApiWatch(const DynamicPage *page)
{
for (Entry &e : entries)
e.page_watched = false;
if (!page)
return;
for (int i = 0; i < page->element_count; i++)
{
const PageElement &element = page->elements[i];
bool uses_state = element.type == ElementType::HaValue || element.type == ElementType::Image;
if (!uses_state || !validEntityId(element.entity))
continue;
Entry &e = add(element.entity, element.attribute);
if (!e.watched())
e.next_poll = millis();
e.page_watched = true;
}
}
void haApiWatchRules(const PageSchedule *schedule)
{
for (Entry &e : entries)
e.rule_interval = 0;
if (!schedule)
return;
for (int i = 0; i < schedule->rule_count; i++)
{
const TriggerRule &rule = schedule->rules[i];
Entry &e = add(rule.entity, rule.attribute);
if (!e.watched())
e.next_poll = millis();
uint32_t ms = rule.interval * 1000UL;
if (!e.rule_interval || ms < e.rule_interval)
e.rule_interval = ms;
}
}
// Blocks at most about 2 x HA_HTTP_TIMEOUT (connect, then the answer)
static const char *fetch(const String &entity, const String &attribute, String &value)
{
if (!token.length())
return "No Home Assistant token set (Home Assistant tab)";
if (!WiFi.isConnected())
return "No WiFi";
HTTPClient http;
http.setConnectTimeout(HA_HTTP_TIMEOUT);
http.setTimeout(HA_HTTP_TIMEOUT);
if (!http.begin(base_url + "/api/states/" + entity))
return "Invalid Home Assistant URL";
http.addHeader("Authorization", "Bearer " + token);
int code = http.GET();
if (code != 200)
{
http.end();
if (code == 401)
return "Home Assistant refused the token";
if (code == 404)
return "Unknown entity";
return code < 0 ? "Home Assistant not reachable" : "Unexpected answer from Home Assistant";
}
JsonDocument filter;
if (attribute.length())
filter["attributes"][attribute] = true;
else
filter["state"] = true;
JsonDocument doc;
DeserializationError error = deserializeJson(doc, http.getString(), DeserializationOption::Filter(filter));
http.end();
if (error)
return "Invalid answer from Home Assistant";
JsonVariantConst v = doc["state"];
if (attribute.length())
v = doc["attributes"][attribute];
if (v.isNull())
return "Unknown attribute";
if (v.is<const char *>())
value = v.as<const char *>();
else
{
value = "";
serializeJson(v, value); // numbers, true/false
}
value.replace("\xC2\xB0", String(DEGREE_SIGN)); // UTF-8 degree sign: the fonts have it as 127
return nullptr;
}
bool haApiLoop(bool wifi_connected)
{
if (!wifi_connected || !token.length())
return false;
uint32_t now = millis();
for (Entry &e : entries)
{
if (!e.watched() || (int32_t)(now - e.next_poll) < 0)
continue;
e.next_poll = now + e.interval();
String value;
const char *error = fetch(e.entity, e.attribute, value);
if (error)
{
// Shown as "--" until it can be read again. Read less often, so a Home
// Assistant that is down doesn't block loop() all the time.
Serial.printf("Home Assistant %s: %s\n", e.entity.c_str(), error);
if (e.interval() < HA_ERROR_INTERVAL)
e.next_poll = now + HA_ERROR_INTERVAL;
bool changed = e.known && e.page_watched;
e.known = false;
return changed;
}
bool changed = (!e.known || e.value != value) && e.page_watched;
e.value = value;
e.known = true;
return changed; // one entity per loop()
}
return false;
}
const char *haApiValue(const char *entity, const char *attribute)
{
Entry *e = find(entity, attribute);
return e && e->known ? e->value.c_str() : nullptr;
}
const char *haApiFetchNow(const char *entity, const char *attribute, String &value)
{
if (!validEntityId(entity))
return "Invalid entity id";
const char *error = fetch(entity, attribute, value);
if (error)
return error;
Entry &e = add(entity, attribute);
e.value = value;
e.known = true;
return nullptr;
}