WiFiUDP ntpUDP; uint32_t lastTimeTriggerChecked = 0; TimeTrigger* lastTimeTrigger = nullptr; TimeTrigger* activeTimeTrigger = nullptr; uint32_t lastTimezoneUpdate = 0; AsyncClient* timezoneClient = nullptr; char* response = nullptr; uint16_t responseSize = 0; static const uint16_t ResponseMaxSize = 1024; void initMotionPins() { if (!motionTriggerSettings->enabled()) return; for (uint8_t i = 0; i < motionTriggerSettings->triggerCount(); i++) { MotionTrigger* trigger = motionTriggerSettings->trigger(i); if (trigger->enabled) pinMode(trigger->pin, INPUT); } } void parseResponse() { if (response == nullptr || responseSize == 0) return; _dln("Timezone :: response:"); _dln(response); char* data = response; if (strncmp(data, "HTTP/1.", 7) != 0) { _dln("Timezone :: not an HTTP response"); return; } data += 9; if (strncmp(data, "200", 3) != 0) { _dln("Timezone :: invalid HTTP status code"); return; } data = strstr(data, "\r\n\r\n"); if (data == nullptr) { _dln("Timezone :: end of HTTP headers not found"); return; } data += 4; DynamicJsonBuffer jsonBuffer(JSON_OBJECT_SIZE(5) + 200); JsonObject& root = jsonBuffer.parseObject(data); if (!sameStr(root["status"], "OK")) { _dln("Timezone :: invalid status in response"); return; } timezoneOffset = root["rawOffset"]; hasTimezone = true; } void updateTimezone() { if (timezoneClient != nullptr) return; timezoneClient = new AsyncClient(); if (!timezoneClient) return; timezoneClient->onError([](void* arg, AsyncClient* client, int error) { _d("Timezone :: error "); _dln(error); timezoneClient = nullptr; delete client; lastTimezoneUpdate = currentTime; }, nullptr); timezoneClient->onConnect([](void* arg, AsyncClient* client) { response = (char*)malloc(ResponseMaxSize + 1); responseSize = 0; timezoneClient->onError(nullptr, nullptr); client->onDisconnect([](void * arg, AsyncClient * c) { timezoneClient = nullptr; delete c; lastTimezoneUpdate = currentTime; parseResponse(); free(response); response = nullptr; }, nullptr); client->onData([](void* arg, AsyncClient* c, void* data, size_t len) { uint16_t copyLen = responseSize == ResponseMaxSize ? 0 : responseSize + len > ResponseMaxSize ? ResponseMaxSize - responseSize : len; if (copyLen > 0) { memcpy(response + responseSize, data, copyLen); responseSize += copyLen; response[responseSize] = 0; } }, nullptr); uint32_t timestamp = ntpClient->getEpochTime(); #ifdef MapsAPIViaProxyScript String request = String("GET ") + TimezoneProxyScriptPath + "?location=" + #else String request = "GET /maps/api/timezone/json?location=" + #endif String(systemSettings->latitude(), 7) + "," + String(systemSettings->longitude(), 7) + "8×tamp=" + String(timestamp); if (systemSettings->mapsAPIKey() != nullptr) request = request + "&key=" + systemSettings->mapsAPIKey(); _d("Timezone :: request: "); _dln(request); #ifdef MapsAPIViaProxyScript request = request + " HTTP/1.0\r\nHost: " + TimezoneProxyScriptHost + "\r\n\r\n"; #else request = request + " HTTP/1.0\r\nHost: maps.googleapis.com\r\n\r\n"; #endif client->write(request.c_str()); }, nullptr); _d("Timezone :: available heap: "); _dln(ESP.getFreeHeap()); #ifdef MapsAPIViaProxyScript if(!timezoneClient->connect(TimezoneProxyScriptHost, 80)) #else if(!timezoneClient->connect("maps.googleapis.com", 443, true)) #endif { _dln("Timezone :: failed to connect to host"); AsyncClient * client = timezoneClient; timezoneClient = nullptr; delete client; lastTimezoneUpdate = currentTime; } } void updateNTPClient() { if (ntpClient == nullptr && WiFi.status() == WL_CONNECTED && systemSettings->ntpServer() != nullptr && systemSettings->ntpInterval() > 0) { _dln("NTP :: initializing NTP client"); ntpClient = new NTPClient(ntpUDP, systemSettings->ntpServer(), 0, systemSettings->ntpInterval() * 60 * 1000); ntpClient->begin(); } // Only update if we're not in the middle of a transition, as it will block // the loop until the NTP server responds or times out (up to a second) if (ntpClient != nullptr && !stairs->inTransition()) { ntpClient->update(); // Lat/lng 0,0 is off the African coast, I think we can safely assume nobody // will have WiFi enabled stair lighting at that location. if (timezoneClient == nullptr && systemSettings->latitude() && systemSettings->longitude()) { uint32_t interval = hasTimezone ? systemSettings->ntpInterval() * 60 * 1000 : TimezoneRetryInterval; if (lastTimezoneUpdate == 0 || currentTime - lastTimezoneUpdate > interval) { updateTimezone(); lastTimezoneUpdate = currentTime; } } } } uint32_t lastTimeElementsChecked = 0; uint32_t epochTime = 0; tmElements_t timeElements; bool isDayTime = true; tmElements_t* getTimeElements() { if (ntpClient == nullptr || !hasTimezone) return nullptr; if (lastTimeElementsChecked != 0 && currentTime - lastTimeElementsChecked < 10000) return epochTime > 0 ? &timeElements : nullptr; lastTimeElementsChecked = currentTime; epochTime = ntpClient->getEpochTime(); if (epochTime == 0) { _dln("Triggers:: time not synchronised yet"); return nullptr; } _dln("Triggers:: updating time elements"); breakTime(epochTime + timezoneOffset, timeElements); // TODO this is a copy of what is in time.cpp. This code, and probably a lot more // in this file, could use some cleanup. Dusk2Dawn location(systemSettings->latitude(), systemSettings->longitude(), timezoneOffset / 3600.0f); // DST is always hardcoded as false, since it is already included in timezoneOffset int16_t sunriseMinutes = location.sunrise(timeElements.Year, timeElements.Month, timeElements.Day, false); int16_t sunsetMinutes = location.sunset(timeElements.Year, timeElements.Month, timeElements.Day, false); int16_t timeMinutes = (timeElements.Hour * 60) + timeElements.Minute; isDayTime = timeMinutes >= sunriseMinutes && timeMinutes <= sunsetMinutes; _d("Triggers:: isDayTime = "); _dln(isDayTime); return &timeElements; } void updateTimeTrigger() { if (ntpClient == nullptr || !hasTimezone || !timeTriggerSettings->enabled()) { activeTimeTrigger = nullptr; return; } if (timeTriggerSettingsChanged) { // Time trigger settings changed, activeTimeTrigger pointer is considered // invalid, force recheck timeTriggerSettingsChanged = false; } else if (currentTime - lastTimeTriggerChecked < 10000) return; lastTimeTriggerChecked = currentTime; tmElements_t* time = getTimeElements(); if (time == nullptr) { activeTimeTrigger = nullptr; return; } activeTimeTrigger = timeTriggerSettings->getActiveTrigger(*time); #ifdef SerialDebug _d("Triggers:: active time trigger: "); if (activeTimeTrigger != nullptr) { _d(activeTimeTrigger->brightness); _d(" @ "); switch (activeTimeTrigger->triggerType) { case RelativeToSunrise: _d("sunrise "); break; case RelativeToSunset: _d("sunset "); break; } _dln(activeTimeTrigger->time); } else _dln("null"); #endif } uint32_t activeMotionStart = 0; uint16_t activeMotionBrightness = 0; MotionDirection activeMotionDirection = Nondirectional; bool lastMotion = false; void updateMotionTrigger() { if (motionTriggerSettingsChanged) { initMotionPins(); activeMotionStart = 0; } if (!motionTriggerSettings->enabled() || !motionTriggerSettings->triggerCount()) { activeMotionStart = 0; return; } if (!motionTriggerSettings->enabledDuringDay() && isDayTime) { activeMotionStart = 0; return; } for (uint8_t i = 0; i < motionTriggerSettings->triggerCount(); i++) { MotionTrigger* trigger = motionTriggerSettings->trigger(i); if (trigger->enabled && digitalRead(trigger->pin) == HIGH) { if (activeMotionStart == 0) { activeMotionDirection = trigger->direction; activeMotionBrightness = trigger->brightness; } activeMotionStart = currentTime; } } if (activeMotionStart != 0 && currentTime - activeMotionStart >= motionTriggerSettings->delay()) activeMotionStart = 0; } void checkTriggers() { if (!timeTriggerSettings->enabled() && activeTimeTrigger == nullptr && !motionTriggerSettings->enabled() && activeMotionStart == 0) return; updateTimeTrigger(); updateMotionTrigger(); bool inTimeTrigger = timeTriggerSettings->enabled() && activeTimeTrigger != nullptr && activeTimeTrigger->brightness; bool timeTriggerChanged = activeTimeTrigger != lastTimeTrigger; lastTimeTrigger = activeTimeTrigger; bool inMotionTrigger = (activeMotionStart > 0) && (!inTimeTrigger || motionTriggerSettings->enabledDuringTimeTrigger()); bool motionChanged = (activeMotionStart > 0) != lastMotion; lastMotion = (activeMotionStart > 0); if (motionTriggerSettingsChanged) { motionChanged = true; motionTriggerSettingsChanged = false; } if (!motionChanged && !timeTriggerChanged) return; if (motionChanged) { if (inMotionTrigger) { _dln("Triggers :: start motion trigger"); if (activeMotionDirection == Nondirectional || motionTriggerSettings->transitionTime() == 0) stairs->setAll(activeMotionBrightness, motionTriggerSettings->transitionTime(), 0); else stairs->sweep(activeMotionBrightness, motionTriggerSettings->transitionTime(), activeMotionDirection == TopDown); } else { if (inTimeTrigger) { _dln("Triggers :: motion stopped, falling back to time trigger"); // Fall back to time trigger value stairs->setAll(activeTimeTrigger->brightness, motionTriggerSettings->transitionTime(), 0); } else { _dln("Triggers :: motion stopped, turning off"); // No more motion, no active time trigger, turn off stairs->setAll(0, motionTriggerSettings->transitionTime(), 0); } } } else if (timeTriggerChanged && !inMotionTrigger) { _dln("Triggers :: time trigger changed"); // Set to time trigger value stairs->setAll(activeTimeTrigger->brightness, timeTriggerSettings->transitionTime(), 0); } }