mirror of
https://github.com/OpenEPaperLink/OpenEPaperLink.git
synced 2026-03-24 20:07:12 +01:00
new content type: day ahead prices (S3-based AP only)
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#define CONTENT_BUIENRADAR
|
||||
#define CONTENT_NFCLUT
|
||||
#define CONTENT_TAGCFG
|
||||
#define CONTENT_DAYAHEAD
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
@@ -198,10 +199,11 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) {
|
||||
imageParams.height = hwdata.height;
|
||||
imageParams.bpp = hwdata.bpp;
|
||||
imageParams.rotatebuffer = hwdata.rotatebuffer;
|
||||
imageParams.highlightColor = getColor(String(hwdata.highlightColor));
|
||||
|
||||
imageParams.hasRed = false;
|
||||
imageParams.dataType = DATATYPE_IMG_RAW_1BPP;
|
||||
imageParams.dither = 0;
|
||||
imageParams.dither = 2;
|
||||
if (taginfo->hasCustomLUT && taginfo->lut != 1) imageParams.grayLut = true;
|
||||
|
||||
imageParams.invert = taginfo->invert;
|
||||
@@ -385,6 +387,7 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) {
|
||||
break;
|
||||
#endif
|
||||
|
||||
#ifdef CONTENT_CAL
|
||||
case 11: // Calendar:
|
||||
|
||||
if (getCalFeed(filename, cfgobj, taginfo, imageParams)) {
|
||||
@@ -395,6 +398,7 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) {
|
||||
taginfo->nextupdate = now + 300;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
|
||||
case 12: // RemoteAP
|
||||
|
||||
@@ -508,7 +512,18 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) {
|
||||
drawTimestamp(filename, cfgobj, taginfo, imageParams);
|
||||
updateTagImage(filename, mac, 0, taginfo, imageParams);
|
||||
taginfo->nextupdate = 3216153600;
|
||||
|
||||
#ifdef CONTENT_DAYAHEAD
|
||||
case 27: // Day Ahead:
|
||||
|
||||
if (getDayAheadFeed(filename, cfgobj, taginfo, imageParams)) {
|
||||
taginfo->nextupdate = now + (3600 - now % 3600);
|
||||
updateTagImage(filename, mac, 0, taginfo, imageParams);
|
||||
} else {
|
||||
taginfo->nextupdate = now + 300;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
taginfo->modeConfigJson = doc.as<String>();
|
||||
@@ -724,14 +739,14 @@ void drawDate(String &filename, tagRecord *&taginfo, imgParam &imageParams) {
|
||||
const auto &date = loc["date"];
|
||||
const auto &weekday = loc["weekday"];
|
||||
if (date) {
|
||||
drawString(spr, languageDays[timeinfo.tm_wday], weekday[0], weekday[1], weekday[2], TC_DATUM, TFT_RED, weekday[3]);
|
||||
drawString(spr, languageDays[timeinfo.tm_wday], weekday[0], weekday[1], weekday[2], TC_DATUM, imageParams.highlightColor, weekday[3]);
|
||||
drawString(spr, String(timeinfo.tm_mday) + " " + languageMonth[timeinfo.tm_mon], date[0], date[1], date[2], TC_DATUM, TFT_BLACK, date[3]);
|
||||
} else {
|
||||
const auto &month = loc["month"];
|
||||
const auto &day = loc["day"];
|
||||
drawString(spr, languageDays[timeinfo.tm_wday], weekday[0], weekday[1], weekday[2], TC_DATUM, TFT_BLACK, weekday[3]);
|
||||
drawString(spr, String(languageMonth[timeinfo.tm_mon]), month[0], month[1], month[2], TC_DATUM, TFT_BLACK, month[3]);
|
||||
drawString(spr, String(timeinfo.tm_mday), day[0], day[1], day[2], TC_DATUM, TFT_RED, day[3]);
|
||||
drawString(spr, String(timeinfo.tm_mday), day[0], day[1], day[2], TC_DATUM, imageParams.highlightColor, day[3]);
|
||||
}
|
||||
|
||||
spr2buffer(spr, filename, imageParams);
|
||||
@@ -768,7 +783,7 @@ void drawNumber(String &filename, int32_t count, int32_t thresholdred, tagRecord
|
||||
initSprite(spr, imageParams.width, imageParams.height, imageParams);
|
||||
uint16_t color = TFT_BLACK;
|
||||
if (countTemp > thresholdred) {
|
||||
color = TFT_RED;
|
||||
color = imageParams.highlightColor;
|
||||
}
|
||||
String font = loc["fonts"][0].as<String>();
|
||||
uint8_t size = loc["fonts"][1].as<uint16_t>();
|
||||
@@ -864,15 +879,15 @@ void drawWeather(String &filename, JsonObject &cfgobj, const tagRecord *taginfo,
|
||||
const auto &location = doc["location"];
|
||||
drawString(spr, cfgobj["location"], location[0], location[1], location[2]);
|
||||
const auto &wind = doc["wind"];
|
||||
drawString(spr, String(windval), wind[0], wind[1], wind[2], TR_DATUM, (beaufort > 4 ? TFT_RED : TFT_BLACK));
|
||||
drawString(spr, String(windval), wind[0], wind[1], wind[2], TR_DATUM, (beaufort > 4 ? imageParams.highlightColor : TFT_BLACK));
|
||||
|
||||
char tmpOutput[5];
|
||||
dtostrf(temperature, 2, 1, tmpOutput);
|
||||
const auto &temp = doc["temp"];
|
||||
drawString(spr, String(tmpOutput), temp[0], temp[1], temp[2], TL_DATUM, (temperature < 0 ? TFT_RED : TFT_BLACK));
|
||||
drawString(spr, String(tmpOutput), temp[0], temp[1], temp[2], TL_DATUM, (temperature < 0 ? imageParams.highlightColor : TFT_BLACK));
|
||||
|
||||
const int iconcolor = (weathercode == 55 || weathercode == 65 || weathercode == 75 || weathercode == 82 || weathercode == 86 || weathercode == 95 || weathercode == 96 || weathercode == 99)
|
||||
? TFT_RED
|
||||
? imageParams.highlightColor
|
||||
: TFT_BLACK;
|
||||
const auto &icon = doc["icon"];
|
||||
drawString(spr, getWeatherIcon(weathercode, isNight), icon[0], icon[1], "/fonts/weathericons.ttf", icon[3], iconcolor, icon[2]);
|
||||
@@ -880,7 +895,7 @@ void drawWeather(String &filename, JsonObject &cfgobj, const tagRecord *taginfo,
|
||||
drawString(spr, windDirectionIcon(winddirection), dir[0], dir[1], "/fonts/weathericons.ttf", TC_DATUM, TFT_BLACK, dir[2]);
|
||||
if (weathercode > 10) {
|
||||
const auto &umbrella = doc["umbrella"];
|
||||
drawString(spr, "\uf084", umbrella[0], umbrella[1], "/fonts/weathericons.ttf", TC_DATUM, TFT_RED, umbrella[2]);
|
||||
drawString(spr, "\uf084", umbrella[0], umbrella[1], "/fonts/weathericons.ttf", TC_DATUM, imageParams.highlightColor, umbrella[2]);
|
||||
}
|
||||
|
||||
spr2buffer(spr, filename, imageParams);
|
||||
@@ -929,7 +944,7 @@ void drawForecast(String &filename, JsonObject &cfgobj, const tagRecord *taginfo
|
||||
if (weathercode > 40) weathercode -= 40;
|
||||
|
||||
const int iconcolor = (weathercode == 55 || weathercode == 65 || weathercode == 75 || weathercode == 82 || weathercode == 86 || weathercode == 95 || weathercode == 96 || weathercode == 99)
|
||||
? TFT_RED
|
||||
? imageParams.highlightColor
|
||||
: TFT_BLACK;
|
||||
drawString(spr, getWeatherIcon(weathercode), loc["icon"][0].as<int>() + dag * column1, loc["icon"][1], "/fonts/weathericons.ttf", TC_DATUM, iconcolor, loc["icon"][2]);
|
||||
|
||||
@@ -948,13 +963,13 @@ void drawForecast(String &filename, JsonObject &cfgobj, const tagRecord *taginfo
|
||||
if (loc["rain"]) {
|
||||
const int8_t rain = round(daily["precipitation_sum"][dag].as<double>());
|
||||
if (rain > 0) {
|
||||
drawString(spr, String(rain) + "mm", dag * column1 + loc["rain"][0].as<int>(), loc["rain"][1], day[2], TC_DATUM, (rain > 10 ? TFT_RED : TFT_BLACK));
|
||||
drawString(spr, String(rain) + "mm", dag * column1 + loc["rain"][0].as<int>(), loc["rain"][1], day[2], TC_DATUM, (rain > 10 ? imageParams.highlightColor : TFT_BLACK));
|
||||
}
|
||||
}
|
||||
|
||||
drawString(spr, String(tmin) + " ", dag * column1 + day[0].as<int>(), day[4], day[2], TR_DATUM, (tmin < 0 ? TFT_RED : TFT_BLACK));
|
||||
drawString(spr, String(" ") + String(tmax), dag * column1 + day[0].as<int>(), day[4], day[2], TL_DATUM, (tmax < 0 ? TFT_RED : TFT_BLACK));
|
||||
drawString(spr, String(wind), dag * column1 + column1 - 10, day[3], day[2], TR_DATUM, (beaufort > 5 ? TFT_RED : TFT_BLACK));
|
||||
drawString(spr, String(tmin) + " ", dag * column1 + day[0].as<int>(), day[4], day[2], TR_DATUM, (tmin < 0 ? imageParams.highlightColor : TFT_BLACK));
|
||||
drawString(spr, String(" ") + String(tmax), dag * column1 + day[0].as<int>(), day[4], day[2], TL_DATUM, (tmax < 0 ? imageParams.highlightColor : TFT_BLACK));
|
||||
drawString(spr, String(wind), dag * column1 + column1 - 10, day[3], day[2], TR_DATUM, (beaufort > 5 ? imageParams.highlightColor : TFT_BLACK));
|
||||
if (dag > 0) {
|
||||
for (int i = loc["line"][0]; i < loc["line"][1]; i += 3) {
|
||||
spr.drawPixel(dag * column1, i, TFT_BLACK);
|
||||
@@ -1115,8 +1130,8 @@ char *epoch_to_display(time_t utc) {
|
||||
return display;
|
||||
}
|
||||
|
||||
bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgParam &imageParams) {
|
||||
#ifdef CONTENT_CAL
|
||||
bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgParam &imageParams) {
|
||||
// google apps scripts method to retrieve calendar
|
||||
// see https://github.com/jjwbruijn/OpenEPaperLink/wiki/Google-Apps-Scripts for description
|
||||
|
||||
@@ -1181,9 +1196,9 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
|
||||
const time_t endtime = obj["end"];
|
||||
|
||||
if (starttime <= now && endtime > now) {
|
||||
spr.fillRect(loc["red"][0], loc["red"][1].as<int>() + i * loc["line"][2].as<int>(), loc["red"][2], loc["red"][3], TFT_RED);
|
||||
drawString(spr, epoch_to_display(obj["start"]), loc["line"][0], loc["line"][1].as<int>() + i * loc["line"][2].as<int>(), loc["line"][3], TL_DATUM, TFT_WHITE, 0, TFT_RED);
|
||||
drawString(spr, eventtitle, loc["line"][4], loc["line"][1].as<int>() + i * loc["line"][2].as<int>(), loc["line"][3], TL_DATUM, TFT_WHITE, 0, TFT_RED);
|
||||
spr.fillRect(loc["red"][0], loc["red"][1].as<int>() + i * loc["line"][2].as<int>(), loc["red"][2], loc["red"][3], imageParams.highlightColor);
|
||||
drawString(spr, epoch_to_display(obj["start"]), loc["line"][0], loc["line"][1].as<int>() + i * loc["line"][2].as<int>(), loc["line"][3], TL_DATUM, TFT_WHITE, 0, imageParams.highlightColor);
|
||||
drawString(spr, eventtitle, loc["line"][4], loc["line"][1].as<int>() + i * loc["line"][2].as<int>(), loc["line"][3], TL_DATUM, TFT_WHITE, 0, imageParams.highlightColor);
|
||||
} else {
|
||||
drawString(spr, epoch_to_display(obj["start"]), loc["line"][0], loc["line"][1].as<int>() + i * loc["line"][2].as<int>(), loc["line"][3], TL_DATUM, TFT_BLACK, 0, TFT_WHITE);
|
||||
drawString(spr, eventtitle, loc["line"][4], loc["line"][1].as<int>() + i * loc["line"][2].as<int>(), loc["line"][3], TL_DATUM, TFT_BLACK, 0, TFT_WHITE);
|
||||
@@ -1220,9 +1235,7 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
|
||||
int calYOffset = loc["gridparam"][1].as<int>();
|
||||
int lineHeight = loc["gridparam"][5].as<int>();
|
||||
|
||||
imageParams.dither = 2;
|
||||
|
||||
// drawString(spr, String(timeinfo.tm_mday), calWidth / 2, -calHeight/5, "Signika-SB.ttf", TC_DATUM, TFT_RED, calHeight * 1.2);
|
||||
// drawString(spr, String(timeinfo.tm_mday), calWidth / 2, -calHeight/5, "Signika-SB.ttf", TC_DATUM, imageParams.highlightColor, calHeight * 1.2);
|
||||
|
||||
for (int i = 0; i < calDays; i++) {
|
||||
struct tm dayTimeinfo = *localtime(&midnightEpoch);
|
||||
@@ -1382,9 +1395,173 @@ bool getCalFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa
|
||||
|
||||
spr2buffer(spr, filename, imageParams);
|
||||
spr.deleteSprite();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CONTENT_DAYAHEAD
|
||||
uint16_t getPercentileColor(const double *prices, int numPrices, double price) {
|
||||
double percentile = 100.0;
|
||||
int colorIndex = 3;
|
||||
const char *colors[] = {"black", "darkgray", "pink", "red"};
|
||||
const int numColors = sizeof(colors) / sizeof(colors[0]);
|
||||
|
||||
const double boundaries[] = {40.0, 80.0, 90.0};
|
||||
const int numBoundaries = sizeof(boundaries) / sizeof(boundaries[0]);
|
||||
|
||||
for (int i = 0; i < numBoundaries; i++) {
|
||||
if (price < prices[int(numPrices * boundaries[i] / 100.0)]) {
|
||||
colorIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return getColor(String(colors[constrain(colorIndex, 0, numColors - 1)]));
|
||||
}
|
||||
|
||||
struct YAxisScale {
|
||||
double min;
|
||||
double max;
|
||||
double step;
|
||||
};
|
||||
|
||||
double roundToNearest(double value, double factor) {
|
||||
return round(value / factor) * factor;
|
||||
}
|
||||
|
||||
int mapDouble(double value, double fromMin, double fromMax, int toMin, int toMax) {
|
||||
return static_cast<int>(round((value - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin));
|
||||
}
|
||||
|
||||
YAxisScale calculateYAxisScale(double priceMin, double priceMax, int divisions) {
|
||||
double range = priceMax - priceMin;
|
||||
double stepSize = range / divisions;
|
||||
double orderOfMagnitude = pow(10, floor(log10(stepSize)));
|
||||
double significantDigit = stepSize / orderOfMagnitude;
|
||||
|
||||
double roundedSignificantDigit;
|
||||
double thresholds[] = {1.0, 2.0, 5.0, 10.0};
|
||||
for (double threshold : thresholds) {
|
||||
if (significantDigit <= threshold) {
|
||||
roundedSignificantDigit = threshold;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double roundedStepSize = roundedSignificantDigit * orderOfMagnitude;
|
||||
double newRange = roundedStepSize * divisions;
|
||||
double minY = floor(priceMin / roundedStepSize) * roundedStepSize;
|
||||
double maxY = minY + newRange;
|
||||
|
||||
return {minY, maxY, roundedStepSize};
|
||||
}
|
||||
|
||||
bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgParam &imageParams) {
|
||||
wsLog("get dayahead prices");
|
||||
|
||||
StaticJsonDocument<512> loc;
|
||||
getTemplate(loc, 27, taginfo->hwType);
|
||||
|
||||
// This is a link to a Google Apps Script script, which fetches (and caches) the tariff from https://transparency.entsoe.eu/
|
||||
// Please don't use this link in any other projects.
|
||||
String URL = "https://script.google.com/macros/s/AKfycbwMmeGAaPrWzVZrESSpmPmD--O132PzW_acnBsuEottKNATTqCRn6h8zN0Yts7S56ggsg/exec?country=" + cfgobj["country"].as<String>();
|
||||
|
||||
time_t now;
|
||||
time(&now);
|
||||
struct tm timeinfo;
|
||||
localtime_r(&now, &timeinfo);
|
||||
|
||||
char dateString[40];
|
||||
strftime(dateString, sizeof(dateString), languageDateFormat[0].c_str(), &timeinfo);
|
||||
|
||||
HTTPClient http;
|
||||
http.begin(URL);
|
||||
http.setTimeout(10000);
|
||||
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
int httpCode = http.GET();
|
||||
if (httpCode != 200) {
|
||||
wsErr("getDayAhead http error " + String(httpCode));
|
||||
return false;
|
||||
}
|
||||
|
||||
DynamicJsonDocument doc(5000);
|
||||
DeserializationError error = deserializeJson(doc, http.getString());
|
||||
if (error) {
|
||||
wsErr(error.c_str());
|
||||
}
|
||||
http.end();
|
||||
|
||||
TFT_eSprite spr = TFT_eSprite(&tft);
|
||||
|
||||
initSprite(spr, imageParams.width, imageParams.height, imageParams);
|
||||
|
||||
int n = doc.size();
|
||||
|
||||
double tarifkwh = cfgobj["tariffkwh"].as<double>();
|
||||
double tariftax = cfgobj["tarifftax"].as<double>();
|
||||
double minPrice = (doc[0]["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100);
|
||||
double maxPrice = minPrice;
|
||||
double prices[n];
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
const JsonObject &obj = doc[i];
|
||||
const double price = (obj["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100);
|
||||
minPrice = min(minPrice, price);
|
||||
maxPrice = max(maxPrice, price);
|
||||
prices[i] = price;
|
||||
}
|
||||
std::sort(prices, prices + n);
|
||||
|
||||
YAxisScale yAxisScale = calculateYAxisScale(minPrice, maxPrice, loc["yaxis"][2].as<int>());
|
||||
minPrice = yAxisScale.min;
|
||||
|
||||
uint16_t yAxisX = loc["yaxis"][1].as<int>();
|
||||
uint16_t barBottom = loc["bars"][3].as<int>();
|
||||
|
||||
for (double i = minPrice; i <= maxPrice; i += yAxisScale.step) {
|
||||
int y = mapDouble(i, minPrice, maxPrice, spr.height() - barBottom, spr.height() - barBottom - loc["bars"][2].as<int>());
|
||||
spr.drawLine(0, y, spr.width(), y, TFT_BLACK);
|
||||
drawString(spr, String(int(i)), yAxisX, y - 8, loc["yaxis"][0], TL_DATUM, TFT_BLACK);
|
||||
}
|
||||
|
||||
uint16_t barwidth = loc["bars"][1].as<int>() / n;
|
||||
uint16_t barheight = loc["bars"][2].as<int>() / (maxPrice - minPrice);
|
||||
|
||||
uint16_t barX = loc["bars"][0].as<int>();
|
||||
double pricenow = std::numeric_limits<double>::quiet_NaN();
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
const JsonObject &obj = doc[i];
|
||||
const time_t item_time = obj["time"];
|
||||
struct tm item_timeinfo;
|
||||
localtime_r(&item_time, &item_timeinfo);
|
||||
|
||||
const double price = (obj["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100);
|
||||
|
||||
uint16_t barcolor = getPercentileColor(prices, n, price);
|
||||
uint16_t thisbarh = mapDouble(price, minPrice, maxPrice, 0, loc["bars"][2].as<int>());
|
||||
spr.fillRect(barX + i * barwidth, spr.height() - barBottom - thisbarh, barwidth - 1, thisbarh, barcolor);
|
||||
if (i % 2 == 0) {
|
||||
drawString(spr, String(item_timeinfo.tm_hour), barX + i * barwidth + barwidth / 3 + 1, spr.height() - barBottom + 3, loc["time"][0], TC_DATUM, TFT_BLACK);
|
||||
}
|
||||
|
||||
if (now - item_time < 3600 && std::isnan(pricenow)) {
|
||||
spr.fillRect(barX + i * barwidth + 3, 5, barwidth - 6, 10, imageParams.highlightColor);
|
||||
spr.fillTriangle(barX + i * barwidth, 15,
|
||||
barX + i * barwidth + barwidth - 1, 15,
|
||||
barX + i * barwidth + barwidth / 2, 15 + barwidth, imageParams.highlightColor);
|
||||
spr.drawLine(barX + i * barwidth + barwidth / 2, 20 + barwidth, barX + i * barwidth + barwidth / 2, spr.height(), getColor("pink"));
|
||||
pricenow = price;
|
||||
}
|
||||
}
|
||||
|
||||
drawString(spr, String(timeinfo.tm_hour) + ":00", barX, 5, loc["head"][0], TL_DATUM, TFT_BLACK, 30);
|
||||
drawString(spr, String(pricenow) + "/kWh", spr.width() - barX, 5, loc["head"][0], TR_DATUM, TFT_BLACK, 30);
|
||||
|
||||
spr2buffer(spr, filename, imageParams);
|
||||
spr.deleteSprite();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CONTENT_QR
|
||||
void drawQR(String &filename, String qrcontent, String title, tagRecord *&taginfo, imgParam &imageParams) {
|
||||
@@ -1488,7 +1665,7 @@ uint8_t drawBuienradar(String &filename, JsonObject &cfgobj, tagRecord *&taginfo
|
||||
}
|
||||
value = value - 70;
|
||||
|
||||
spr.fillRect(i * cols2 + bars0, bars1 - (value * factor), bars2, value * factor, (value > 50 ? TFT_RED : TFT_BLACK));
|
||||
spr.fillRect(i * cols2 + bars0, bars1 - (value * factor), bars2, value * factor, (value > 50 ? imageParams.highlightColor : TFT_BLACK));
|
||||
|
||||
if (minutes % 15 == 0) {
|
||||
drawString(spr, timestring, i * cols2 + cols0, cols1, cols3);
|
||||
@@ -1720,7 +1897,6 @@ void drawJsonStream(Stream &stream, String &filename, tagRecord *&taginfo, imgPa
|
||||
TFT_eSprite spr = TFT_eSprite(&tft);
|
||||
initSprite(spr, imageParams.width, imageParams.height, imageParams);
|
||||
uint8_t screenCurrentOrientation = 0;
|
||||
imageParams.dither = 2;
|
||||
DynamicJsonDocument doc(500);
|
||||
if (stream.find("[")) {
|
||||
do {
|
||||
|
||||
Reference in New Issue
Block a user