18 Commits

Author SHA1 Message Date
1bb358c961 Merge branch 'develop' of https://git.schle.nz/fabian/esmp3 into develop 2022-08-18 13:25:01 +02:00
12a8391cd7 Multiple old changes. 2022-08-18 13:24:52 +02:00
b6dc04920a Merge branch 'develop' of https://git.schle.nz/fabian/esmp3 into develop 2019-12-07 11:25:38 +01:00
aca1736201 Playlist: RSS feeds can happen with MIME type "application/xml". 2019-12-07 11:23:44 +01:00
fdf986a61e Playlist: Fixed missing include. 2019-12-04 06:22:42 +01:00
5c0822b704 Re-Added 'Added an overlay to display when the websocket isn't connected.' Don't know what happened there... 2019-12-04 06:21:38 +01:00
fad4f2c707 Webinterface: index.html and timezones.json are now saved to the Flash as GZIP compressed binary data. Compression happens on-the-fly during pio run. 2019-12-04 06:13:07 +01:00
84530f76fd DataSources: Implemented ID3 tag skipping for HTTPSDataSources. 2019-12-04 06:07:20 +01:00
fa208858d9 Webinterface: Added an overlay to display when the websocket isn't connected. 2019-12-04 05:59:52 +01:00
6d452ecbc0 Added tracing stuff to RSSParser. 2019-12-04 05:58:25 +01:00
23fbddb055 TinyXML is broken, but I couldn't find an alternative nor fix it. So the code now is pretty hack-y to work around the bugs. 2019-12-04 05:57:58 +01:00
fe2a209e44 Debug and Trace modes can now be (de)activated via API commands and are persisted across reboots. 2019-11-30 13:53:50 +01:00
82905a8cdd Fixed M3U parser for last lines ending without a newline. 2019-11-30 13:38:34 +01:00
3751904cb4 Start with speakers off. 2019-11-30 13:37:35 +01:00
bcf7625285 deploy.sh: Fix calculation error. 2019-11-30 00:14:02 +01:00
4a3e79f02e Added empty (for now) update.manifest. 2019-11-30 00:12:02 +01:00
68e1073858 Deploy.sh: Don't show the commands being executed. 2019-11-30 00:09:36 +01:00
f73d45404f Merge branch 'develop' 2019-11-30 00:06:56 +01:00
19 changed files with 668 additions and 68 deletions

View File

@ -170,3 +170,5 @@ PLS files, M3U files or podcast XML feeds are supported). |
| `add_mapping=<ID>=<PATH>` | Adds a mapping between RFID card <ID> and path
<PATH>. See `play` for valid path formats. |
| `update` | Runs an update check. |
| `debug=<0|1>` | Enables / disables debug messages. This value is persisted across reboots. |
| `trace=<0|1>` | Enables / disables tracing messages. This value is also persisted across reboots. |

3
bin/update.manifest Normal file
View File

@ -0,0 +1,3 @@
VERSION=1
IMAGE_PATH=https://files.schle.nz/esmp3/firmware.bin
IMAGE_MD5=00000000000000000000000000000000

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
set -x
#set -e
#set -x
set -e
if ! git diff-index --quiet HEAD ; then
echo "Git isn't clean. Cant deploy."
@ -27,7 +27,7 @@ done <<< "$vers"
read -p "Version to generate: " VERSION
OTA_VERSION=`grep "VERSION=" bin/update.manifest | cut -d"=" -f2`
OTA_VERSION=$(( "$OTA_VERSION" + 1 ))
OTA_VERSION=$(( $OTA_VERSION + 1 ))
sed -i.bak "s/#define OTA_VERSION .*/#define OTA_VERSION $OTA_VERSION/" include/config.h include/config.sample.h
rm include/config.h.bak include/config.sample.h.bak

View File

@ -16,8 +16,9 @@ public:
virtual void seek(size_t position) = 0;
virtual size_t size() = 0;
virtual void close() = 0;
virtual void skip_id3_tag() {};
virtual bool usable() = 0;
virtual int peek(int offset) = 0;
void skip_id3_tag();
};
class SDDataSource : public DataSource {
@ -32,8 +33,8 @@ public:
void seek(size_t position);
size_t size();
void close();
void skip_id3_tag();
bool usable();
int peek(int offset=0);
};
class HTTPSDataSource : public DataSource {
@ -41,6 +42,8 @@ private:
WiFiClient* _stream = NULL;
HTTPClientWrapper* _http = NULL;
uint32_t _position;
String _url;
void _init(String url, uint32_t offset);
public:
HTTPSDataSource(String url, uint32_t offset=0);
~HTTPSDataSource();
@ -51,4 +54,5 @@ public:
size_t size();
void close();
bool usable();
int peek(int offset=0);
};

View File

@ -34,4 +34,5 @@ public:
uint32_t getSize();
String readUntil(String sep);
String readLine();
int peek(int offset=0);
};

View File

@ -1,5 +1,13 @@
#pragma once
#include <Preferences.h>
void wifi_connect();
extern const uint8_t file_index_html_start[] asm("_binary_src_index_html_start");
extern bool debug_enabled;
extern bool trace_enabled;
extern Preferences prefs;
extern const uint8_t file_index_html_start[] asm("_binary_src_webinterface_index_html_gz_start");
extern const size_t file_index_html_size asm("_binary_src_webinterface_index_html_gz_size");
extern const uint8_t file_timezones_json_start[] asm("_binary_src_webinterface_timezones_json_gz_start");
extern const size_t file_timezones_json_size asm("_binary_src_webinterface_timezones_json_gz_size");

View File

@ -2,6 +2,7 @@
#include <Arduino.h>
#include <vector>
#include <ArduinoJson.h>
#include "main.h"
#include "http_client_wrapper.h"
enum PlaylistPersistence {

View File

@ -11,30 +11,27 @@
[platformio]
default_envs = esp32
[extra]
lib_deps =
63 ; MFRC522
https://github.com/me-no-dev/ESPAsyncWebServer.git
ArduinoJSON
6691 ; TinyXML
[env:esp32]
[env]
platform = espressif32
board = esp-wrover-kit
framework = arduino
upload_speed = 512000
build_flags=!./build_version.sh
lib_deps = ${extra.lib_deps}
upload_port = /dev/cu.SLAB_USBtoUART
lib_deps =
63 ; MFRC522
https://github.com/me-no-dev/ESPAsyncWebServer.git
64 ; ArduinoJSON
6691 ; TinyXML
monitor_speed = 115200
board_build.embed_txtfiles = src/index.html
board_build.embed_files =
src/webinterface/timezones.json.gz
src/webinterface/index.html.gz
;board_build.partitions = partitions.csv
;monitor_port = /dev/cu.wchusbserial1420
extra_scripts =
post:tools/post_build.py
[env:esp32]
build_flags=!./build_version.sh
upload_port = /dev/cu.SLAB_USBtoUART
[env:deploy]
platform = espressif32
board = esp-wrover-kit
framework = arduino
lib_deps = ${extra.lib_deps}
board_build.embed_txtfiles = src/index.html
board_build.partitions = partitions.csv

View File

@ -301,6 +301,24 @@ bool Controller::process_message(String cmd) {
} else if (cmd.equals("update")) {
Updater::run();
#endif
} else if (cmd.startsWith("trace=")) {
int val = cmd.substring(6).toInt();
if (val==0) {
trace_enabled = false;
prefs.putBool("trace_enabled", false);
} else if (val==1) {
trace_enabled = true;
prefs.putBool("trace_enabled", true);
}
} else if (cmd.startsWith("debug=")) {
int val = cmd.substring(6).toInt();
if (val==0) {
debug_enabled = false;
prefs.putBool("debug_enabled", false);
} else if (val==1) {
debug_enabled = true;
prefs.putBool("debug_enabled", true);
}
} else {
ERROR("Unknown command: %s\n", cmd.c_str());
return false;

View File

@ -1,5 +1,29 @@
#include "data_sources.h"
void DataSource::skip_id3_tag() {
if (peek(0)=='I' && peek(1)=='D' && peek(2)=='3') {
DEBUG("ID3 tag found\n");
// Skip ID3 tag marker
read(); read(); read();
// Skip ID3 tag version
read(); read();
byte tags = read();
bool footer_present = tags & 0x10;
DEBUG("ID3 footer found: %d\n", footer_present);
uint32_t offset = 0;
for (byte i=0; i<4; i++) {
offset <<= 7;
offset |= (0x7F & read());
}
offset += 10;
if (footer_present) offset += 10;
DEBUG("ID3 tag length is %d bytes.\n", offset);
seek(offset);
} else {
DEBUG("No ID3 tag found\n");
}
}
////////////// SDDataSource //////////////
SDDataSource::SDDataSource(String file) { _file = SD.open(file, "r"); }
SDDataSource::~SDDataSource() { if (_file) _file.close(); }
@ -10,35 +34,24 @@ void SDDataSource::seek(size_t position) { _file.seek(position); }
size_t SDDataSource::size() { return _file.size(); }
void SDDataSource::close() { _file.close(); }
bool SDDataSource::usable() { return _file; }
void SDDataSource::skip_id3_tag() {
uint32_t original_position = _file.position();
uint32_t offset = 0;
if (_file.read()=='I' && _file.read()=='D' && _file.read()=='3') {
DEBUG("ID3 tag found\n");
// Skip ID3 tag version
_file.read(); _file.read();
byte tags = _file.read();
bool footer_present = tags & 0x10;
DEBUG("ID3 footer found: %d\n", footer_present);
for (byte i=0; i<4; i++) {
offset <<= 7;
offset |= (0x7F & _file.read());
int SDDataSource::peek(int offset) {
if (offset==0) return _file.peek();
size_t start_position = _file.position();
_file.seek(start_position + offset);
int result = _file.peek();
_file.seek(start_position);
return result;
}
offset += 10;
if (footer_present) offset += 10;
DEBUG("ID3 tag length is %d bytes.\n", offset);
_file.seek(offset);
} else {
DEBUG("No ID3 tag found\n");
_file.seek(original_position);
}
}
////////////// HTTPSDataSource //////////////
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
_url = url;
_init(url, offset);
}
void HTTPSDataSource::_init(String url, uint32_t offset) {
_url = url;
_http = new HTTPClientWrapper();
if (!_http->get(url, offset)) return;
_position = 0;
@ -52,6 +65,7 @@ bool HTTPSDataSource::usable() { return _http; }
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; }
int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; }
size_t HTTPSDataSource::position() { return _position; }
void HTTPSDataSource::seek(size_t position) { return; /* TODO */ }
void HTTPSDataSource::seek(size_t position) { _http->close(); delete _http; _init(_url, position); }
size_t HTTPSDataSource::size() { return _http->getSize(); }
void HTTPSDataSource::close() { _http->close(); }
int HTTPSDataSource::peek(int offset) { return _http->peek(offset); }

View File

@ -211,3 +211,12 @@ String HTTPClientWrapper::readUntil(String sep) {
String HTTPClientWrapper::readLine() {
return readUntil("\n\r");
}
int HTTPClientWrapper::peek(int offset) {
if (_buffer_position >= _buffer_length) _fill_buffer();
if (_buffer_position + offset < 0 || _buffer_position + offset >= _buffer_length) {
return -1;
}
return _buffer[_buffer_position + offset];
}

View File

@ -12,7 +12,12 @@ HTTPServer::HTTPServer(Player* p, Controller* c) {
ws->onEvent([&](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){this->_onEvent(server, client, type, arg, data, len);});
_server->on("/", HTTP_GET, [&](AsyncWebServerRequest* req) {
req->send(200, "text/html", (const char*)file_index_html_start);
req->send_P(200, "text/html", file_index_html_start, file_index_html_size);
});
_server->on("/timezone.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
AsyncWebServerResponse* res = req->beginResponse_P(200, "application/json", file_timezones_json_start, file_timezones_json_size);
res->addHeader("Content-Encoding", "gzip");
req->send(res);
});
_server->on("/upload", HTTP_POST, [](AsyncWebServerRequest* req) {
req->send(200);

View File

@ -4,6 +4,7 @@
#include <WiFi.h>
#include <WiFiMulti.h>
#include <ESPmDNS.h>
#include <Preferences.h>
#include "main.h"
#include "config.h"
#include "controller.h"
@ -20,6 +21,10 @@ HTTPServer* http_server;
uint8_t SPIMaster::state = 0;
bool debug_enabled = true;
bool trace_enabled = false;
Preferences prefs;
void wifi_connect() {
INFO("Connecting to WiFi...\n");
WiFiMulti wifi;
@ -69,6 +74,14 @@ void setup() {
INFO("ESMP3, version unknown (OTA_VERSION %d)\n", OTA_VERSION);
#endif
INFO("Initializing...\n");
prefs.begin("esmp3");
debug_enabled = prefs.getBool("debug_enabled", true);
trace_enabled = prefs.getBool("trace_enabled", false);
PIN_SPEAKER_L_SETUP();
PIN_SPEAKER_R_SETUP();
PIN_SPEAKER_L(LOW);
PIN_SPEAKER_R(LOW);
DEBUG("Setting up SPI...\n");
SPI.begin();

View File

@ -10,8 +10,6 @@ Player::Player(SPIMaster* s) {
_spi = s;
PIN_VS1053_XRESET_SETUP();
PIN_VS1053_XRESET(HIGH);
PIN_SPEAKER_L_SETUP();
PIN_SPEAKER_R_SETUP();
_speaker_off();
_spi->disable();
PIN_VS1053_DREQ_SETUP();
@ -534,6 +532,7 @@ bool Player::play() {
if (_state != idle) return false;
if (_current_playlist == NULL) return false;
if (_current_playlist->get_file_count()==0) return false;
_speaker_on();
_current_playlist->start();
String file;
if (!_current_playlist->get_current_file(&file)) {

View File

@ -77,12 +77,11 @@ void Playlist::_examine_http_url(String url) {
String ct = http->getContentType();
DEBUG("Content-Type is %s.\n", ct.c_str());
if (ct.startsWith("audio/x-mpegurl")) {
_parse_m3u(http);
} else if (ct.startsWith("audio/")) {
persistence = PERSIST_NONE;
_files.push_back({.filename=url, .title=url, .id="none"});
} else if (ct.startsWith("application/rss+xml")) {
} else if (ct.startsWith("application/rss+xml") || ct.startsWith("application/xml")) {
persistence = PERSIST_PERMANENTLY;
_parse_rss(http);
} else if (ct.startsWith("application/pls+xml")) {
@ -108,37 +107,44 @@ void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t
String tag(tagName);
if (status & STATUS_START_TAG) xml_last_tag = tag;
if (trace_enabled) {
if (status & STATUS_START_TAG) {
TRACE("Start of tag: %s\n", tagName);
} else if (status & STATUS_END_TAG) {
TRACE("End of tag: %s\n", tagName);
}
}
if (tag.equals("/rss/channel/title") && (status & STATUS_TAG_TEXT)) {
xml_album_title = data;
} else if (tag.endsWith("/item") && (status & STATUS_START_TAG)) {
xml_title = "";
xml_url = "";
xml_guid = "";
} else if (tag.endsWith("/item/title") && (status & STATUS_TAG_TEXT)) {
} else if (tag.endsWith("/title") && (status & STATUS_TAG_TEXT)) {
xml_title = String(data);
} else if (tag.endsWith("/item/guid") && (status & STATUS_TAG_TEXT)) {
} else if (tag.endsWith("/guid") && (status & STATUS_TAG_TEXT)) {
xml_guid = data;
//} else if (xml_last_tag.endsWith("/item/enclosure") && (status & STATUS_ATTR_TEXT)) {
// DEBUG("tag: %s, data: %s\n", tag.c_str(), data);
} else if (xml_last_tag.endsWith("/item/enclosure") && tag.equals("type") && (status & STATUS_ATTR_TEXT) && String(data).indexOf("audio/")>=0) {
} else if (xml_last_tag.endsWith("/enclosure") && tag.equals("type") && (status & STATUS_ATTR_TEXT) && String(data).indexOf("audio/")>=0) {
DEBUG("enclosure is audio\n");
xml_enclosure_is_audio = true;
} else if (xml_last_tag.endsWith("/item/enclosure") && tag.equals("url") && (status & STATUS_ATTR_TEXT)) {
} else if (xml_last_tag.endsWith("/enclosure") && tag.equals("url") && (status & STATUS_ATTR_TEXT)) {
DEBUG("found url\n");
xml_enclosure_url = String(data);
} else if (tag.endsWith("/item/enclosure") && (status & STATUS_END_TAG)) {
} else if (tag.endsWith("/enclosure") && (status & STATUS_END_TAG)) {
DEBUG("end of enclosure. xml_enclosure_is_audio: %d, xml_enclosure_url: %s\n", xml_enclosure_is_audio, xml_enclosure_url.c_str());
if (xml_enclosure_is_audio && xml_enclosure_url.length()>0) {
xml_url = xml_enclosure_url;
}
xml_enclosure_is_audio = false;
xml_enclosure_url = "";
} else if (tag.endsWith("/item") && (status & STATUS_END_TAG)) {
} else if (tag.endsWith("/item") && (status & STATUS_END_TAG || status & STATUS_START_TAG)) {
if (xml_title.length()>0 && xml_url.length()>0) {
if (xml_files_ptr->size() > 20) return;
DEBUG("Adding playlist entry: '%s' => '%s'\n", xml_title.c_str(), xml_url.c_str());
xml_files_ptr->insert(xml_files_ptr->begin(), {.filename=xml_url, .title=xml_title, .id=xml_guid});
}
xml_title = "";
xml_url = "";
xml_guid = "";
}
}
@ -178,7 +184,7 @@ void Playlist::_parse_m3u(HTTPClientWrapper* http) {
do {
i = http->read();
char c = i;
if (i>=-1 && c!='\r' && c!='\n') {
if (i>=0 && c!='\r' && c!='\n') {
line += c;
} else {
if (line.equals("#EXTM3U")) {

View File

@ -9,9 +9,24 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="https://kit.fontawesome.com/272149490a.js" crossorigin="anonymous"></script>
<style>
.overlay {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
position: absolute;
z-index: 1;
margin: auto 0px;
vertical-align: middle;
color: white;
font-size: 60px;
text-align: center;
}
</style>
</head>
<body>
<div id="overlay" class="overlay">Not connected...</div>
<div class="container bg-dark text-light">
<div class="row">
<div class="col-sm-1">
@ -104,7 +119,7 @@
<input type="text" class="form-control" id="input_url" />
<div class="input-group-append">
<button class="btn btn-primary" id="button_url_open">Go</button>
<button class="btn btn-danger" id="button_url_add_mapping" style="display: none;"><i class="fa fa-arrows-alt-h"></i></button>
<button class="btn btn-warning" id="button_url_add_mapping" style="display: none;"><i class="fa fa-arrows-alt-h"></i></button>
</div>
</div>
@ -273,10 +288,30 @@ process_ws_message = function(event) {
}
var play_on_click = true;
interval = null;
ws = null;
var start_reconnect_timer = function() {
console.log("start_reconnect_timer() running...");
$('#overlay').show();
interval = setInterval(connect_to_ws, 2500);
}
var connect_to_ws = function() {
if (!ws || ws.readyState >= ws.CLOSING) {
ws = new WebSocket("ws://" + location.host + "/ws");
ws.onopen = function() {
console.log("on_open() running...");
clearInterval(interval);
$('#overlay').hide();
};
ws.onmessage = process_ws_message;
ws.onclose = start_reconnect_timer;
}
}
$(function() {
ws = new WebSocket("ws://" + location.host + "/ws");
ws.onmessage = process_ws_message;
start_reconnect_timer();
$('#volume_slider').change(function(e) { ws.send("volume=" + e.target.value); });
$('#button_play').click(function(e) { ws.send("play"); });

View File

@ -0,0 +1,462 @@
{"timezones": {
"Africa/Abidjan":"GMT0",
"Africa/Accra":"GMT0",
"Africa/Addis_Ababa":"EAT-3",
"Africa/Algiers":"CET-1",
"Africa/Asmara":"EAT-3",
"Africa/Bamako":"GMT0",
"Africa/Bangui":"WAT-1",
"Africa/Banjul":"GMT0",
"Africa/Bissau":"GMT0",
"Africa/Blantyre":"CAT-2",
"Africa/Brazzaville":"WAT-1",
"Africa/Bujumbura":"CAT-2",
"Africa/Cairo":"EET-2",
"Africa/Casablanca":"<+01>-1",
"Africa/Ceuta":"CET-1CEST,M3.5.0,M10.5.0/3",
"Africa/Conakry":"GMT0",
"Africa/Dakar":"GMT0",
"Africa/Dar_es_Salaam":"EAT-3",
"Africa/Djibouti":"EAT-3",
"Africa/Douala":"WAT-1",
"Africa/El_Aaiun":"<+01>-1",
"Africa/Freetown":"GMT0",
"Africa/Gaborone":"CAT-2",
"Africa/Harare":"CAT-2",
"Africa/Johannesburg":"SAST-2",
"Africa/Juba":"EAT-3",
"Africa/Kampala":"EAT-3",
"Africa/Khartoum":"CAT-2",
"Africa/Kigali":"CAT-2",
"Africa/Kinshasa":"WAT-1",
"Africa/Lagos":"WAT-1",
"Africa/Libreville":"WAT-1",
"Africa/Lome":"GMT0",
"Africa/Luanda":"WAT-1",
"Africa/Lubumbashi":"CAT-2",
"Africa/Lusaka":"CAT-2",
"Africa/Malabo":"WAT-1",
"Africa/Maputo":"CAT-2",
"Africa/Maseru":"SAST-2",
"Africa/Mbabane":"SAST-2",
"Africa/Mogadishu":"EAT-3",
"Africa/Monrovia":"GMT0",
"Africa/Nairobi":"EAT-3",
"Africa/Ndjamena":"WAT-1",
"Africa/Niamey":"WAT-1",
"Africa/Nouakchott":"GMT0",
"Africa/Ouagadougou":"GMT0",
"Africa/Porto-Novo":"WAT-1",
"Africa/Sao_Tome":"GMT0",
"Africa/Tripoli":"EET-2",
"Africa/Tunis":"CET-1",
"Africa/Windhoek":"CAT-2",
"America/Adak":"HST10HDT,M3.2.0,M11.1.0",
"America/Anchorage":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Anguilla":"AST4",
"America/Antigua":"AST4",
"America/Araguaina":"<-03>3",
"America/Argentina/Buenos_Aires":"<-03>3",
"America/Argentina/Catamarca":"<-03>3",
"America/Argentina/Cordoba":"<-03>3",
"America/Argentina/Jujuy":"<-03>3",
"America/Argentina/La_Rioja":"<-03>3",
"America/Argentina/Mendoza":"<-03>3",
"America/Argentina/Rio_Gallegos":"<-03>3",
"America/Argentina/Salta":"<-03>3",
"America/Argentina/San_Juan":"<-03>3",
"America/Argentina/San_Luis":"<-03>3",
"America/Argentina/Tucuman":"<-03>3",
"America/Argentina/Ushuaia":"<-03>3",
"America/Aruba":"AST4",
"America/Asuncion":"<-04>4<-03>,M10.1.0/0,M3.4.0/0",
"America/Atikokan":"EST5",
"America/Bahia":"<-03>3",
"America/Bahia_Banderas":"CST6CDT,M4.1.0,M10.5.0",
"America/Barbados":"AST4",
"America/Belem":"<-03>3",
"America/Belize":"CST6",
"America/Blanc-Sablon":"AST4",
"America/Boa_Vista":"<-04>4",
"America/Bogota":"<-05>5",
"America/Boise":"MST7MDT,M3.2.0,M11.1.0",
"America/Cambridge_Bay":"MST7MDT,M3.2.0,M11.1.0",
"America/Campo_Grande":"<-04>4",
"America/Cancun":"EST5",
"America/Caracas":"<-04>4",
"America/Cayenne":"<-03>3",
"America/Cayman":"EST5",
"America/Chicago":"CST6CDT,M3.2.0,M11.1.0",
"America/Chihuahua":"MST7MDT,M4.1.0,M10.5.0",
"America/Costa_Rica":"CST6",
"America/Creston":"MST7",
"America/Cuiaba":"<-04>4",
"America/Curacao":"AST4",
"America/Danmarkshavn":"GMT0",
"America/Dawson":"PST8PDT,M3.2.0,M11.1.0",
"America/Dawson_Creek":"MST7",
"America/Denver":"MST7MDT,M3.2.0,M11.1.0",
"America/Detroit":"EST5EDT,M3.2.0,M11.1.0",
"America/Dominica":"AST4",
"America/Edmonton":"MST7MDT,M3.2.0,M11.1.0",
"America/Eirunepe":"<-05>5",
"America/El_Salvador":"CST6",
"America/Fortaleza":"<-03>3",
"America/Fort_Nelson":"MST7",
"America/Glace_Bay":"AST4ADT,M3.2.0,M11.1.0",
"America/Godthab":"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1",
"America/Goose_Bay":"AST4ADT,M3.2.0,M11.1.0",
"America/Grand_Turk":"EST5EDT,M3.2.0,M11.1.0",
"America/Grenada":"AST4",
"America/Guadeloupe":"AST4",
"America/Guatemala":"CST6",
"America/Guayaquil":"<-05>5",
"America/Guyana":"<-04>4",
"America/Halifax":"AST4ADT,M3.2.0,M11.1.0",
"America/Havana":"CST5CDT,M3.2.0/0,M11.1.0/1",
"America/Hermosillo":"MST7",
"America/Indiana/Indianapolis":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Knox":"CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Marengo":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Petersburg":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Tell_City":"CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Vevay":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Vincennes":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Winamac":"EST5EDT,M3.2.0,M11.1.0",
"America/Inuvik":"MST7MDT,M3.2.0,M11.1.0",
"America/Iqaluit":"EST5EDT,M3.2.0,M11.1.0",
"America/Jamaica":"EST5",
"America/Juneau":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Kentucky/Louisville":"EST5EDT,M3.2.0,M11.1.0",
"America/Kentucky/Monticello":"EST5EDT,M3.2.0,M11.1.0",
"America/Kralendijk":"AST4",
"America/La_Paz":"<-04>4",
"America/Lima":"<-05>5",
"America/Los_Angeles":"PST8PDT,M3.2.0,M11.1.0",
"America/Lower_Princes":"AST4",
"America/Maceio":"<-03>3",
"America/Managua":"CST6",
"America/Manaus":"<-04>4",
"America/Marigot":"AST4",
"America/Martinique":"AST4",
"America/Matamoros":"CST6CDT,M3.2.0,M11.1.0",
"America/Mazatlan":"MST7MDT,M4.1.0,M10.5.0",
"America/Menominee":"CST6CDT,M3.2.0,M11.1.0",
"America/Merida":"CST6CDT,M4.1.0,M10.5.0",
"America/Metlakatla":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Mexico_City":"CST6CDT,M4.1.0,M10.5.0",
"America/Miquelon":"<-03>3<-02>,M3.2.0,M11.1.0",
"America/Moncton":"AST4ADT,M3.2.0,M11.1.0",
"America/Monterrey":"CST6CDT,M4.1.0,M10.5.0",
"America/Montevideo":"<-03>3",
"America/Montreal":"EST5EDT,M3.2.0,M11.1.0",
"America/Montserrat":"AST4",
"America/Nassau":"EST5EDT,M3.2.0,M11.1.0",
"America/New_York":"EST5EDT,M3.2.0,M11.1.0",
"America/Nipigon":"EST5EDT,M3.2.0,M11.1.0",
"America/Nome":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Noronha":"<-02>2",
"America/North_Dakota/Beulah":"CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/Center":"CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/New_Salem":"CST6CDT,M3.2.0,M11.1.0",
"America/Ojinaga":"MST7MDT,M3.2.0,M11.1.0",
"America/Panama":"EST5",
"America/Pangnirtung":"EST5EDT,M3.2.0,M11.1.0",
"America/Paramaribo":"<-03>3",
"America/Phoenix":"MST7",
"America/Port-au-Prince":"EST5EDT,M3.2.0,M11.1.0",
"America/Port_of_Spain":"AST4",
"America/Porto_Velho":"<-04>4",
"America/Puerto_Rico":"AST4",
"America/Punta_Arenas":"<-03>3",
"America/Rainy_River":"CST6CDT,M3.2.0,M11.1.0",
"America/Rankin_Inlet":"CST6CDT,M3.2.0,M11.1.0",
"America/Recife":"<-03>3",
"America/Regina":"CST6",
"America/Resolute":"CST6CDT,M3.2.0,M11.1.0",
"America/Rio_Branco":"<-05>5",
"America/Santarem":"<-03>3",
"America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24",
"America/Santo_Domingo":"AST4",
"America/Sao_Paulo":"<-03>3",
"America/Scoresbysund":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
"America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0",
"America/St_Barthelemy":"AST4",
"America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0",
"America/St_Kitts":"AST4",
"America/St_Lucia":"AST4",
"America/St_Thomas":"AST4",
"America/St_Vincent":"AST4",
"America/Swift_Current":"CST6",
"America/Tegucigalpa":"CST6",
"America/Thule":"AST4ADT,M3.2.0,M11.1.0",
"America/Thunder_Bay":"EST5EDT,M3.2.0,M11.1.0",
"America/Tijuana":"PST8PDT,M3.2.0,M11.1.0",
"America/Toronto":"EST5EDT,M3.2.0,M11.1.0",
"America/Tortola":"AST4",
"America/Vancouver":"PST8PDT,M3.2.0,M11.1.0",
"America/Whitehorse":"PST8PDT,M3.2.0,M11.1.0",
"America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0",
"America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0",
"Antarctica/Casey":"<+08>-8",
"Antarctica/Davis":"<+07>-7",
"Antarctica/DumontDUrville":"<+10>-10",
"Antarctica/Macquarie":"<+11>-11",
"Antarctica/Mawson":"<+05>-5",
"Antarctica/McMurdo":"NZST-12NZDT,M9.5.0,M4.1.0/3",
"Antarctica/Palmer":"<-03>3",
"Antarctica/Rothera":"<-03>3",
"Antarctica/Syowa":"<+03>-3",
"Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
"Antarctica/Vostok":"<+06>-6",
"Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3",
"Asia/Aden":"<+03>-3",
"Asia/Almaty":"<+06>-6",
"Asia/Amman":"EET-2EEST,M3.5.4/24,M10.5.5/1",
"Asia/Anadyr":"<+12>-12",
"Asia/Aqtau":"<+05>-5",
"Asia/Aqtobe":"<+05>-5",
"Asia/Ashgabat":"<+05>-5",
"Asia/Atyrau":"<+05>-5",
"Asia/Baghdad":"<+03>-3",
"Asia/Bahrain":"<+03>-3",
"Asia/Baku":"<+04>-4",
"Asia/Bangkok":"<+07>-7",
"Asia/Barnaul":"<+07>-7",
"Asia/Beirut":"EET-2EEST,M3.5.0/0,M10.5.0/0",
"Asia/Bishkek":"<+06>-6",
"Asia/Brunei":"<+08>-8",
"Asia/Chita":"<+09>-9",
"Asia/Choibalsan":"<+08>-8",
"Asia/Colombo":"<+0530>-5:30",
"Asia/Damascus":"EET-2EEST,M3.5.5/0,M10.5.5/0",
"Asia/Dhaka":"<+06>-6",
"Asia/Dili":"<+09>-9",
"Asia/Dubai":"<+04>-4",
"Asia/Dushanbe":"<+05>-5",
"Asia/Famagusta":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Gaza":"EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Hebron":"EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Ho_Chi_Minh":"<+07>-7",
"Asia/Hong_Kong":"HKT-8",
"Asia/Hovd":"<+07>-7",
"Asia/Irkutsk":"<+08>-8",
"Asia/Jakarta":"WIB-7",
"Asia/Jayapura":"WIT-9",
"Asia/Jerusalem":"IST-2IDT,M3.4.4/26,M10.5.0",
"Asia/Kabul":"<+0430>-4:30",
"Asia/Kamchatka":"<+12>-12",
"Asia/Karachi":"PKT-5",
"Asia/Kathmandu":"<+0545>-5:45",
"Asia/Khandyga":"<+09>-9",
"Asia/Kolkata":"IST-5:30",
"Asia/Krasnoyarsk":"<+07>-7",
"Asia/Kuala_Lumpur":"<+08>-8",
"Asia/Kuching":"<+08>-8",
"Asia/Kuwait":"<+03>-3",
"Asia/Macau":"CST-8",
"Asia/Magadan":"<+11>-11",
"Asia/Makassar":"WITA-8",
"Asia/Manila":"PST-8",
"Asia/Muscat":"<+04>-4",
"Asia/Nicosia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Novokuznetsk":"<+07>-7",
"Asia/Novosibirsk":"<+07>-7",
"Asia/Omsk":"<+06>-6",
"Asia/Oral":"<+05>-5",
"Asia/Phnom_Penh":"<+07>-7",
"Asia/Pontianak":"WIB-7",
"Asia/Pyongyang":"KST-9",
"Asia/Qatar":"<+03>-3",
"Asia/Qyzylorda":"<+05>-5",
"Asia/Riyadh":"<+03>-3",
"Asia/Sakhalin":"<+11>-11",
"Asia/Samarkand":"<+05>-5",
"Asia/Seoul":"KST-9",
"Asia/Shanghai":"CST-8",
"Asia/Singapore":"<+08>-8",
"Asia/Srednekolymsk":"<+11>-11",
"Asia/Taipei":"CST-8",
"Asia/Tashkent":"<+05>-5",
"Asia/Tbilisi":"<+04>-4",
"Asia/Tehran":"<+0330>-3:30<+0430>,J79/24,J263/24",
"Asia/Thimphu":"<+06>-6",
"Asia/Tokyo":"JST-9",
"Asia/Tomsk":"<+07>-7",
"Asia/Ulaanbaatar":"<+08>-8",
"Asia/Urumqi":"<+06>-6",
"Asia/Ust-Nera":"<+10>-10",
"Asia/Vientiane":"<+07>-7",
"Asia/Vladivostok":"<+10>-10",
"Asia/Yakutsk":"<+09>-9",
"Asia/Yangon":"<+0630>-6:30",
"Asia/Yekaterinburg":"<+05>-5",
"Asia/Yerevan":"<+04>-4",
"Atlantic/Azores":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
"Atlantic/Bermuda":"AST4ADT,M3.2.0,M11.1.0",
"Atlantic/Canary":"WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Cape_Verde":"<-01>1",
"Atlantic/Faroe":"WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Madeira":"WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Reykjavik":"GMT0",
"Atlantic/South_Georgia":"<-02>2",
"Atlantic/Stanley":"<-03>3",
"Atlantic/St_Helena":"GMT0",
"Australia/Adelaide":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Brisbane":"AEST-10",
"Australia/Broken_Hill":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Currie":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Darwin":"ACST-9:30",
"Australia/Eucla":"<+0845>-8:45",
"Australia/Hobart":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Lindeman":"AEST-10",
"Australia/Lord_Howe":"<+1030>-10:30<+11>-11,M10.1.0,M4.1.0",
"Australia/Melbourne":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Perth":"AWST-8",
"Australia/Sydney":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Europe/Amsterdam":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Andorra":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Astrakhan":"<+04>-4",
"Europe/Athens":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Belgrade":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Berlin":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bratislava":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Brussels":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bucharest":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Budapest":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Busingen":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Chisinau":"EET-2EEST,M3.5.0,M10.5.0/3",
"Europe/Copenhagen":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Dublin":"IST-1GMT0,M10.5.0,M3.5.0/1",
"Europe/Gibraltar":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Guernsey":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Helsinki":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Isle_of_Man":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Istanbul":"<+03>-3",
"Europe/Jersey":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Kaliningrad":"EET-2",
"Europe/Kiev":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Kirov":"<+03>-3",
"Europe/Lisbon":"WET0WEST,M3.5.0/1,M10.5.0",
"Europe/Ljubljana":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/London":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Luxembourg":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Madrid":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Malta":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Mariehamn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Minsk":"<+03>-3",
"Europe/Monaco":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Moscow":"MSK-3",
"Europe/Oslo":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Paris":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Podgorica":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Prague":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Riga":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Rome":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Samara":"<+04>-4",
"Europe/San_Marino":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sarajevo":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Saratov":"<+04>-4",
"Europe/Simferopol":"MSK-3",
"Europe/Skopje":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sofia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Stockholm":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Tallinn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Tirane":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Ulyanovsk":"<+04>-4",
"Europe/Uzhgorod":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Vaduz":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vatican":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vienna":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vilnius":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Volgograd":"<+04>-4",
"Europe/Warsaw":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zagreb":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zaporozhye":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Zurich":"CET-1CEST,M3.5.0,M10.5.0/3",
"Indian/Antananarivo":"EAT-3",
"Indian/Chagos":"<+06>-6",
"Indian/Christmas":"<+07>-7",
"Indian/Cocos":"<+0630>-6:30",
"Indian/Comoro":"EAT-3",
"Indian/Kerguelen":"<+05>-5",
"Indian/Mahe":"<+04>-4",
"Indian/Maldives":"<+05>-5",
"Indian/Mauritius":"<+04>-4",
"Indian/Mayotte":"EAT-3",
"Indian/Reunion":"<+04>-4",
"Pacific/Apia":"<+13>-13<+14>,M9.5.0/3,M4.1.0/4",
"Pacific/Auckland":"NZST-12NZDT,M9.5.0,M4.1.0/3",
"Pacific/Bougainville":"<+11>-11",
"Pacific/Chatham":"<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45",
"Pacific/Chuuk":"<+10>-10",
"Pacific/Easter":"<-06>6<-05>,M9.1.6/22,M4.1.6/22",
"Pacific/Efate":"<+11>-11",
"Pacific/Enderbury":"<+13>-13",
"Pacific/Fakaofo":"<+13>-13",
"Pacific/Fiji":"<+12>-12<+13>,M11.2.0,M1.2.3/99",
"Pacific/Funafuti":"<+12>-12",
"Pacific/Galapagos":"<-06>6",
"Pacific/Gambier":"<-09>9",
"Pacific/Guadalcanal":"<+11>-11",
"Pacific/Guam":"ChST-10",
"Pacific/Honolulu":"HST10",
"Pacific/Kiritimati":"<+14>-14",
"Pacific/Kosrae":"<+11>-11",
"Pacific/Kwajalein":"<+12>-12",
"Pacific/Majuro":"<+12>-12",
"Pacific/Marquesas":"<-0930>9:30",
"Pacific/Midway":"SST11",
"Pacific/Nauru":"<+12>-12",
"Pacific/Niue":"<-11>11",
"Pacific/Norfolk":"<+11>-11<+12>,M10.1.0,M4.1.0/3",
"Pacific/Noumea":"<+11>-11",
"Pacific/Pago_Pago":"SST11",
"Pacific/Palau":"<+09>-9",
"Pacific/Pitcairn":"<-08>8",
"Pacific/Pohnpei":"<+11>-11",
"Pacific/Port_Moresby":"<+10>-10",
"Pacific/Rarotonga":"<-10>10",
"Pacific/Saipan":"ChST-10",
"Pacific/Tahiti":"<-10>10",
"Pacific/Tarawa":"<+12>-12",
"Pacific/Tongatapu":"<+13>-13",
"Pacific/Wake":"<+12>-12",
"Pacific/Wallis":"<+12>-12",
"Etc/GMT":"GMT0",
"Etc/GMT-0":"GMT0",
"Etc/GMT-1":"<+01>-1",
"Etc/GMT-2":"<+02>-2",
"Etc/GMT-3":"<+03>-3",
"Etc/GMT-4":"<+04>-4",
"Etc/GMT-5":"<+05>-5",
"Etc/GMT-6":"<+06>-6",
"Etc/GMT-7":"<+07>-7",
"Etc/GMT-8":"<+08>-8",
"Etc/GMT-9":"<+09>-9",
"Etc/GMT-10":"<+10>-10",
"Etc/GMT-11":"<+11>-11",
"Etc/GMT-12":"<+12>-12",
"Etc/GMT-13":"<+13>-13",
"Etc/GMT-14":"<+14>-14",
"Etc/GMT0":"GMT0",
"Etc/GMT+0":"GMT0",
"Etc/GMT+1":"<-01>1",
"Etc/GMT+2":"<-02>2",
"Etc/GMT+3":"<-03>3",
"Etc/GMT+4":"<-04>4",
"Etc/GMT+5":"<-05>5",
"Etc/GMT+6":"<-06>6",
"Etc/GMT+7":"<-07>7",
"Etc/GMT+8":"<-08>8",
"Etc/GMT+9":"<-09>9",
"Etc/GMT+10":"<-10>10",
"Etc/GMT+11":"<-11>11",
"Etc/GMT+12":"<-12>12",
"Etc/UCT":"UTC0",
"Etc/UTC":"UTC0",
"Etc/Greenwich":"GMT0",
"Etc/Universal":"UTC0",
"Etc/Zulu":"UTC0"
}}

14
tools/create_tz_json.sh Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/bash
URL="https://raw.githubusercontent.com/nayarsystems/posix_tz_db/master/zones.csv"
(
first=1
echo "{\"timezones\": {"
curl --silent "$URL" | while read line; do
[ $first -ne 1 ] && echo ","
first=0
echo -n "${line/\",\"/\":\"}"
done
echo
echo "}}"
) > src/webinterface/timezones.json

9
tools/post_build.py Normal file
View File

@ -0,0 +1,9 @@
Import("env")
env.Execute("gzip -9 < src/webinterface/index.html > src/webinterface/index.html.gz")
env.Execute("gzip -9 < src/webinterface/timezones.json > src/webinterface/timezones.json.gz")
def build(source, target, env):
env.Execute("rm src/webinterface/index.html.gz src/webinterface/timezones.json.gz")
env.AddPostAction("buildprog", build)