-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Usermod: Add BARTdepart transit departure usermod #4884
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b4deb47
104f016
754db27
353d708
2a11198
e6bbcf3
c323ab5
e34cf5e
0236850
df82eb3
350caef
964c9e9
e679768
03f718b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
#include "bart_station_model.h" | ||
#include "util.h" | ||
|
||
#include <algorithm> | ||
#include <cstdio> | ||
#include <cstring> | ||
|
||
BartStationModel::Platform::Platform(const String& platformId) | ||
: platformId_(platformId) {} | ||
|
||
void BartStationModel::Platform::update(const JsonObject& root) { | ||
if (platformId_.isEmpty()) return; | ||
|
||
ETDBatch batch; | ||
const char* dateStr = root["date"] | ""; | ||
const char* timeStr = root["time"] | ""; | ||
batch.apiTs = parseHeaderTimestamp(dateStr, timeStr); | ||
batch.ourTs = bartdepart::util::time_now(); | ||
|
||
if (root["station"].is<JsonArray>()) { | ||
for (JsonObject station : root["station"].as<JsonArray>()) { | ||
if (!station["etd"].is<JsonArray>()) continue; | ||
for (JsonObject etd : station["etd"].as<JsonArray>()) { | ||
if (!etd["estimate"].is<JsonArray>()) continue; | ||
bool matches = false; | ||
for (JsonObject est : etd["estimate"].as<JsonArray>()) { | ||
if (String(est["platform"] | "0") != platformId_) continue; | ||
matches = true; | ||
int mins = atoi(est["minutes"] | "0"); | ||
time_t dep = batch.apiTs + mins * 60; | ||
TrainColor col = parseTrainColor(est["color"] | ""); | ||
batch.etds.push_back(ETD{dep, col}); | ||
} | ||
if (matches) { | ||
String dest = etd["destination"] | ""; | ||
if (!dest.isEmpty()) { | ||
auto it = std::find(destinations_.begin(), destinations_.end(), dest); | ||
if (it == destinations_.end()) destinations_.push_back(dest); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
std::sort(batch.etds.begin(), batch.etds.end(), | ||
[](const ETD& a, const ETD& b){ return a.estDep < b.estDep; }); | ||
|
||
history_.push_back(std::move(batch)); | ||
while (history_.size() > 5) { | ||
history_.pop_front(); | ||
} | ||
|
||
DEBUG_PRINTF("BartDepart::update platform %s: %s\n", | ||
platformId_.c_str(), toString().c_str()); | ||
} | ||
|
||
void BartStationModel::Platform::merge(const Platform& other) { | ||
for (auto const& b : other.history_) { | ||
history_.push_back(b); | ||
if (history_.size() > 5) history_.pop_front(); | ||
} | ||
|
||
for (auto const& d : other.destinations_) { | ||
auto it = std::find(destinations_.begin(), destinations_.end(), d); | ||
if (it == destinations_.end()) destinations_.push_back(d); | ||
} | ||
} | ||
|
||
time_t BartStationModel::Platform::oldest() const { | ||
if (history_.empty()) return 0; | ||
return history_.front().ourTs; | ||
} | ||
|
||
const String& BartStationModel::Platform::platformId() const { | ||
return platformId_; | ||
} | ||
|
||
const std::deque<BartStationModel::Platform::ETDBatch>& | ||
BartStationModel::Platform::history() const { | ||
return history_; | ||
} | ||
|
||
const std::vector<String>& BartStationModel::Platform::destinations() const { | ||
return destinations_; | ||
} | ||
|
||
String BartStationModel::Platform::toString() const { | ||
if (history_.empty()) return String(); | ||
|
||
const ETDBatch& batch = history_.back(); | ||
const auto& etds = batch.etds; | ||
if (etds.empty()) return String(); | ||
|
||
char nowBuf[20]; | ||
bartdepart::util::fmt_local(nowBuf, sizeof(nowBuf), batch.ourTs, "%H:%M:%S"); | ||
int lagSecs = batch.ourTs - batch.apiTs; | ||
|
||
String out; | ||
out += nowBuf; | ||
out += ": lag "; | ||
out += lagSecs; | ||
out += ":"; | ||
|
||
time_t prevTs = batch.ourTs; | ||
|
||
for (const auto& e : etds) { | ||
out += " +"; | ||
int diff = e.estDep - prevTs; | ||
out += diff / 60; | ||
out += " ("; | ||
prevTs = e.estDep; | ||
|
||
char depBuf[20]; | ||
bartdepart::util::fmt_local(depBuf, sizeof(depBuf), e.estDep, "%H:%M:%S"); | ||
out += depBuf; | ||
out += ":"; | ||
out += ::toString(e.color); | ||
out += ")"; | ||
} | ||
return out; | ||
} | ||
|
||
time_t BartStationModel::Platform::parseHeaderTimestamp(const char* dateStr, | ||
const char* timeStr) const { | ||
int month=0, day=0, year=0; | ||
int hour=0, min=0, sec=0; | ||
char ampm[3] = {0}; | ||
sscanf(dateStr, "%d/%d/%d", &month, &day, &year); | ||
sscanf(timeStr, "%d:%d:%d %2s", &hour, &min, &sec, ampm); | ||
if (strcasecmp(ampm, "PM") == 0 && hour < 12) hour += 12; | ||
if (strcasecmp(ampm, "AM") == 0 && hour == 12) hour = 0; | ||
struct tm tm{}; | ||
tm.tm_year = year - 1900; | ||
tm.tm_mon = month - 1; | ||
tm.tm_mday = day; | ||
tm.tm_hour = hour; | ||
tm.tm_min = min; | ||
tm.tm_sec = sec; | ||
return mktime(&tm) - bartdepart::util::current_offset(); // return UTC | ||
} | ||
|
||
void BartStationModel::update(std::time_t now, BartStationModel&& delta) { | ||
for (auto &p : delta.platforms) { | ||
auto it = std::find_if(platforms.begin(), platforms.end(), | ||
[&](const Platform& x){ return x.platformId() == p.platformId(); }); | ||
if (it != platforms.end()) { | ||
it->merge(p); | ||
} else { | ||
platforms.push_back(std::move(p)); | ||
} | ||
} | ||
} | ||
|
||
time_t BartStationModel::oldest() const { | ||
time_t oldest = 0; | ||
for (auto const& p : platforms) { | ||
time_t o = p.oldest(); | ||
if (!oldest || (o && o < oldest)) oldest = o; | ||
} | ||
return oldest; | ||
} | ||
|
||
std::vector<String> BartStationModel::destinationsForPlatform(const String& platformId) const { | ||
for (auto const& p : platforms) { | ||
if (p.platformId() == platformId) { | ||
return p.destinations(); | ||
} | ||
} | ||
return {}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
#pragma once | ||
|
||
#include "wled.h" | ||
|
||
#include <deque> | ||
#include <vector> | ||
#include <ctime> | ||
|
||
#include "train_color.h" | ||
|
||
struct BartStationModel { | ||
struct Platform { | ||
struct ETD { | ||
time_t estDep; | ||
TrainColor color; | ||
}; | ||
struct ETDBatch { | ||
time_t apiTs; | ||
time_t ourTs; | ||
std::vector<ETD> etds; | ||
}; | ||
|
||
explicit Platform(const String& platformId); | ||
|
||
void update(const JsonObject& root); | ||
void merge(const Platform& other); | ||
time_t oldest() const; | ||
const String& platformId() const; | ||
const std::deque<ETDBatch>& history() const; | ||
const std::vector<String>& destinations() const; | ||
String toString() const; | ||
|
||
private: | ||
String platformId_; | ||
std::deque<ETDBatch> history_; | ||
std::vector<String> destinations_; | ||
|
||
// return UTC tstamp | ||
time_t parseHeaderTimestamp(const char* dateStr, const char* timeStr) const; | ||
}; | ||
|
||
std::vector<Platform> platforms; | ||
|
||
void update(std::time_t now, BartStationModel&& delta); | ||
time_t oldest() const; | ||
std::vector<String> destinationsForPlatform(const String& platformId) const; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
#pragma once | ||
|
||
#include <ctime> | ||
#include <memory> | ||
#include <string> | ||
#include "wled.h" | ||
|
||
/// Config interface | ||
struct IConfigurable { | ||
virtual ~IConfigurable() = default; | ||
virtual void addToConfig(JsonObject& root) = 0; | ||
virtual void appendConfigData(Print& s) {} | ||
virtual bool readFromConfig(JsonObject& root, | ||
bool startup_complete, | ||
bool& invalidate_history) = 0; | ||
virtual const char* configKey() const = 0; | ||
}; | ||
|
||
/// Templated data source interface | ||
/// @tparam ModelType The concrete data model type | ||
template<typename ModelType> | ||
class IDataSourceT : public IConfigurable { | ||
public: | ||
virtual ~IDataSourceT() = default; | ||
|
||
/// Fetch new data, nullptr if no new data | ||
virtual std::unique_ptr<ModelType> fetch(std::time_t now) = 0; | ||
|
||
/// Backfill older history if needed, nullptr if no new data | ||
virtual std::unique_ptr<ModelType> checkhistory(std::time_t now, std::time_t oldestTstamp) = 0; | ||
|
||
/// Force the internal schedule to fetch ASAP (e.g. after ON or re-enable) | ||
virtual void reload(std::time_t now) = 0; | ||
|
||
/// Identify the source (optional) | ||
virtual std::string name() const = 0; | ||
}; | ||
|
||
/// Templated data view interface | ||
/// @tparam ModelType The concrete data model type | ||
template<typename ModelType> | ||
class IDataViewT : public IConfigurable { | ||
public: | ||
virtual ~IDataViewT() = default; | ||
|
||
/// Render the model to output (LEDs, serial, etc.) | ||
virtual void view(std::time_t now, const ModelType& model, int16_t dbgPixelIndex) = 0; | ||
|
||
/// Identify the view (optional) | ||
virtual std::string name() const = 0; | ||
|
||
/// Append DebugPixel info | ||
virtual void appendDebugPixel(Print& s) const = 0; | ||
|
||
/// Append config page info, optionally using the latest model data | ||
virtual void appendConfigData(Print& s, const ModelType* model) { | ||
IConfigurable::appendConfigData(s); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,95 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
#include "legacy_bart_source.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
#include "util.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
LegacyBartSource::LegacyBartSource() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
client_.setInsecure(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
void LegacyBartSource::reload(std::time_t now) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
nextFetch_ = now; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
backoffMult_ = 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
static String composeUrl(const String& base, const String& key, const String& station) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
String url = base; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
url += "&key="; url += key; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
url += "&orig="; url += station; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return url; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
std::unique_ptr<BartStationModel> LegacyBartSource::fetch(std::time_t now) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (now == 0 || now < nextFetch_) return nullptr; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
String url = composeUrl(apiBase_, apiKey_, apiStation_); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!https_.begin(client_, url)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
https_.end(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
DEBUG_PRINTLN(F("BartDepart: LegacyBartSource::fetch: trouble initiating request")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
nextFetch_ = now + updateSecs_ * backoffMult_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (backoffMult_ < 16) backoffMult_ *= 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return nullptr; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: free heap before GET: %u\n", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
ESP.getFreeHeap()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
int httpCode = https_.GET(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (httpCode < 200 || httpCode >= 300) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
https_.end(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: HTTP status not OK: %d\n", httpCode); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
nextFetch_ = now + updateSecs_ * backoffMult_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (backoffMult_ < 16) backoffMult_ *= 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return nullptr; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
String payload = https_.getString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
https_.end(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
size_t jsonSz = payload.length() * 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
DynamicJsonDocument doc(jsonSz); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
auto err = deserializeJson(doc, payload); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
nextFetch_ = now + updateSecs_ * backoffMult_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (backoffMult_ < 16) backoffMult_ *= 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return nullptr; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+41
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Stream JSON instead of building a String to cut peak RAM and avoid OOM. Current flow holds payload String and DynamicJsonDocument simultaneously. Stream-deserialize from the HTTPClient stream, size the doc from Content-Length, and log parse errors. Apply: - String payload = https_.getString();
- https_.end();
-
- size_t jsonSz = payload.length() * 2;
- DynamicJsonDocument doc(jsonSz);
- auto err = deserializeJson(doc, payload);
+ size_t contentLen = https_.getSize();
+ size_t jsonSz = contentLen > 0 ? (size_t)(contentLen + 512) : 4096; // heuristic
+ DynamicJsonDocument doc(jsonSz);
+ auto err = deserializeJson(doc, https_.getStream());
+ https_.end();
if (err) {
+ DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: JSON parse error: %s\n", err.c_str());
nextFetch_ = now + updateSecs_ * backoffMult_;
if (backoffMult_ < 16) backoffMult_ *= 2;
return nullptr;
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
JsonObject root = doc["root"].as<JsonObject>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (root.isNull()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
nextFetch_ = now + updateSecs_ * backoffMult_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (backoffMult_ < 16) backoffMult_ *= 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return nullptr; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
std::unique_ptr<BartStationModel> model(new BartStationModel()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
for (const String& pid : platformIds()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (pid.isEmpty()) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
BartStationModel::Platform tp(pid); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
tp.update(root); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
model->platforms.push_back(std::move(tp)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
nextFetch_ = now + updateSecs_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
backoffMult_ = 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return model; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
void LegacyBartSource::addToConfig(JsonObject& root) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
root["UpdateSecs"] = updateSecs_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
root["ApiBase"] = apiBase_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
root["ApiKey"] = apiKey_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
root["ApiStation"] = apiStation_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
bool LegacyBartSource::readFromConfig(JsonObject& root, bool startup_complete, bool& invalidate_history) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
bool ok = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
uint16_t prevUpdate = updateSecs_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
String prevBase = apiBase_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
String prevKey = apiKey_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
String prevStation= apiStation_; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
ok &= getJsonValue(root["UpdateSecs"], updateSecs_, 60); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
ok &= getJsonValue(root["ApiBase"], apiBase_, apiBase_); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
ok &= getJsonValue(root["ApiKey"], apiKey_, apiKey_); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
ok &= getJsonValue(root["ApiStation"], apiStation_, apiStation_); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
// Only invalidate when source identity changes (base/station) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
invalidate_history |= (apiBase_ != prevBase) || (apiStation_ != prevStation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return ok; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Uh oh!
There was an error while loading. Please reload this page.