#include "net/ha_api.h" #include #include #include #include #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 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()) value = v.as(); 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; }