Public Access
178 lines
4.3 KiB
C++
178 lines
4.3 KiB
C++
#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 watched;
|
||
|
|
uint32_t next_poll;
|
||
|
|
};
|
||
|
|
|
||
|
|
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 = 16;
|
||
|
|
|
||
|
|
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, millis()});
|
||
|
|
return entries.back();
|
||
|
|
}
|
||
|
|
|
||
|
|
void haApiWatch(const DynamicPage *page)
|
||
|
|
{
|
||
|
|
for (Entry &e : entries)
|
||
|
|
e.watched = false;
|
||
|
|
if (!page)
|
||
|
|
return;
|
||
|
|
for (int i = 0; i < page->element_count; i++)
|
||
|
|
{
|
||
|
|
const PageElement &element = page->elements[i];
|
||
|
|
if (element.type != ElementType::HaValue || !validEntityId(element.entity))
|
||
|
|
continue;
|
||
|
|
Entry &e = add(element.entity, element.attribute);
|
||
|
|
if (!e.watched)
|
||
|
|
e.next_poll = millis();
|
||
|
|
e.watched = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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
|
||
|
|
}
|
||
|
|
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 + HA_POLL_INTERVAL;
|
||
|
|
String value;
|
||
|
|
const char *error = fetch(e.entity, e.attribute, value);
|
||
|
|
if (error)
|
||
|
|
{
|
||
|
|
// Shown as "--" until it can be read again
|
||
|
|
Serial.printf("Home Assistant %s: %s\n", e.entity.c_str(), error);
|
||
|
|
bool changed = e.known;
|
||
|
|
e.known = false;
|
||
|
|
return changed;
|
||
|
|
}
|
||
|
|
bool changed = !e.known || e.value != value;
|
||
|
|
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;
|
||
|
|
}
|