327 lines
8.4 KiB
Arduino
327 lines
8.4 KiB
Arduino
\
|
|
/****************************************************
|
|
* LADEBOKS V2
|
|
* ESP8266 + INA226 + 20x4 LCD
|
|
* Måler V/A/W
|
|
* Henter robotstatus fra HA
|
|
****************************************************/
|
|
|
|
#include <ESP8266WiFi.h>
|
|
#include <ESP8266HTTPClient.h>
|
|
#include <Wire.h>
|
|
#include <LiquidCrystal_I2C.h>
|
|
#include <time.h>
|
|
|
|
// ---------------- WIFI / HA ----------------
|
|
const char* WIFI_SSID = "DIT_WIFI";
|
|
const char* WIFI_PASSWORD = "DIT_PASS";
|
|
|
|
const char* HA_HOST = "192.168.2.10";
|
|
const uint16_t HA_PORT = 8123;
|
|
const char* HA_TOKEN = "DIN_LONG_LIVED_ACCESS_TOKEN";
|
|
|
|
// ---------------- LCD ----------------
|
|
LiquidCrystal_I2C lcd(0x27, 20, 4);
|
|
|
|
// ---------------- I2C pins ----------------
|
|
const uint8_t I2C_SDA = D2;
|
|
const uint8_t I2C_SCL = D1;
|
|
|
|
// ---------------- INA226 ----------------
|
|
const uint8_t INA226_ADDR = 0x40;
|
|
const uint8_t REG_CONFIG = 0x00;
|
|
const uint8_t REG_BUS_VOLT = 0x02;
|
|
const uint8_t REG_POWER = 0x03;
|
|
const uint8_t REG_CURRENT = 0x04;
|
|
const uint8_t REG_CALIBRATION = 0x05;
|
|
|
|
const float SHUNT_OHMS = 0.01f;
|
|
const float CURRENT_LSB = 0.0005f;
|
|
const uint16_t INA226_CAL = (uint16_t)(0.00512f / (CURRENT_LSB * SHUNT_OHMS));
|
|
|
|
// ---------------- Buffers ----------------
|
|
const int FILTER_SAMPLES = 10;
|
|
float voltsBuf[FILTER_SAMPLES] = {0};
|
|
float ampsBuf[FILTER_SAMPLES] = {0};
|
|
float wattsBuf[FILTER_SAMPLES] = {0};
|
|
int filterIndex = 0;
|
|
bool filterFilled = false;
|
|
|
|
// ---------------- Status fra HA ----------------
|
|
String gpsStatus = "NOFX";
|
|
String motionStatus = "STOP";
|
|
String homeStatus = "AWAY";
|
|
String chargeStatus = "IDLE";
|
|
|
|
// ---------------- Timing ----------------
|
|
unsigned long lastMeasureMs = 0;
|
|
unsigned long lastDisplayMs = 0;
|
|
unsigned long lastPushMs = 0;
|
|
unsigned long lastFetchMs = 0;
|
|
|
|
// ---------------- Helpers ----------------
|
|
String haBaseUrl() {
|
|
return "http://" + String(HA_HOST) + ":" + String(HA_PORT);
|
|
}
|
|
|
|
void wifiConnect() {
|
|
WiFi.mode(WIFI_STA);
|
|
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
|
|
|
unsigned long startMs = millis();
|
|
while (WiFi.status() != WL_CONNECTED) {
|
|
delay(500);
|
|
if (millis() - startMs > 30000) {
|
|
ESP.restart();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ntpSetup() {
|
|
// UTC offset sættes via tzset nedenfor
|
|
configTime(0, 0, "pool.ntp.org", "dk.pool.ntp.org", "time.google.com");
|
|
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0/3", 1);
|
|
tzset();
|
|
}
|
|
|
|
String nowString() {
|
|
time_t now = time(nullptr);
|
|
struct tm* t = localtime(&now);
|
|
if (!t) return "--- --/-- --:--";
|
|
char buf[21];
|
|
snprintf(buf, sizeof(buf), "%02d-%02d-%04d %02d:%02d",
|
|
t->tm_mday, t->tm_mon + 1, t->tm_year + 1900, t->tm_hour, t->tm_min);
|
|
return String(buf);
|
|
}
|
|
|
|
void writeRegister16(uint8_t reg, uint16_t value) {
|
|
Wire.beginTransmission(INA226_ADDR);
|
|
Wire.write(reg);
|
|
Wire.write((value >> 8) & 0xFF);
|
|
Wire.write(value & 0xFF);
|
|
Wire.endTransmission();
|
|
}
|
|
|
|
uint16_t readRegister16u(uint8_t reg) {
|
|
Wire.beginTransmission(INA226_ADDR);
|
|
Wire.write(reg);
|
|
Wire.endTransmission(false);
|
|
Wire.requestFrom((int)INA226_ADDR, 2);
|
|
if (Wire.available() < 2) return 0;
|
|
return ((uint16_t)Wire.read() << 8) | Wire.read();
|
|
}
|
|
|
|
int16_t readRegister16s(uint8_t reg) {
|
|
Wire.beginTransmission(INA226_ADDR);
|
|
Wire.write(reg);
|
|
Wire.endTransmission(false);
|
|
Wire.requestFrom((int)INA226_ADDR, 2);
|
|
if (Wire.available() < 2) return 0;
|
|
return ((int16_t)Wire.read() << 8) | Wire.read();
|
|
}
|
|
|
|
void ina226Setup() {
|
|
writeRegister16(REG_CONFIG, 0x8000);
|
|
delay(10);
|
|
|
|
uint16_t config = 0;
|
|
config |= (0b100 << 9); // avg 16
|
|
config |= (0b100 << 6); // bus conv 1.1ms
|
|
config |= (0b100 << 3); // shunt conv 1.1ms
|
|
config |= (0b111); // continuous shunt+bus
|
|
writeRegister16(REG_CONFIG, config);
|
|
|
|
writeRegister16(REG_CALIBRATION, INA226_CAL);
|
|
}
|
|
|
|
float readBusVoltageV() {
|
|
return readRegister16u(REG_BUS_VOLT) * 0.00125f;
|
|
}
|
|
|
|
float readCurrentA() {
|
|
return readRegister16s(REG_CURRENT) * CURRENT_LSB;
|
|
}
|
|
|
|
float readPowerW() {
|
|
float powerLSB = 25.0f * CURRENT_LSB;
|
|
return readRegister16u(REG_POWER) * powerLSB;
|
|
}
|
|
|
|
void pushFilter(float v, float a, float w) {
|
|
voltsBuf[filterIndex] = v;
|
|
ampsBuf[filterIndex] = a;
|
|
wattsBuf[filterIndex] = w;
|
|
|
|
filterIndex++;
|
|
if (filterIndex >= FILTER_SAMPLES) {
|
|
filterIndex = 0;
|
|
filterFilled = true;
|
|
}
|
|
}
|
|
|
|
float avgBuf(float* buf) {
|
|
int count = filterFilled ? FILTER_SAMPLES : filterIndex;
|
|
if (count <= 0) return 0;
|
|
float sum = 0;
|
|
for (int i = 0; i < count; i++) sum += buf[i];
|
|
return sum / count;
|
|
}
|
|
|
|
bool postState(const String& entityId, const String& state, const String& unit = "", const String& devClass = "", const String& stateClass = "") {
|
|
WiFiClient client;
|
|
HTTPClient http;
|
|
|
|
String url = haBaseUrl() + "/api/states/" + entityId;
|
|
if (!http.begin(client, url)) return false;
|
|
|
|
http.addHeader("Authorization", "Bearer " + String(HA_TOKEN));
|
|
http.addHeader("Content-Type", "application/json");
|
|
|
|
String json = "{\"state\":\"" + state + "\",\"attributes\":{";
|
|
bool first = true;
|
|
|
|
if (unit.length()) {
|
|
json += "\"unit_of_measurement\":\"" + unit + "\"";
|
|
first = false;
|
|
}
|
|
if (devClass.length()) {
|
|
if (!first) json += ",";
|
|
json += "\"device_class\":\"" + devClass + "\"";
|
|
first = false;
|
|
}
|
|
if (stateClass.length()) {
|
|
if (!first) json += ",";
|
|
json += "\"state_class\":\"" + stateClass + "\"";
|
|
first = false;
|
|
}
|
|
if (!first) json += ",";
|
|
json += "\"friendly_name\":\"" + entityId + "\"";
|
|
json += "}}";
|
|
|
|
int code = http.POST(json);
|
|
http.end();
|
|
return (code >= 200 && code < 300);
|
|
}
|
|
|
|
String extractState(const String& payload) {
|
|
int p = payload.indexOf("\"state\":\"");
|
|
if (p < 0) return "unknown";
|
|
p += 9;
|
|
int e = payload.indexOf("\"", p);
|
|
if (e < 0) return "unknown";
|
|
return payload.substring(p, e);
|
|
}
|
|
|
|
String getEntityState(const String& entityId) {
|
|
WiFiClient client;
|
|
HTTPClient http;
|
|
String url = haBaseUrl() + "/api/states/" + entityId;
|
|
|
|
if (!http.begin(client, url)) return "unknown";
|
|
http.addHeader("Authorization", "Bearer " + String(HA_TOKEN));
|
|
int code = http.GET();
|
|
String payload = (code > 0) ? http.getString() : "";
|
|
http.end();
|
|
|
|
if (code < 200 || code >= 300) return "unknown";
|
|
return extractState(payload);
|
|
}
|
|
|
|
void updateDisplay(float v, float a, float w) {
|
|
// Linje 1: dato/tid
|
|
lcd.setCursor(0, 0);
|
|
lcd.print(" ");
|
|
lcd.setCursor(0, 0);
|
|
lcd.print(nowString());
|
|
|
|
// Linje 2: spænding / strøm
|
|
lcd.setCursor(0, 1);
|
|
lcd.print("U:");
|
|
lcd.print(v, 2);
|
|
lcd.print(" I:");
|
|
lcd.print(a, 2);
|
|
lcd.print(" ");
|
|
|
|
// Linje 3: effekt / GPS
|
|
lcd.setCursor(0, 2);
|
|
lcd.print("P:");
|
|
lcd.print(w, 1);
|
|
lcd.print("W GPS:");
|
|
lcd.print(gpsStatus);
|
|
lcd.print(" ");
|
|
|
|
// Linje 4: HOME/AWAY CHG/IDLE RUN/STOP
|
|
lcd.setCursor(0, 3);
|
|
lcd.print(homeStatus);
|
|
lcd.print(" ");
|
|
lcd.print(chargeStatus);
|
|
lcd.print(" ");
|
|
lcd.print(motionStatus);
|
|
lcd.print(" ");
|
|
}
|
|
|
|
void setup() {
|
|
Wire.begin(I2C_SDA, I2C_SCL);
|
|
|
|
lcd.init();
|
|
lcd.backlight();
|
|
lcd.clear();
|
|
lcd.setCursor(0, 0);
|
|
lcd.print("Starter ladeboks");
|
|
|
|
wifiConnect();
|
|
ntpSetup();
|
|
ina226Setup();
|
|
}
|
|
|
|
void loop() {
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
wifiConnect();
|
|
}
|
|
|
|
unsigned long now = millis();
|
|
|
|
if (now - lastMeasureMs >= 250) {
|
|
lastMeasureMs = now;
|
|
float v = readBusVoltageV();
|
|
float a = readCurrentA();
|
|
float w = readPowerW();
|
|
|
|
if (a < 0) a = 0;
|
|
if (w < 0) w = 0;
|
|
|
|
pushFilter(v, a, w);
|
|
}
|
|
|
|
if (now - lastFetchMs >= 5000) {
|
|
lastFetchMs = now;
|
|
|
|
String gpsRaw = getEntityState("sensor.robot_gps_status");
|
|
String motionRaw = getEntityState("sensor.robot_motion_status");
|
|
String homeRaw = getEntityState("sensor.robot_home_status");
|
|
String chargeRaw = getEntityState("sensor.robot_charge_status");
|
|
|
|
gpsStatus = (gpsRaw == "FIX") ? "FIX" : "NOFX";
|
|
motionStatus = (motionRaw == "MOVING") ? "RUN" : "STOP";
|
|
homeStatus = (homeRaw == "HOME") ? "HOME" : "AWAY";
|
|
chargeStatus = (chargeRaw == "CHARGING") ? "CHG" : "IDLE";
|
|
}
|
|
|
|
if (now - lastDisplayMs >= 1000) {
|
|
lastDisplayMs = now;
|
|
updateDisplay(avgBuf(voltsBuf), avgBuf(ampsBuf), avgBuf(wattsBuf));
|
|
}
|
|
|
|
if (now - lastPushMs >= 5000) {
|
|
lastPushMs = now;
|
|
float v = avgBuf(voltsBuf);
|
|
float a = avgBuf(ampsBuf);
|
|
float w = avgBuf(wattsBuf);
|
|
|
|
postState("sensor.robot_dock_voltage", String(v, 2), "V", "voltage", "measurement");
|
|
postState("sensor.robot_dock_current", String(a, 3), "A", "current", "measurement");
|
|
postState("sensor.robot_dock_power", String(w, 1), "W", "power", "measurement");
|
|
postState("binary_sensor.robot_dock_online", "on");
|
|
}
|
|
}
|