From bec4144dd2c151bbd59591661c1ef28c4a175cdb Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 15 Apr 2026 06:40:18 +0000 Subject: [PATCH] Upload files to "dock_v2_esp8266" --- dock_v2_esp8266/dock_v2_esp8266.ino | 326 ++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 dock_v2_esp8266/dock_v2_esp8266.ino diff --git a/dock_v2_esp8266/dock_v2_esp8266.ino b/dock_v2_esp8266/dock_v2_esp8266.ino new file mode 100644 index 0000000..83fe2bf --- /dev/null +++ b/dock_v2_esp8266/dock_v2_esp8266.ino @@ -0,0 +1,326 @@ +\ +/**************************************************** + * LADEBOKS V2 + * ESP8266 + INA226 + 20x4 LCD + * Måler V/A/W + * Henter robotstatus fra HA + ****************************************************/ + +#include +#include +#include +#include +#include + +// ---------------- 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"); + } +}