439 lines
11 KiB
C
439 lines
11 KiB
C
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"].as<uint32_t>() + root["dstOffset"].as<uint32_t>();
|
|
|
|
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;
|
|
default: 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 = false;
|
|
|
|
if (activeMotionStart > 0)
|
|
{
|
|
switch (motionTriggerSettings->enabledDuringTimeTrigger())
|
|
{
|
|
case Never:
|
|
inMotionTrigger = !inTimeTrigger;
|
|
break;
|
|
|
|
case Always:
|
|
inMotionTrigger = true;
|
|
break;
|
|
|
|
case Only:
|
|
inMotionTrigger = inTimeTrigger;
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
bool motionChanged = inMotionTrigger != lastMotion;
|
|
lastMotion = inMotionTrigger;
|
|
|
|
if (motionTriggerSettingsChanged)
|
|
{
|
|
motionChanged = true;
|
|
motionTriggerSettingsChanged = false;
|
|
}
|
|
|
|
|
|
if (!motionChanged && !timeTriggerChanged)
|
|
return;
|
|
|
|
|
|
_d("Triggers :: motionChanged = "); _dln(motionChanged);
|
|
_d("Triggers :: timeTriggerChanged = "); _dln(timeTriggerChanged);
|
|
|
|
|
|
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);
|
|
}
|
|
} |