Skip to content

Commit b4deb47

Browse files
committed
Add BARTdepart transit departure usermod
1 parent 8aeb9e1 commit b4deb47

15 files changed

+1007
-0
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#include "bart_station_model.h"
2+
#include "util.h"
3+
4+
#include <algorithm>
5+
#include <cstdio>
6+
#include <cstring>
7+
8+
BartStationModel::Platform::Platform(const String& platformId)
9+
: platformId_(platformId) {}
10+
11+
void BartStationModel::Platform::update(const JsonObject& root) {
12+
if (platformId_.isEmpty()) return;
13+
14+
ETDBatch batch;
15+
const char* dateStr = root["date"] | "";
16+
const char* timeStr = root["time"] | "";
17+
batch.apiTs = parseHeaderTimestamp(dateStr, timeStr);
18+
batch.ourTs = bartdepart::util::time_now();
19+
20+
if (root["station"].is<JsonArray>()) {
21+
for (JsonObject station : root["station"].as<JsonArray>()) {
22+
if (!station["etd"].is<JsonArray>()) continue;
23+
for (JsonObject etd : station["etd"].as<JsonArray>()) {
24+
if (!etd["estimate"].is<JsonArray>()) continue;
25+
bool matches = false;
26+
for (JsonObject est : etd["estimate"].as<JsonArray>()) {
27+
if (String(est["platform"] | "0") != platformId_) continue;
28+
matches = true;
29+
int mins = atoi(est["minutes"] | "0");
30+
time_t dep = batch.apiTs + mins * 60;
31+
TrainColor col = parseTrainColor(est["color"] | "");
32+
batch.etds.push_back(ETD{dep, col});
33+
}
34+
if (matches) {
35+
String dest = etd["destination"] | "";
36+
if (!dest.isEmpty()) {
37+
auto it = std::find(destinations_.begin(), destinations_.end(), dest);
38+
if (it == destinations_.end()) destinations_.push_back(dest);
39+
}
40+
}
41+
}
42+
}
43+
}
44+
45+
std::sort(batch.etds.begin(), batch.etds.end(),
46+
[](const ETD& a, const ETD& b){ return a.estDep < b.estDep; });
47+
48+
history_.push_back(std::move(batch));
49+
while (history_.size() > 5) {
50+
history_.pop_front();
51+
}
52+
53+
DEBUG_PRINTF("BartDepart::update platform %s: %s\n",
54+
platformId_.c_str(), toString().c_str());
55+
}
56+
57+
void BartStationModel::Platform::merge(const Platform& other) {
58+
for (auto const& b : other.history_) {
59+
history_.push_back(b);
60+
if (history_.size() > 5) history_.pop_front();
61+
}
62+
63+
for (auto const& d : other.destinations_) {
64+
auto it = std::find(destinations_.begin(), destinations_.end(), d);
65+
if (it == destinations_.end()) destinations_.push_back(d);
66+
}
67+
}
68+
69+
time_t BartStationModel::Platform::oldest() const {
70+
if (history_.empty()) return 0;
71+
return history_.front().ourTs;
72+
}
73+
74+
const String& BartStationModel::Platform::platformId() const {
75+
return platformId_;
76+
}
77+
78+
const std::deque<BartStationModel::Platform::ETDBatch>&
79+
BartStationModel::Platform::history() const {
80+
return history_;
81+
}
82+
83+
const std::vector<String>& BartStationModel::Platform::destinations() const {
84+
return destinations_;
85+
}
86+
87+
String BartStationModel::Platform::toString() const {
88+
if (history_.empty()) return String();
89+
90+
const ETDBatch& batch = history_.back();
91+
const auto& etds = batch.etds;
92+
if (etds.empty()) return String();
93+
94+
char nowBuf[20];
95+
bartdepart::util::fmt_local(nowBuf, sizeof(nowBuf), batch.ourTs, "%H:%M:%S");
96+
int lagSecs = batch.ourTs - batch.apiTs;
97+
98+
String out;
99+
out += nowBuf;
100+
out += ": lag ";
101+
out += lagSecs;
102+
out += ":";
103+
104+
time_t prevTs = batch.ourTs;
105+
106+
for (const auto& e : etds) {
107+
out += " +";
108+
int diff = e.estDep - prevTs;
109+
out += diff / 60;
110+
out += " (";
111+
prevTs = e.estDep;
112+
113+
char depBuf[20];
114+
bartdepart::util::fmt_local(depBuf, sizeof(depBuf), e.estDep, "%H:%M:%S");
115+
out += depBuf;
116+
out += ":";
117+
out += ::toString(e.color);
118+
out += ")";
119+
}
120+
return out;
121+
}
122+
123+
time_t BartStationModel::Platform::parseHeaderTimestamp(const char* dateStr,
124+
const char* timeStr) const {
125+
int month=0, day=0, year=0;
126+
int hour=0, min=0, sec=0;
127+
char ampm[3] = {0};
128+
sscanf(dateStr, "%d/%d/%d", &month, &day, &year);
129+
sscanf(timeStr, "%d:%d:%d %2s", &hour, &min, &sec, ampm);
130+
if (strcasecmp(ampm, "PM") == 0 && hour < 12) hour += 12;
131+
if (strcasecmp(ampm, "AM") == 0 && hour == 12) hour = 0;
132+
struct tm tm{};
133+
tm.tm_year = year - 1900;
134+
tm.tm_mon = month - 1;
135+
tm.tm_mday = day;
136+
tm.tm_hour = hour;
137+
tm.tm_min = min;
138+
tm.tm_sec = sec;
139+
return mktime(&tm) - bartdepart::util::current_offset(); // return UTC
140+
}
141+
142+
void BartStationModel::update(std::time_t now, BartStationModel&& delta) {
143+
for (auto &p : delta.platforms) {
144+
auto it = std::find_if(platforms.begin(), platforms.end(),
145+
[&](const Platform& x){ return x.platformId() == p.platformId(); });
146+
if (it != platforms.end()) {
147+
it->merge(p);
148+
} else {
149+
platforms.push_back(std::move(p));
150+
}
151+
}
152+
}
153+
154+
time_t BartStationModel::oldest() const {
155+
time_t oldest = 0;
156+
for (auto const& p : platforms) {
157+
time_t o = p.oldest();
158+
if (!oldest || (o && o < oldest)) oldest = o;
159+
}
160+
return oldest;
161+
}
162+
163+
std::vector<String> BartStationModel::destinationsForPlatform(const String& platformId) const {
164+
for (auto const& p : platforms) {
165+
if (p.platformId() == platformId) {
166+
return p.destinations();
167+
}
168+
}
169+
return {};
170+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#pragma once
2+
3+
#include "wled.h"
4+
5+
#include <deque>
6+
#include <vector>
7+
#include <ctime>
8+
9+
#include "train_color.h"
10+
11+
struct BartStationModel {
12+
struct Platform {
13+
struct ETD {
14+
time_t estDep;
15+
TrainColor color;
16+
};
17+
struct ETDBatch {
18+
time_t apiTs;
19+
time_t ourTs;
20+
std::vector<ETD> etds;
21+
};
22+
23+
explicit Platform(const String& platformId);
24+
25+
void update(const JsonObject& root);
26+
void merge(const Platform& other);
27+
time_t oldest() const;
28+
const String& platformId() const;
29+
const std::deque<ETDBatch>& history() const;
30+
const std::vector<String>& destinations() const;
31+
String toString() const;
32+
33+
private:
34+
String platformId_;
35+
std::deque<ETDBatch> history_;
36+
std::vector<String> destinations_;
37+
38+
// return UTC tstamp
39+
time_t parseHeaderTimestamp(const char* dateStr, const char* timeStr) const;
40+
};
41+
42+
std::vector<Platform> platforms;
43+
44+
void update(std::time_t now, BartStationModel&& delta);
45+
time_t oldest() const;
46+
std::vector<String> destinationsForPlatform(const String& platformId) const;
47+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#pragma once
2+
3+
#include <ctime>
4+
#include <memory>
5+
#include <string>
6+
#include "wled.h"
7+
8+
/// Config interface
9+
struct IConfigurable {
10+
virtual ~IConfigurable() = default;
11+
virtual void addToConfig(JsonObject& root) = 0;
12+
virtual void appendConfigData(Print& s) {}
13+
virtual bool readFromConfig(JsonObject& root,
14+
bool startup_complete,
15+
bool& invalidate_history) = 0;
16+
virtual const char* configKey() const = 0;
17+
};
18+
19+
/// Templated data source interface
20+
/// @tparam ModelType The concrete data model type
21+
template<typename ModelType>
22+
class IDataSourceT : public IConfigurable {
23+
public:
24+
virtual ~IDataSourceT() = default;
25+
26+
/// Fetch new data, nullptr if no new data
27+
virtual std::unique_ptr<ModelType> fetch(std::time_t now) = 0;
28+
29+
/// Backfill older history if needed, nullptr if no new data
30+
virtual std::unique_ptr<ModelType> checkhistory(std::time_t now, std::time_t oldestTstamp) = 0;
31+
32+
/// Force the internal schedule to fetch ASAP (e.g. after ON or re-enable)
33+
virtual void reload(std::time_t now) = 0;
34+
35+
/// Identify the source (optional)
36+
virtual std::string name() const = 0;
37+
};
38+
39+
/// Templated data view interface
40+
/// @tparam ModelType The concrete data model type
41+
template<typename ModelType>
42+
class IDataViewT : public IConfigurable {
43+
public:
44+
virtual ~IDataViewT() = default;
45+
46+
/// Render the model to output (LEDs, serial, etc.)
47+
virtual void view(std::time_t now, const ModelType& model, int16_t dbgPixelIndex) = 0;
48+
49+
/// Identify the view (optional)
50+
virtual std::string name() const = 0;
51+
52+
/// Append DebugPixel info
53+
virtual void appendDebugPixel(Print& s) const = 0;
54+
55+
/// Append config page info, optionally using the latest model data
56+
virtual void appendConfigData(Print& s, const ModelType* model) {
57+
IConfigurable::appendConfigData(s);
58+
}
59+
};
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#include "legacy_bart_source.h"
2+
#include "util.h"
3+
4+
LegacyBartSource::LegacyBartSource() {
5+
client_.setInsecure();
6+
}
7+
8+
void LegacyBartSource::reload(std::time_t now) {
9+
nextFetch_ = now;
10+
backoffMult_ = 1;
11+
}
12+
13+
static String composeUrl(const String& base, const String& key, const String& station) {
14+
String url = base;
15+
url += "&key="; url += key;
16+
url += "&orig="; url += station;
17+
return url;
18+
}
19+
20+
std::unique_ptr<BartStationModel> LegacyBartSource::fetch(std::time_t now) {
21+
if (now == 0 || now < nextFetch_) return nullptr;
22+
23+
String url = composeUrl(apiBase_, apiKey_, apiStation_);
24+
if (!https_.begin(client_, url)) {
25+
https_.end();
26+
DEBUG_PRINTLN(F("BartDepart: LegacyBartSource::fetch: trouble initiating request"));
27+
nextFetch_ = now + updateSecs_ * backoffMult_;
28+
if (backoffMult_ < 16) backoffMult_ *= 2;
29+
return nullptr;
30+
}
31+
DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: free heap before GET: %u\n",
32+
ESP.getFreeHeap());
33+
int httpCode = https_.GET();
34+
if (httpCode <= 0) {
35+
https_.end();
36+
DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: https get error code: %d\n", httpCode);
37+
nextFetch_ = now + updateSecs_ * backoffMult_;
38+
if (backoffMult_ < 16) backoffMult_ *= 2;
39+
return nullptr;
40+
}
41+
String payload = https_.getString();
42+
https_.end();
43+
44+
size_t jsonSz = payload.length() * 2;
45+
DynamicJsonDocument doc(jsonSz);
46+
auto err = deserializeJson(doc, payload);
47+
if (err) {
48+
nextFetch_ = now + updateSecs_ * backoffMult_;
49+
if (backoffMult_ < 16) backoffMult_ *= 2;
50+
return nullptr;
51+
}
52+
53+
JsonObject root = doc["root"].as<JsonObject>();
54+
if (root.isNull()) {
55+
nextFetch_ = now + updateSecs_ * backoffMult_;
56+
if (backoffMult_ < 16) backoffMult_ *= 2;
57+
return nullptr;
58+
}
59+
60+
std::unique_ptr<BartStationModel> model(new BartStationModel());
61+
for (const String& pid : platformIds()) {
62+
if (pid.isEmpty()) continue;
63+
BartStationModel::Platform tp(pid);
64+
tp.update(root);
65+
model->platforms.push_back(std::move(tp));
66+
}
67+
68+
nextFetch_ = now + updateSecs_;
69+
backoffMult_ = 1;
70+
return model;
71+
}
72+
73+
void LegacyBartSource::addToConfig(JsonObject& root) {
74+
root["UpdateSecs"] = updateSecs_;
75+
root["ApiBase"] = apiBase_;
76+
root["ApiKey"] = apiKey_;
77+
root["ApiStation"] = apiStation_;
78+
}
79+
80+
bool LegacyBartSource::readFromConfig(JsonObject& root, bool startup_complete, bool& invalidate_history) {
81+
bool ok = true;
82+
ok &= getJsonValue(root["UpdateSecs"], updateSecs_, 60);
83+
ok &= getJsonValue(root["ApiBase"], apiBase_, apiBase_);
84+
ok &= getJsonValue(root["ApiKey"], apiKey_, apiKey_);
85+
ok &= getJsonValue(root["ApiStation"], apiStation_, apiStation_);
86+
invalidate_history = true;
87+
return ok;
88+
}

0 commit comments

Comments
 (0)