move local website to homekit's tree
This commit is contained in:
parent
902a3bfbe2
commit
6f53e6e997
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,3 +5,7 @@ __pycache__
|
||||
.DS_Store
|
||||
/src/test/test_inverter_monitor.log
|
||||
/esp32-cam/CameraWebServer/wifi_password.h
|
||||
|
||||
/localwebsite/vendor
|
||||
/localwebsite/.debug.log
|
||||
/localwebsite/config.local.php
|
||||
|
292
localwebsite/classes/E3372.php
Normal file
292
localwebsite/classes/E3372.php
Normal file
@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
class E3372
|
||||
{
|
||||
|
||||
const WIFI_CONNECTING = '900';
|
||||
const WIFI_CONNECTED = '901';
|
||||
const WIFI_DISCONNECTED = '902';
|
||||
const WIFI_DISCONNECTING = '903';
|
||||
|
||||
const CRADLE_CONNECTING = '900';
|
||||
const CRADLE_CONNECTED = '901';
|
||||
const CRADLE_DISCONNECTED = '902';
|
||||
const CRADLE_DISCONNECTING = '903';
|
||||
const CRADLE_CONNECTFAILED = '904';
|
||||
const CRADLE_CONNECTSTATUSNULL = '905';
|
||||
const CRANDLE_CONNECTSTATUSERRO = '906';
|
||||
|
||||
const MACRO_EVDO_LEVEL_ZERO = '0';
|
||||
const MACRO_EVDO_LEVEL_ONE = '1';
|
||||
const MACRO_EVDO_LEVEL_TWO = '2';
|
||||
const MACRO_EVDO_LEVEL_THREE = '3';
|
||||
const MACRO_EVDO_LEVEL_FOUR = '4';
|
||||
const MACRO_EVDO_LEVEL_FIVE = '5';
|
||||
|
||||
// CurrentNetworkType
|
||||
const MACRO_NET_WORK_TYPE_NOSERVICE = 0;
|
||||
const MACRO_NET_WORK_TYPE_GSM = 1;
|
||||
const MACRO_NET_WORK_TYPE_GPRS = 2;
|
||||
const MACRO_NET_WORK_TYPE_EDGE = 3;
|
||||
const MACRO_NET_WORK_TYPE_WCDMA = 4;
|
||||
const MACRO_NET_WORK_TYPE_HSDPA = 5;
|
||||
const MACRO_NET_WORK_TYPE_HSUPA = 6;
|
||||
const MACRO_NET_WORK_TYPE_HSPA = 7;
|
||||
const MACRO_NET_WORK_TYPE_TDSCDMA = 8;
|
||||
const MACRO_NET_WORK_TYPE_HSPA_PLUS = 9;
|
||||
const MACRO_NET_WORK_TYPE_EVDO_REV_0 = 10;
|
||||
const MACRO_NET_WORK_TYPE_EVDO_REV_A = 11;
|
||||
const MACRO_NET_WORK_TYPE_EVDO_REV_B = 12;
|
||||
const MACRO_NET_WORK_TYPE_1xRTT = 13;
|
||||
const MACRO_NET_WORK_TYPE_UMB = 14;
|
||||
const MACRO_NET_WORK_TYPE_1xEVDV = 15;
|
||||
const MACRO_NET_WORK_TYPE_3xRTT = 16;
|
||||
const MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM = 17;
|
||||
const MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO = 18;
|
||||
const MACRO_NET_WORK_TYPE_LTE = 19;
|
||||
const MACRO_NET_WORK_TYPE_EX_NOSERVICE = 0;
|
||||
const MACRO_NET_WORK_TYPE_EX_GSM = 1;
|
||||
const MACRO_NET_WORK_TYPE_EX_GPRS = 2;
|
||||
const MACRO_NET_WORK_TYPE_EX_EDGE = 3;
|
||||
const MACRO_NET_WORK_TYPE_EX_IS95A = 21;
|
||||
const MACRO_NET_WORK_TYPE_EX_IS95B = 22;
|
||||
const MACRO_NET_WORK_TYPE_EX_CDMA_1x = 23;
|
||||
const MACRO_NET_WORK_TYPE_EX_EVDO_REV_0 = 24;
|
||||
const MACRO_NET_WORK_TYPE_EX_EVDO_REV_A = 25;
|
||||
const MACRO_NET_WORK_TYPE_EX_EVDO_REV_B = 26;
|
||||
const MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x = 27;
|
||||
const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0 = 28;
|
||||
const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A = 29;
|
||||
const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B = 30;
|
||||
const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0 = 31;
|
||||
const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A = 32;
|
||||
const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B = 33;
|
||||
const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0 = 34;
|
||||
const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A = 35;
|
||||
const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B = 36;
|
||||
const MACRO_NET_WORK_TYPE_EX_WCDMA = 41;
|
||||
const MACRO_NET_WORK_TYPE_EX_HSDPA = 42;
|
||||
const MACRO_NET_WORK_TYPE_EX_HSUPA = 43;
|
||||
const MACRO_NET_WORK_TYPE_EX_HSPA = 44;
|
||||
const MACRO_NET_WORK_TYPE_EX_HSPA_PLUS = 45;
|
||||
const MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS = 46;
|
||||
const MACRO_NET_WORK_TYPE_EX_TD_SCDMA = 61;
|
||||
const MACRO_NET_WORK_TYPE_EX_TD_HSDPA = 62;
|
||||
const MACRO_NET_WORK_TYPE_EX_TD_HSUPA = 63;
|
||||
const MACRO_NET_WORK_TYPE_EX_TD_HSPA = 64;
|
||||
const MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS = 65;
|
||||
const MACRO_NET_WORK_TYPE_EX_802_16E = 81;
|
||||
const MACRO_NET_WORK_TYPE_EX_LTE = 101;
|
||||
|
||||
|
||||
const ERROR_SYSTEM_NO_SUPPORT = 100002;
|
||||
const ERROR_SYSTEM_NO_RIGHTS = 100003;
|
||||
const ERROR_SYSTEM_BUSY = 100004;
|
||||
const ERROR_LOGIN_USERNAME_WRONG = 108001;
|
||||
const ERROR_LOGIN_PASSWORD_WRONG = 108002;
|
||||
const ERROR_LOGIN_ALREADY_LOGIN = 108003;
|
||||
const ERROR_LOGIN_USERNAME_PWD_WRONG = 108006;
|
||||
const ERROR_LOGIN_USERNAME_PWD_ORERRUN = 108007;
|
||||
const ERROR_LOGIN_TOUCH_ALREADY_LOGIN = 108009;
|
||||
const ERROR_VOICE_BUSY = 120001;
|
||||
const ERROR_WRONG_TOKEN = 125001;
|
||||
const ERROR_WRONG_SESSION = 125002;
|
||||
const ERROR_WRONG_SESSION_TOKEN = 125003;
|
||||
|
||||
|
||||
private $host;
|
||||
private $headers = [];
|
||||
private $authorized = false;
|
||||
private $useLegacyTokenAuth = false;
|
||||
|
||||
public function __construct(string $host, bool $legacy_token_auth = false) {
|
||||
$this->host = $host;
|
||||
$this->useLegacyTokenAuth = $legacy_token_auth;
|
||||
}
|
||||
|
||||
public function __call(string $name, array $arguments) {
|
||||
if (startsWith($name, 'get'))
|
||||
$this->auth();
|
||||
|
||||
return call_user_func_array([$this, $name], $arguments);
|
||||
}
|
||||
|
||||
public function auth() {
|
||||
if ($this->authorized)
|
||||
return;
|
||||
|
||||
if (!$this->useLegacyTokenAuth) {
|
||||
$data = $this->request('webserver/SesTokInfo');
|
||||
$this->headers = [
|
||||
'Cookie: '.$data['SesInfo'],
|
||||
'__RequestVerificationToken: '.$data['TokInfo'],
|
||||
'Content-Type: text/xml'
|
||||
];
|
||||
} else {
|
||||
$data = $this->request('webserver/token');
|
||||
$this->headers = [
|
||||
'__RequestVerificationToken: '.$data['token'],
|
||||
'Content-Type: text/xml'
|
||||
];
|
||||
}
|
||||
$this->authorized = true;
|
||||
}
|
||||
|
||||
|
||||
// get* methods have to be protected for __call magic to work
|
||||
|
||||
protected function getDeviceInformation() {
|
||||
return $this->request('device/information');
|
||||
}
|
||||
|
||||
protected function getDeviceSignal() {
|
||||
return $this->request('device/signal');
|
||||
}
|
||||
|
||||
protected function getMonitoringStatus() {
|
||||
return $this->request('monitoring/status');
|
||||
}
|
||||
|
||||
protected function getNotifications() {
|
||||
return $this->request('monitoring/check-notifications');
|
||||
}
|
||||
|
||||
protected function getDialupConnection() {
|
||||
return $this->request('dialup/connection');
|
||||
}
|
||||
|
||||
protected function getTrafficStats() {
|
||||
return $this->request('monitoring/traffic-statistics');
|
||||
}
|
||||
|
||||
protected function getSMSCount() {
|
||||
return $this->request('sms/sms-count');
|
||||
}
|
||||
|
||||
protected function getSMSList($page = 1, $count = 20) {
|
||||
$xml = $this->request('sms/sms-list', 'POST', [
|
||||
'PageIndex' => $page,
|
||||
'ReadCount' => $count,
|
||||
'BoxType' => 1,
|
||||
'SortType' => 0,
|
||||
'Ascending' => 0,
|
||||
'UnreadPreferred' => 1
|
||||
], true);
|
||||
$xml = simplexml_load_string($xml);
|
||||
|
||||
$messages = [];
|
||||
foreach ($xml->Messages->Message as $message) {
|
||||
$messages[] = [
|
||||
'date' => (string)$message->Date,
|
||||
'phone' => (string)$message->Phone,
|
||||
'content' => (string)$message->Content
|
||||
];
|
||||
}
|
||||
return $messages;
|
||||
}
|
||||
|
||||
private function xmlToAssoc(string $xml): array {
|
||||
$xml = new SimpleXMLElement($xml);
|
||||
$data = [];
|
||||
foreach ($xml as $name => $value) {
|
||||
$data[$name] = (string)$value;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function request(string $method, string $http_method = 'GET', array $data = [], bool $return_body = false) {
|
||||
$ch = curl_init();
|
||||
$url = 'http://'.$this->host.'/api/'.$method;
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
if (!empty($this->headers))
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
|
||||
if ($http_method == 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
if (!empty($data))
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->postDataToXML($data));
|
||||
}
|
||||
$body = curl_exec($ch);
|
||||
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($code != 200)
|
||||
throw new Exception('e3372 host returned code '.$code);
|
||||
|
||||
curl_close($ch);
|
||||
return $return_body ? $body : $this->xmlToAssoc($body);
|
||||
}
|
||||
|
||||
private function postDataToXML(array $data, int $depth = 1) {
|
||||
if ($depth == 1)
|
||||
return '<?xml version: "1.0" encoding="UTF-8"?>'.$this->postDataToXML(['request' => $data], $depth+1);
|
||||
|
||||
$items = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value))
|
||||
$value = $this->postDataToXML($value, $depth+1);
|
||||
$items[] = "<{$key}>{$value}</{$key}>";
|
||||
}
|
||||
|
||||
return implode('', $items);
|
||||
}
|
||||
|
||||
public static function getNetworkTypeLabel($type): string {
|
||||
switch ((int)$type) {
|
||||
case self::MACRO_NET_WORK_TYPE_NOSERVICE: return 'NOSERVICE';
|
||||
case self::MACRO_NET_WORK_TYPE_GSM: return 'GSM';
|
||||
case self::MACRO_NET_WORK_TYPE_GPRS: return 'GPRS';
|
||||
case self::MACRO_NET_WORK_TYPE_EDGE: return 'EDGE';
|
||||
case self::MACRO_NET_WORK_TYPE_WCDMA: return 'WCDMA';
|
||||
case self::MACRO_NET_WORK_TYPE_HSDPA: return 'HSDPA';
|
||||
case self::MACRO_NET_WORK_TYPE_HSUPA: return 'HSUPA';
|
||||
case self::MACRO_NET_WORK_TYPE_HSPA: return 'HSPA';
|
||||
case self::MACRO_NET_WORK_TYPE_TDSCDMA: return 'TDSCDMA';
|
||||
case self::MACRO_NET_WORK_TYPE_HSPA_PLUS: return 'HSPA_PLUS';
|
||||
case self::MACRO_NET_WORK_TYPE_EVDO_REV_0: return 'EVDO_REV_0';
|
||||
case self::MACRO_NET_WORK_TYPE_EVDO_REV_A: return 'EVDO_REV_A';
|
||||
case self::MACRO_NET_WORK_TYPE_EVDO_REV_B: return 'EVDO_REV_B';
|
||||
case self::MACRO_NET_WORK_TYPE_1xRTT: return '1xRTT';
|
||||
case self::MACRO_NET_WORK_TYPE_UMB: return 'UMB';
|
||||
case self::MACRO_NET_WORK_TYPE_1xEVDV: return '1xEVDV';
|
||||
case self::MACRO_NET_WORK_TYPE_3xRTT: return '3xRTT';
|
||||
case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM: return 'HSPA_PLUS_64QAM';
|
||||
case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO: return 'HSPA_PLUS_MIMO';
|
||||
case self::MACRO_NET_WORK_TYPE_LTE: return 'LTE';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_NOSERVICE: return 'NOSERVICE';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_GSM: return 'GSM';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_GPRS: return 'GPRS';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_EDGE: return 'EDGE';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_IS95A: return 'IS95A';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_IS95B: return 'IS95B';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_CDMA_1x: return 'CDMA_1x';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_0: return 'EVDO_REV_0';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_A: return 'EVDO_REV_A';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_B: return 'EVDO_REV_B';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x: return 'HYBRID_CDMA_1x';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0: return 'HYBRID_EVDO_REV_0';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A: return 'HYBRID_EVDO_REV_A';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B: return 'HYBRID_EVDO_REV_B';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0: return 'EHRPD_REL_0';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A: return 'EHRPD_REL_A';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B: return 'EHRPD_REL_B';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0: return 'HYBRID_EHRPD_REL_0';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A: return 'HYBRID_EHRPD_REL_A';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B: return 'HYBRID_EHRPD_REL_B';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_WCDMA: return 'WCDMA';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HSDPA: return 'HSDPA';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HSUPA: return 'HSUPA';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HSPA: return 'HSPA';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_HSPA_PLUS: return 'HSPA_PLUS';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS: return 'DC_HSPA_PLUS';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_TD_SCDMA: return 'TD_SCDMA';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_TD_HSDPA: return 'TD_HSDPA';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_TD_HSUPA: return 'TD_HSUPA';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA: return 'TD_HSPA';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS: return 'TD_HSPA_PLUS';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_802_16E: return '802_16E';
|
||||
case self::MACRO_NET_WORK_TYPE_EX_LTE: return 'LTE';
|
||||
default: return '?';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
18
localwebsite/classes/GPIORelaydClient.php
Normal file
18
localwebsite/classes/GPIORelaydClient.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
class GPIORelaydClient extends MySimpleSocketClient {
|
||||
|
||||
const STATUS_ON = 'on';
|
||||
const STATUS_OFF = 'off';
|
||||
|
||||
public function setStatus(string $status) {
|
||||
$this->send($status);
|
||||
return $this->recv();
|
||||
}
|
||||
|
||||
public function getStatus() {
|
||||
$this->send('get');
|
||||
return $this->recv();
|
||||
}
|
||||
|
||||
}
|
69
localwebsite/classes/InverterdClient.php
Normal file
69
localwebsite/classes/InverterdClient.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
class InverterdClient extends MySimpleSocketClient {
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setProtocol(int $v): string
|
||||
{
|
||||
$this->send("v $v");
|
||||
return $this->recv();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setFormat(string $fmt): string
|
||||
{
|
||||
$this->send("format $fmt");
|
||||
return $this->recv();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function exec(string $command, array $arguments = []): string
|
||||
{
|
||||
$buf = "exec $command";
|
||||
if (!empty($arguments)) {
|
||||
foreach ($arguments as $arg)
|
||||
$buf .= " $arg";
|
||||
}
|
||||
$this->send($buf);
|
||||
return $this->recv();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function recv()
|
||||
{
|
||||
$recv_buf = '';
|
||||
$buf = '';
|
||||
|
||||
while (true) {
|
||||
$result = socket_recv($this->sock, $recv_buf, 1024, 0);
|
||||
if ($result === false)
|
||||
throw new Exception(__METHOD__ . ": socket_recv() failed: " . $this->getSocketError());
|
||||
|
||||
// peer disconnected
|
||||
if ($result === 0)
|
||||
break;
|
||||
|
||||
$buf .= $recv_buf;
|
||||
if (endsWith($buf, "\r\n\r\n"))
|
||||
break;
|
||||
}
|
||||
|
||||
$response = explode("\r\n", $buf);
|
||||
$status = array_shift($response);
|
||||
if (!in_array($status, ['ok', 'err']))
|
||||
throw new Exception(__METHOD__.': unexpected status ('.$status.')');
|
||||
if ($status == 'err')
|
||||
throw new Exception(empty($response) ? 'unknown inverterd error' : $response[0]);
|
||||
|
||||
return trim(implode("\r\n", $response));
|
||||
}
|
||||
|
||||
}
|
133
localwebsite/classes/MyOpenWrtUtils.php
Normal file
133
localwebsite/classes/MyOpenWrtUtils.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
class MyOpenWrtUtils {
|
||||
|
||||
public static function getRoutingTable($table = null) {
|
||||
$arguments = ['route-show'];
|
||||
if ($table)
|
||||
$arguments[] = $table;
|
||||
|
||||
return self::toList(self::run($arguments));
|
||||
}
|
||||
|
||||
public static function getRoutingRules() {
|
||||
return self::toList(self::run(['rule-show']));
|
||||
}
|
||||
|
||||
public static function ipsetList($set_name) {
|
||||
return self::toList(self::run(['ipset-list', $set_name]));
|
||||
}
|
||||
|
||||
public static function ipsetListAll() {
|
||||
global $config;
|
||||
|
||||
$args = ['ipset-list-all'];
|
||||
$args = array_merge($args, array_keys($config['modems']));
|
||||
|
||||
$lines = self::toList(self::run($args));
|
||||
|
||||
$sets = [];
|
||||
$cur_set = null;
|
||||
foreach ($lines as $line) {
|
||||
if (startsWith($line, '>')) {
|
||||
$cur_set = substr($line, 1);
|
||||
if (!isset($sets[$cur_set]))
|
||||
$sets[$cur_set] = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_null($cur_set)) {
|
||||
debugError(__METHOD__.': cur_set is not set');
|
||||
continue;
|
||||
}
|
||||
|
||||
$sets[$cur_set][] = $line;
|
||||
}
|
||||
|
||||
return $sets;
|
||||
}
|
||||
|
||||
public static function ipsetAdd($set_name, $ip) {
|
||||
return self::run(['ipset-add', $set_name, $ip]);
|
||||
}
|
||||
|
||||
public static function ipsetDel($set_name, $ip) {
|
||||
return self::run(['ipset-del', $set_name, $ip]);
|
||||
}
|
||||
|
||||
public static function getDHCPLeases() {
|
||||
$list = self::toList(self::run(['dhcp-leases']));
|
||||
$list = array_map('self::toDHCPLease', $list);
|
||||
return $list;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// http functions
|
||||
//
|
||||
|
||||
private static function run(array $arguments) {
|
||||
$url = self::getLink($arguments);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$body = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($code != 200)
|
||||
throw new Exception(__METHOD__.': http code '.$code);
|
||||
|
||||
curl_close($ch);
|
||||
return trim($body);
|
||||
}
|
||||
|
||||
private static function getLink($arguments) {
|
||||
global $config;
|
||||
|
||||
$url = 'http://'.$config['openwrt_ip'].'/cgi-bin/luci/command/cfg099944';
|
||||
if (!empty($arguments)) {
|
||||
$arguments = array_map(function($arg) {
|
||||
$arg = str_replace('/', '_', $arg);
|
||||
return urlencode($arg);
|
||||
}, $arguments);
|
||||
$arguments = implode('%20', $arguments);
|
||||
|
||||
$url .= '/';
|
||||
$url .= $arguments;
|
||||
}
|
||||
|
||||
// debugLog($url);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// parsing functions
|
||||
//
|
||||
|
||||
private static function toList(string $s): array {
|
||||
if ($s == '')
|
||||
return [];
|
||||
return explode("\n", $s);
|
||||
}
|
||||
|
||||
private static function toDHCPLease(string $s): array {
|
||||
$words = explode(' ', $s);
|
||||
$time = array_shift($words);
|
||||
$mac = array_shift($words);
|
||||
$ip = array_shift($words);
|
||||
array_pop($words);
|
||||
$hostname = trim(implode(' ', $words));
|
||||
if (!$hostname)
|
||||
$hostname = '?';
|
||||
return [
|
||||
'time' => $time,
|
||||
'time_s' => date('d M, H:i:s', $time),
|
||||
'mac' => $mac,
|
||||
'ip' => $ip,
|
||||
'hostname' => $hostname
|
||||
];
|
||||
}
|
||||
|
||||
}
|
90
localwebsite/classes/MySimpleSocketClient.php
Normal file
90
localwebsite/classes/MySimpleSocketClient.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
class MySimpleSocketClient {
|
||||
|
||||
protected $sock;
|
||||
|
||||
public function __construct(string $host, int $port)
|
||||
{
|
||||
if (($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false)
|
||||
throw new Exception("socket_create() failed: ".$this->getSocketError());
|
||||
|
||||
$this->sock = $socket;
|
||||
|
||||
if ((socket_connect($socket, $host, $port)) === false)
|
||||
throw new Exception("socket_connect() failed: ".$this->getSocketError());
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function send(string $data)
|
||||
{
|
||||
$data .= "\r\n";
|
||||
$remained = strlen($data);
|
||||
|
||||
while ($remained > 0) {
|
||||
$result = socket_write($this->sock, $data);
|
||||
if ($result === false)
|
||||
throw new Exception(__METHOD__ . ": socket_write() failed: ".$this->getSocketError());
|
||||
|
||||
$remained -= $result;
|
||||
if ($remained > 0)
|
||||
$data = substr($data, $result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function recv()
|
||||
{
|
||||
$recv_buf = '';
|
||||
$buf = '';
|
||||
|
||||
while (true) {
|
||||
$result = socket_recv($this->sock, $recv_buf, 1024, 0);
|
||||
if ($result === false)
|
||||
throw new Exception(__METHOD__ . ": socket_recv() failed: " . $this->getSocketError());
|
||||
|
||||
// peer disconnected
|
||||
if ($result === 0)
|
||||
break;
|
||||
|
||||
$buf .= $recv_buf;
|
||||
if (endsWith($buf, "\r\n"))
|
||||
break;
|
||||
}
|
||||
|
||||
return trim($buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close connection.
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
if (!$this->sock)
|
||||
return;
|
||||
|
||||
socket_close($this->sock);
|
||||
$this->sock = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getSocketError(): string
|
||||
{
|
||||
$sle_args = [];
|
||||
if ($this->sock !== null)
|
||||
$sle_args[] = $this->sock;
|
||||
return socket_strerror(socket_last_error(...$sle_args));
|
||||
}
|
||||
|
||||
}
|
31
localwebsite/classes/Si7021dClient.php
Normal file
31
localwebsite/classes/Si7021dClient.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
class Si7021dClient extends MySimpleSocketClient {
|
||||
|
||||
public string $name;
|
||||
public float $temp;
|
||||
public float $humidity;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(string $host, int $port, string $name) {
|
||||
parent::__construct($host, $port);
|
||||
$this->name = $name;
|
||||
|
||||
socket_set_timeout($this->sock, 3);
|
||||
}
|
||||
|
||||
public function readSensor(): void {
|
||||
$this->send('read');
|
||||
|
||||
$data = jsonDecode($this->recv());
|
||||
|
||||
$temp = round((float)$data['temp'], 3);
|
||||
$hum = round((float)$data['humidity'], 3);
|
||||
|
||||
$this->temp = $temp;
|
||||
$this->humidity = $hum;
|
||||
}
|
||||
|
||||
}
|
14
localwebsite/composer.json
Normal file
14
localwebsite/composer.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "ch1p/localwebsite.homekit",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"twig/twig": "^3.3",
|
||||
"ext-mbstring": "*",
|
||||
"ext-sockets": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-gmp": "*"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
205
localwebsite/composer.lock
generated
Normal file
205
localwebsite/composer.lock
generated
Normal file
@ -0,0 +1,205 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d61e8a038cdf7b7e0b456809da3a1660",
|
||||
"packages": [
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.23.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
|
||||
"reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.23-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Ctype\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gert de Pagter",
|
||||
"email": "BackEndTea@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for ctype functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"ctype",
|
||||
"polyfill",
|
||||
"portable"
|
||||
],
|
||||
"time": "2021-02-19T12:13:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.23.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
|
||||
"reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.23-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"time": "2021-05-27T09:27:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "21578f00e83d4a82ecfa3d50752b609f13de6790"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/21578f00e83d4a82ecfa3d50752b609f13de6790",
|
||||
"reference": "21578f00e83d4a82ecfa3d50752b609f13de6790",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/polyfill-ctype": "^1.8",
|
||||
"symfony/polyfill-mbstring": "^1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/container": "^1.0",
|
||||
"symfony/phpunit-bridge": "^4.4.9|^5.0.9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.3-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Twig\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com",
|
||||
"homepage": "http://fabien.potencier.org",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Twig Team",
|
||||
"role": "Contributors"
|
||||
},
|
||||
{
|
||||
"name": "Armin Ronacher",
|
||||
"email": "armin.ronacher@active-4.com",
|
||||
"role": "Project Founder"
|
||||
}
|
||||
],
|
||||
"description": "Twig, the flexible, fast, and secure template language for PHP",
|
||||
"homepage": "https://twig.symfony.com",
|
||||
"keywords": [
|
||||
"templating"
|
||||
],
|
||||
"time": "2021-05-16T12:14:13+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": []
|
||||
}
|
58
localwebsite/config.php
Normal file
58
localwebsite/config.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'group' => 'www-data',
|
||||
'files_mode' => 0664,
|
||||
'dirs_mode' => 0775,
|
||||
'is_dev' => true,
|
||||
'static_public_path' => '/assets',
|
||||
|
||||
'openwrt_ip' => '192.168.1.1',
|
||||
|
||||
'inverterd_host' => '192.168.1.2',
|
||||
'inverterd_port' => 8305,
|
||||
|
||||
'pump_host' => '192.168.1.2',
|
||||
'pump_port' => 8307,
|
||||
|
||||
'si7021d_servers' => [
|
||||
// fill here, example:
|
||||
'hall' => ['192.168.1.3', 8306, 'Big Hall'],
|
||||
],
|
||||
|
||||
// modem names (array keys) must match ipset names and
|
||||
// routing table names on the openwrt router
|
||||
//
|
||||
// the order of the keys in the array must be the same
|
||||
// as the order in which fwmark iptables rules are applied
|
||||
'modems' => [
|
||||
'modem-example' => [
|
||||
'ip' => '1.2.3.4',
|
||||
'label' => 'Modem Name',
|
||||
'short_label' => 'Mname',
|
||||
'legacy_token_auth' => false,
|
||||
],
|
||||
],
|
||||
|
||||
// 'routing_smallhome_ip' => 'fill_me',
|
||||
// 'routing_default' => 'fill_me',
|
||||
|
||||
'debug_backtrace' => true,
|
||||
'debug_file' => '.debug.log',
|
||||
|
||||
'twig_cache' => true,
|
||||
'templates' => [
|
||||
'web' => [
|
||||
'root' => 'templates-web',
|
||||
'cache' => 'cache/templates-web',
|
||||
],
|
||||
],
|
||||
|
||||
'static' => [
|
||||
'app.css' => 6,
|
||||
'app.js' => 1,
|
||||
'polyfills.js' => 1,
|
||||
'modem.js' => 1,
|
||||
'inverter.js' => 2,
|
||||
]
|
||||
];
|
355
localwebsite/engine/debug.php
Normal file
355
localwebsite/engine/debug.php
Normal file
@ -0,0 +1,355 @@
|
||||
<?php
|
||||
|
||||
// require_once 'engine/mysql.php';
|
||||
|
||||
class debug {
|
||||
|
||||
protected static $Types = [
|
||||
1 => 'E_ERROR',
|
||||
2 => 'E_WARNING',
|
||||
4 => 'E_PARSE',
|
||||
8 => 'E_NOTICE',
|
||||
16 => 'E_CORE_ERROR',
|
||||
32 => 'E_CORE_WARNING',
|
||||
64 => 'E_COMPILE_ERROR',
|
||||
128 => 'E_COMPILE_WARNING',
|
||||
256 => 'E_USER_ERROR',
|
||||
512 => 'E_USER_WARNING',
|
||||
1024 => 'E_USER_NOTICE',
|
||||
2048 => 'E_STRICT',
|
||||
4096 => 'E_RECOVERABLE_ERROR',
|
||||
8192 => 'E_DEPRECATED',
|
||||
16384 => 'E_USER_DEPRECATED',
|
||||
32767 => 'E_ALL'
|
||||
];
|
||||
|
||||
const STORE_NONE = -1;
|
||||
const STORE_MYSQL = 0;
|
||||
const STORE_FILE = 1;
|
||||
const STORE_BOTH = 2;
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
protected $enabled = false;
|
||||
protected $errCounter = 0;
|
||||
protected $logCounter = 0;
|
||||
protected $messagesStoreType = self::STORE_NONE;
|
||||
protected $errorsStoreType = self::STORE_NONE;
|
||||
protected $filter;
|
||||
protected $reportRecursionLevel = 0;
|
||||
protected $overridenDebugFile = null;
|
||||
protected $silent = false;
|
||||
protected $prefix;
|
||||
|
||||
private function __construct($filter) {
|
||||
$this->filter = $filter;
|
||||
}
|
||||
|
||||
public static function getInstance($filter = null) {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self($filter);
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function enable() {
|
||||
$self = $this;
|
||||
|
||||
set_error_handler(function($no, $str, $file, $line) use ($self) {
|
||||
if ($self->silent || !$self->enabled) {
|
||||
return;
|
||||
}
|
||||
if ((is_callable($this->filter) && !($this->filter)($no, $file, $line, $str)) || !$self->canReport()) {
|
||||
return;
|
||||
}
|
||||
$self->report(true, $str, $no, $file, $line);
|
||||
});
|
||||
|
||||
append_shutdown_function(function() use ($self) {
|
||||
if (!$self->enabled || !($error = error_get_last())) {
|
||||
return;
|
||||
}
|
||||
if (is_callable($this->filter)
|
||||
&& !($this->filter)($error['type'], $error['file'], $error['line'], $error['message'])) {
|
||||
return;
|
||||
}
|
||||
if (!$self->canReport()) {
|
||||
return;
|
||||
}
|
||||
$self->report(true, $error['message'], $error['type'], $error['file'], $error['line']);
|
||||
});
|
||||
|
||||
$this->enabled = true;
|
||||
}
|
||||
|
||||
public function disable() {
|
||||
restore_error_handler();
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
public function report($is_error, $text, $errno = 0, $errfile = '', $errline = '') {
|
||||
global $config;
|
||||
|
||||
$this->reportRecursionLevel++;
|
||||
|
||||
$logstarted = $this->errCounter > 0 || $this->logCounter > 0;
|
||||
$num = $is_error ? $this->errCounter++ : $this->logCounter++;
|
||||
$custom = $is_error && !$errno;
|
||||
$ts = time();
|
||||
$exectime = exectime();
|
||||
$bt = backtrace(2);
|
||||
|
||||
$store_file = (!$is_error && $this->checkMessagesStoreType(self::STORE_FILE))
|
||||
|| ($is_error && $this->checkErrorsStoreType(self::STORE_FILE));
|
||||
|
||||
$store_mysql = (!$is_error && $this->checkMessagesStoreType(self::STORE_MYSQL))
|
||||
|| ($is_error && $this->checkErrorsStoreType(self::STORE_MYSQL));
|
||||
|
||||
if ($this->prefix)
|
||||
$text = $this->prefix.$text;
|
||||
|
||||
// if ($store_mysql) {
|
||||
// $db = getMySQL('local_logs', true);
|
||||
// $data = [
|
||||
// 'ts' => $ts,
|
||||
// 'num' => $num,
|
||||
// 'time' => $exectime,
|
||||
// 'custom' => intval($custom),
|
||||
// 'errno' => $errno,
|
||||
// 'file' => $errfile,
|
||||
// 'line' => $errline,
|
||||
// 'text' => $text,
|
||||
// 'stacktrace' => $bt,
|
||||
// 'is_cli' => PHP_SAPI == 'cli' ? 1 : 0,
|
||||
// ];
|
||||
// if (PHP_SAPI == 'cli') {
|
||||
// $data += [
|
||||
// 'ip' => '',
|
||||
// 'ua' => '',
|
||||
// 'url' => '',
|
||||
// ];
|
||||
// } else {
|
||||
// $data += [
|
||||
// 'ip' => ip2ulong($_SERVER['REMOTE_ADDR']),
|
||||
// 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
// 'url' => $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']
|
||||
// ];
|
||||
// }
|
||||
// $db->insert('backend_errors', $data);
|
||||
// }
|
||||
|
||||
if ($store_file) {
|
||||
$title = PHP_SAPI == 'cli' ? 'cli' : $_SERVER['REQUEST_URI'];
|
||||
$date = date('d/m/y H:i:s', $ts);
|
||||
$exectime = (string)$exectime;
|
||||
if (strlen($exectime) < 6)
|
||||
$exectime .= str_repeat('0', 6 - strlen($exectime));
|
||||
|
||||
$buf = "";
|
||||
if (!$logstarted) {
|
||||
$buf .= "\n<e fg=white bg=magenta style=fgbright,bold> {$title} </e><e fg=white bg=blue style=fgbright> {$date} </e>\n";
|
||||
}
|
||||
$buf .= "<e fg=".($is_error ? 'red' : 'white').">".($is_error ? 'E' : 'I')."=<e style=bold>${num}</e> <e fg=cyan>{$exectime}</e> ";
|
||||
if ($is_error && !$custom) {
|
||||
$buf .= "<e fg=green>{$errfile}<e fg=white>:<e fg=green style=fgbright>{$errline}</e> (".self::errname($errno).") ";
|
||||
}
|
||||
$buf = stransi($buf);
|
||||
|
||||
$buf .= $text;
|
||||
$buf .= "\n";
|
||||
if ($is_error && $config['debug_backtrace']) {
|
||||
$buf .= $bt."\n";
|
||||
}
|
||||
|
||||
$debug_file = $this->getDebugFile();
|
||||
|
||||
$logdir = dirname($debug_file);
|
||||
if (!file_exists($logdir)) {
|
||||
mkdir($logdir);
|
||||
setperm($logdir);
|
||||
}
|
||||
|
||||
$f = fopen($debug_file, 'a');
|
||||
if ($f) {
|
||||
fwrite($f, $buf);
|
||||
fclose($f);
|
||||
}
|
||||
}
|
||||
|
||||
$this->reportRecursionLevel--;
|
||||
}
|
||||
|
||||
public function canReport() {
|
||||
return $this->reportRecursionLevel < 2;
|
||||
}
|
||||
|
||||
public function setErrorsStoreType($errorsStoreType) {
|
||||
$this->errorsStoreType = $errorsStoreType;
|
||||
}
|
||||
|
||||
public function setMessagesStoreType($messagesStoreType) {
|
||||
$this->messagesStoreType = $messagesStoreType;
|
||||
}
|
||||
|
||||
public function checkMessagesStoreType($store_type) {
|
||||
return $this->messagesStoreType == $store_type || $this->messagesStoreType == self::STORE_BOTH;
|
||||
}
|
||||
|
||||
public function checkErrorsStoreType($store_type) {
|
||||
return $this->errorsStoreType == $store_type || $this->errorsStoreType == self::STORE_BOTH;
|
||||
}
|
||||
|
||||
public function overrideDebugFile($file) {
|
||||
$this->overridenDebugFile = $file;
|
||||
}
|
||||
|
||||
protected function getDebugFile() {
|
||||
global $config;
|
||||
return is_null($this->overridenDebugFile) ? ROOT.'/'.$config['debug_file'] : $this->overridenDebugFile;
|
||||
}
|
||||
|
||||
public function setSilence($silent) {
|
||||
$this->silent = $silent;
|
||||
}
|
||||
|
||||
public function setPrefix($prefix) {
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
public static function errname($errno) {
|
||||
static $errors = null;
|
||||
if (is_null($errors)) {
|
||||
$errors = array_flip(array_slice(get_defined_constants(true)['Core'], 0, 15, true));
|
||||
}
|
||||
return $errors[$errno];
|
||||
}
|
||||
|
||||
public static function getTypes() {
|
||||
return self::$Types;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class debug_measure {
|
||||
|
||||
private $name;
|
||||
private $time;
|
||||
private $started = false;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return $this
|
||||
*/
|
||||
public function start($name = null) {
|
||||
if (is_null($name)) {
|
||||
$name = strgen(3);
|
||||
}
|
||||
$this->name = $name;
|
||||
$this->time = microtime(true);
|
||||
$this->started = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float|string|null
|
||||
*/
|
||||
public function finish() {
|
||||
if (!$this->started) {
|
||||
debugLog("debug_measure::finish(): not started, name=".$this->name);
|
||||
return null;
|
||||
}
|
||||
|
||||
$time = (microtime(true) - $this->time);
|
||||
debugLog("MEASURE".($this->name != '' ? ' '.$this->name : '').": ".$time);
|
||||
|
||||
$this->started = false;
|
||||
return $time;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $var
|
||||
* @return string
|
||||
*/
|
||||
function str_print_r($var) {
|
||||
ob_start();
|
||||
print_r($var);
|
||||
return trim(ob_get_clean());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $var
|
||||
* @return string
|
||||
*/
|
||||
function str_var_dump($var) {
|
||||
ob_start();
|
||||
var_dump($var);
|
||||
return trim(ob_get_clean());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $args
|
||||
* @param bool $all_dump
|
||||
* @return string
|
||||
*/
|
||||
function str_vars($args, $all_dump = false) {
|
||||
return implode(' ', array_map(function($a) use ($all_dump) {
|
||||
if ($all_dump) {
|
||||
return str_var_dump($a);
|
||||
}
|
||||
$type = gettype($a);
|
||||
if ($type == 'string' || $type == 'integer' || $type == 'double') {
|
||||
return $a;
|
||||
} else if ($type == 'array' || $type == 'object') {
|
||||
return str_print_r($a);
|
||||
} else {
|
||||
return str_var_dump($a);
|
||||
}
|
||||
}, $args));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $shift
|
||||
* @return string
|
||||
*/
|
||||
function backtrace($shift = 0) {
|
||||
$bt = debug_backtrace();
|
||||
$lines = [];
|
||||
foreach ($bt as $i => $t) {
|
||||
if ($i < $shift) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($t['file'])) {
|
||||
$lines[] = 'from ?';
|
||||
} else {
|
||||
$lines[] = 'from '.$t['file'].':'.$t['line'];
|
||||
}
|
||||
}
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed ...$args
|
||||
*/
|
||||
function debugLog(...$args) {
|
||||
global $config;
|
||||
if (!$config['is_dev'])
|
||||
return;
|
||||
|
||||
debug::getInstance()->report(false, str_vars($args));
|
||||
}
|
||||
|
||||
function debugLogOnProd(...$args) {
|
||||
debug::getInstance()->report(false, str_vars($args));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed ...$args
|
||||
*/
|
||||
function debugError(...$args) {
|
||||
$debug = debug::getInstance();
|
||||
if ($debug->canReport()) {
|
||||
$debug->report(true, str_vars($args));
|
||||
}
|
||||
}
|
142
localwebsite/engine/request_handler.php
Normal file
142
localwebsite/engine/request_handler.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
abstract class request_handler {
|
||||
|
||||
const GET = 'GET';
|
||||
const POST = 'POST';
|
||||
|
||||
private static array $AllowedInputTypes = ['i', 'f', 'b', 'e' /* enum */];
|
||||
|
||||
public function dispatch(string $act) {
|
||||
$method = $_SERVER['REQUEST_METHOD'] == 'POST' ? 'POST' : 'GET';
|
||||
return $this->call_act($method, $act);
|
||||
}
|
||||
|
||||
protected function before_dispatch(string $method, string $act)/*: ?array*/ {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function call_act(string $method, string $act, array $input = []) {
|
||||
global $RouterInput;
|
||||
|
||||
$notfound = !method_exists($this, $method.'_'.$act) || !((new ReflectionMethod($this, $method.'_'.$act))->isPublic());
|
||||
if ($notfound)
|
||||
$this->method_not_found($method, $act);
|
||||
|
||||
if (!empty($input)) {
|
||||
foreach ($input as $k => $v)
|
||||
$RouterInput[$k] = $v;
|
||||
}
|
||||
|
||||
$args = $this->before_dispatch($method, $act);
|
||||
return call_user_func_array([$this, $method.'_'.$act], is_array($args) ? [$args] : []);
|
||||
}
|
||||
|
||||
abstract protected function method_not_found(string $method, string $act);
|
||||
|
||||
protected function input(string $input, bool $as_assoc = false): array {
|
||||
$input = preg_split('/,\s+?/', $input, null, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
$ret = [];
|
||||
foreach ($input as $var) {
|
||||
list($type, $name, $enum_values, $enum_default) = self::parse_input_var($var);
|
||||
|
||||
$value = param($name);
|
||||
|
||||
switch ($type) {
|
||||
case 'i':
|
||||
if (is_null($value) && !is_null($enum_default)) {
|
||||
$value = (int)$enum_default;
|
||||
} else {
|
||||
$value = (int)$value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'f':
|
||||
if (is_null($value) && !is_null($enum_default)) {
|
||||
$value = (float)$enum_default;
|
||||
} else {
|
||||
$value = (float)$value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'b':
|
||||
if (is_null($value) && !is_null($enum_default)) {
|
||||
$value = (bool)$enum_default;
|
||||
} else {
|
||||
$value = (bool)$value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'e':
|
||||
if (!in_array($value, $enum_values)) {
|
||||
$value = !is_null($enum_default) ? $enum_default : '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$as_assoc) {
|
||||
$ret[] = $value;
|
||||
} else {
|
||||
$ret[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
protected static function parse_input_var(string $var): array {
|
||||
$type = null;
|
||||
$name = null;
|
||||
$enum_values = null;
|
||||
$enum_default = null;
|
||||
|
||||
$pos = strpos($var, ':');
|
||||
if ($pos !== false) {
|
||||
$type = substr($var, 0, $pos);
|
||||
$rest = substr($var, $pos+1);
|
||||
|
||||
if (!in_array($type, self::$AllowedInputTypes)) {
|
||||
trigger_error('request_handler::parse_input_var('.$var.'): unknown type '.$type);
|
||||
$type = null;
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'e':
|
||||
$br_from = strpos($rest, '(');
|
||||
$br_to = strpos($rest, ')');
|
||||
|
||||
if ($br_from === false || $br_to === false) {
|
||||
trigger_error('request_handler::parse_input_var('.$var.'): failed to parse enum values');
|
||||
$type = null;
|
||||
$name = $rest;
|
||||
break;
|
||||
}
|
||||
|
||||
$enum_values = array_map('trim', explode('|', trim(substr($rest, $br_from+1, $br_to-$br_from-1))));
|
||||
$name = trim(substr($rest, 0, $br_from));
|
||||
|
||||
if (!empty($enum_values)) foreach ($enum_values as $key => $val) {
|
||||
if (substr($val, 0, 1) == '=') {
|
||||
$enum_values[$key] = substr($val, 1);
|
||||
$enum_default = $enum_values[$key];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (($eq_pos = strpos($rest, '=')) !== false) {
|
||||
$enum_default = substr($rest, $eq_pos+1);
|
||||
$rest = substr($rest, 0, $eq_pos);
|
||||
}
|
||||
$name = trim($rest);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$type = 's';
|
||||
$name = $var;
|
||||
}
|
||||
|
||||
return [$type, $name, $enum_values, $enum_default];
|
||||
}
|
||||
|
||||
}
|
199
localwebsite/engine/router.php
Normal file
199
localwebsite/engine/router.php
Normal file
@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
class router {
|
||||
|
||||
protected array $routes = [
|
||||
'children' => [],
|
||||
're_children' => []
|
||||
];
|
||||
|
||||
public function add($template, $value) {
|
||||
if ($template == '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// expand {enum,erat,ions}
|
||||
$templates = [[$template, $value]];
|
||||
if (preg_match_all('/\{([\w\d_\-,]+)\}/', $template, $matches)) {
|
||||
foreach ($matches[1] as $match_index => $variants) {
|
||||
$variants = explode(',', $variants);
|
||||
$variants = array_map('trim', $variants);
|
||||
$variants = array_filter($variants, function($s) { return $s != ''; });
|
||||
|
||||
for ($i = 0; $i < count($templates); ) {
|
||||
list($template, $value) = $templates[$i];
|
||||
$new_templates = [];
|
||||
foreach ($variants as $variant_index => $variant) {
|
||||
$new_templates[] = [
|
||||
str_replace_once($matches[0][$match_index], $variant, $template),
|
||||
str_replace('${'.($match_index+1).'}', $variant, $value)
|
||||
];
|
||||
}
|
||||
array_splice($templates, $i, 1, $new_templates);
|
||||
$i += count($new_templates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process all generated routes
|
||||
foreach ($templates as $template) {
|
||||
list($template, $value) = $template;
|
||||
|
||||
$start_pos = 0;
|
||||
$parent = &$this->routes;
|
||||
$template_len = strlen($template);
|
||||
|
||||
while ($start_pos < $template_len) {
|
||||
$slash_pos = strpos($template, '/', $start_pos);
|
||||
if ($slash_pos !== false) {
|
||||
$part = substr($template, $start_pos, $slash_pos-$start_pos+1);
|
||||
$start_pos = $slash_pos+1;
|
||||
} else {
|
||||
$part = substr($template, $start_pos);
|
||||
$start_pos = $template_len;
|
||||
}
|
||||
|
||||
$parent = &$this->_addRoute($parent, $part,
|
||||
$start_pos < $template_len ? null : $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function &_addRoute(&$parent, $part, $value = null) {
|
||||
$par_pos = strpos($part, '(');
|
||||
$is_regex = $par_pos !== false && ($par_pos == 0 || $part[$par_pos-1] != '\\');
|
||||
|
||||
$children_key = !$is_regex ? 'children' : 're_children';
|
||||
|
||||
if (isset($parent[$children_key][$part])) {
|
||||
if (is_null($value)) {
|
||||
$parent = &$parent[$children_key][$part];
|
||||
} else {
|
||||
if (!isset($parent[$children_key][$part]['value'])) {
|
||||
$parent[$children_key][$part]['value'] = $value;
|
||||
} else {
|
||||
trigger_error(__METHOD__.': route is already defined');
|
||||
}
|
||||
}
|
||||
return $parent;
|
||||
}
|
||||
|
||||
$child = [
|
||||
'children' => [],
|
||||
're_children' => []
|
||||
];
|
||||
if (!is_null($value)) {
|
||||
$child['value'] = $value;
|
||||
}
|
||||
|
||||
$parent[$children_key][$part] = $child;
|
||||
return $parent[$children_key][$part];
|
||||
}
|
||||
|
||||
public function find($uri) {
|
||||
if ($uri != '/' && $uri[0] == '/') {
|
||||
$uri = substr($uri, 1);
|
||||
}
|
||||
$start_pos = 0;
|
||||
$parent = &$this->routes;
|
||||
$uri_len = strlen($uri);
|
||||
$matches = [];
|
||||
|
||||
while ($start_pos < $uri_len) {
|
||||
$slash_pos = strpos($uri, '/', $start_pos);
|
||||
if ($slash_pos !== false) {
|
||||
$part = substr($uri, $start_pos, $slash_pos-$start_pos+1);
|
||||
$start_pos = $slash_pos+1;
|
||||
} else {
|
||||
$part = substr($uri, $start_pos);
|
||||
$start_pos = $uri_len;
|
||||
}
|
||||
|
||||
$found = false;
|
||||
if (isset($parent['children'][$part])) {
|
||||
$parent = &$parent['children'][$part];
|
||||
$found = true;
|
||||
} else if (!empty($parent['re_children'])) {
|
||||
foreach ($parent['re_children'] as $re => &$child) {
|
||||
$exp = '#^'.$re.'$#';
|
||||
$re_result = preg_match($exp, $part, $match);
|
||||
if ($re_result === false) {
|
||||
debugError(__METHOD__.": regex $exp failed");
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($re_result) {
|
||||
if (count($match) > 1) {
|
||||
$matches = array_merge($matches, array_slice($match, 1));
|
||||
}
|
||||
$parent = &$child;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($parent['value'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value = $parent['value'];
|
||||
if (!empty($matches)) {
|
||||
foreach ($matches as $i => $match) {
|
||||
$needle = '$('.($i+1).')';
|
||||
$pos = strpos($value, $needle);
|
||||
if ($pos !== false) {
|
||||
$value = substr_replace($value, $match, $pos, strlen($needle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function load($routes) {
|
||||
$this->routes = $routes;
|
||||
}
|
||||
|
||||
public function dump() {
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function routerFind(router $router) {
|
||||
$document_uri = $_SERVER['REQUEST_URI'];
|
||||
if (($pos = strpos($document_uri, '?')) !== false)
|
||||
$document_uri = substr($document_uri, 0, $pos);
|
||||
$document_uri = urldecode($document_uri);
|
||||
|
||||
$fixed_document_uri = preg_replace('#/+#', '/', $document_uri);
|
||||
if ($fixed_document_uri != $document_uri && !is_xhr_request()) {
|
||||
redirect($fixed_document_uri);
|
||||
} else {
|
||||
$document_uri = $fixed_document_uri;
|
||||
}
|
||||
|
||||
$route = $router->find($document_uri);
|
||||
if ($route === false)
|
||||
return false;
|
||||
|
||||
$route = preg_split('/ +/', $route);
|
||||
$handler = $route[0];
|
||||
$act = $route[1];
|
||||
$input = [];
|
||||
if (count($route) > 2) {
|
||||
for ($i = 2; $i < count($route); $i++) {
|
||||
$var = $route[$i];
|
||||
list($k, $v) = explode('=', $var);
|
||||
$input[trim($k)] = trim($v);
|
||||
}
|
||||
}
|
||||
|
||||
return [$handler, $act, $input];
|
||||
}
|
520
localwebsite/engine/tpl.php
Normal file
520
localwebsite/engine/tpl.php
Normal file
@ -0,0 +1,520 @@
|
||||
<?php
|
||||
|
||||
abstract class base_tpl {
|
||||
|
||||
public $twig;
|
||||
protected $vars = [];
|
||||
protected $global_vars = [];
|
||||
protected $title = '';
|
||||
protected $title_modifiers = [];
|
||||
protected $keywords = '';
|
||||
protected $description = '';
|
||||
protected $js = [];
|
||||
protected $lang_keys = [];
|
||||
protected $static = [];
|
||||
protected $external_static = [];
|
||||
protected $head = [];
|
||||
protected $globals_applied = false;
|
||||
protected $static_time;
|
||||
|
||||
public function __construct($templates_dir, $cache_dir) {
|
||||
global $config;
|
||||
|
||||
$cl = get_called_class();
|
||||
|
||||
$this->twig = self::twig_instance($templates_dir, $cache_dir, $config['is_dev']);
|
||||
$this->static_time = time();
|
||||
}
|
||||
|
||||
public static function twig_instance($templates_dir, $cache_dir, $auto_reload) {
|
||||
// must specify a second argument ($rootPath) here
|
||||
// otherwise it will be getcwd() and it's www-prod/htdocs/ for apache and www-prod/ for cli code
|
||||
// this is bad for templates rebuilding
|
||||
$twig_loader = new \Twig\Loader\FilesystemLoader($templates_dir, ROOT);
|
||||
|
||||
$env_options = [];
|
||||
if (!is_null($cache_dir)) {
|
||||
$env_options += [
|
||||
'cache' => $cache_dir,
|
||||
'auto_reload' => $auto_reload
|
||||
];
|
||||
}
|
||||
|
||||
$twig = new \Twig\Environment($twig_loader, $env_options);
|
||||
$twig->addExtension(new Twig_MyExtension);
|
||||
|
||||
return $twig;
|
||||
}
|
||||
|
||||
public function render($template, array $vars = []) {
|
||||
$this->apply_globals();
|
||||
return $this->do_render($template, array_merge($this->vars, $vars));
|
||||
}
|
||||
|
||||
protected function do_render($template, $vars) {
|
||||
global $config;
|
||||
$s = '';
|
||||
try {
|
||||
$s = $this->twig->render($template, $vars);
|
||||
} catch (\Twig\Error\Error $e) {
|
||||
$error = get_class($e).": failed to render";
|
||||
$source_ctx = $e->getSourceContext();
|
||||
if ($source_ctx) {
|
||||
$path = $source_ctx->getPath();
|
||||
if (startsWith($path, ROOT))
|
||||
$path = substr($path, strlen(ROOT)+1);
|
||||
$error .= " ".$source_ctx->getName()." (".$path.") at line ".$e->getTemplateLine();
|
||||
}
|
||||
$error .= ": ";
|
||||
$error .= $e->getMessage();
|
||||
debugError($error);
|
||||
if ($config['is_dev'])
|
||||
$s = $error."\n";
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function set($arg1, $arg2 = null) {
|
||||
if (is_array($arg1)) {
|
||||
foreach ($arg1 as $key => $value) {
|
||||
$this->vars[$key] = $value;
|
||||
}
|
||||
} elseif ($arg2 !== null) {
|
||||
$this->vars[$arg1] = $arg2;
|
||||
}
|
||||
}
|
||||
|
||||
public function is_set($key): bool {
|
||||
return isset($this->vars[$key]);
|
||||
}
|
||||
|
||||
public function set_global($arg1, $arg2 = null) {
|
||||
if (is_array($arg1)) {
|
||||
foreach ($arg1 as $key => $value) {
|
||||
$this->global_vars[$key] = $value;
|
||||
}
|
||||
} elseif ($arg2 !== null) {
|
||||
$this->global_vars[$arg1] = $arg2;
|
||||
}
|
||||
}
|
||||
|
||||
public function is_global_set($key): bool {
|
||||
return isset($this->global_vars[$key]);
|
||||
}
|
||||
|
||||
public function get_global($key) {
|
||||
return $this->is_global_set($key) ? $this->global_vars[$key] : null;
|
||||
}
|
||||
|
||||
public function apply_globals() {
|
||||
if (!empty($this->global_vars) && !$this->globals_applied) {
|
||||
foreach ($this->global_vars as $key => $value)
|
||||
$this->twig->addGlobal($key, $value);
|
||||
$this->globals_applied = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
*/
|
||||
public function set_title($title) {
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function get_title() {
|
||||
$title = $this->title != '' ? $this->title : 'Домашний сайт';
|
||||
if (!empty($this->title_modifiers)) {
|
||||
foreach ($this->title_modifiers as $modifier) {
|
||||
$title = $modifier($title);
|
||||
}
|
||||
}
|
||||
return $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $callable
|
||||
*/
|
||||
public function add_page_title_modifier(callable $callable) {
|
||||
if (!is_callable($callable)) {
|
||||
trigger_error(__METHOD__.': argument is not callable');
|
||||
} else {
|
||||
$this->title_modifiers[] = $callable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $css_name
|
||||
* @param null $extra
|
||||
*/
|
||||
public function add_static(string $name, $extra = null) {
|
||||
global $config;
|
||||
// $is_css = endsWith($name, '.css');
|
||||
$this->static[] = [$name, $extra];
|
||||
}
|
||||
|
||||
public function add_external_static($type, $url) {
|
||||
$this->external_static[] = ['type' => $type, 'url' => $url];
|
||||
}
|
||||
|
||||
public function add_js($js) {
|
||||
$this->js[] = $js;
|
||||
}
|
||||
|
||||
public function add_lang_keys(array $keys) {
|
||||
$this->lang_keys = array_merge($this->lang_keys, $keys);
|
||||
}
|
||||
|
||||
public function add_head($html) {
|
||||
$this->head[] = $html;
|
||||
}
|
||||
|
||||
public function get_head_html() {
|
||||
global $config;
|
||||
$lines = [];
|
||||
$public_path = $config['static_public_path'];
|
||||
foreach ($this->static as $val) {
|
||||
list($name, $extra) = $val;
|
||||
if (endsWith($name, '.js'))
|
||||
$lines[] = self::js_link($public_path.'/'.$name, $config['static'][$name] ?? 1);
|
||||
else
|
||||
$lines[] = self::css_link($public_path.'/'.$name, $config['static'][$name] ?? 1, $extra);
|
||||
}
|
||||
if (!empty($this->external_static)) {
|
||||
foreach ($this->external_static as $ext) {
|
||||
if ($ext['type'] == 'js')
|
||||
$lines[] = self::js_link($ext['url']);
|
||||
else if ($ext['type'] == 'css')
|
||||
$lines[] = self::css_link($ext['url']);
|
||||
}
|
||||
}
|
||||
if (!empty($this->head)) {
|
||||
$lines = array_merge($lines, $this->head);
|
||||
}
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
public static function js_link($name, $version = null): string {
|
||||
if ($version !== null)
|
||||
$name .= '?'.$version;
|
||||
return '<script src="'.$name.'" type="text/javascript"></script>';
|
||||
}
|
||||
|
||||
public static function css_link($name, $version = null, $extra = null) {
|
||||
if ($version !== null)
|
||||
$name .= '?'.$version;
|
||||
$s = '<link';
|
||||
if (is_array($extra)) {
|
||||
if (!empty($extra['id']))
|
||||
$s .= ' id="'.$extra['id'].'"';
|
||||
}
|
||||
$s .= ' rel="stylesheet" type="text/css"';
|
||||
if (is_array($extra) && !empty($extra['media']))
|
||||
$s .= ' media="'.$extra['media'].'"';
|
||||
$s .= ' href="'.$name.'"';
|
||||
$s .= '>';
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function get_lang_keys() {
|
||||
global $lang;
|
||||
$keys = [];
|
||||
if (!empty($this->lang_keys)) {
|
||||
foreach ($this->lang_keys as $key)
|
||||
$keys[$key] = $lang[$key];
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
public function render_not_found() {
|
||||
http_response_code(404);
|
||||
if (!is_xhr_request()) {
|
||||
$this->render_page('404.twig');
|
||||
} else {
|
||||
ajax_error(['code' => 404]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $reason
|
||||
*/
|
||||
public function render_forbidden($reason = null) {
|
||||
http_response_code(403);
|
||||
if (!is_xhr_request()) {
|
||||
$this->set(['reason' => $reason]);
|
||||
$this->render_page('403.twig');
|
||||
} else {
|
||||
$data = ['code' => 403];
|
||||
if (!is_null($reason))
|
||||
$data['reason'] = $reason;
|
||||
ajax_error($data);
|
||||
}
|
||||
}
|
||||
|
||||
public function must_revalidate() {
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
}
|
||||
|
||||
abstract public function render_page($template);
|
||||
|
||||
}
|
||||
|
||||
class web_tpl extends base_tpl {
|
||||
|
||||
protected $alternate = false;
|
||||
|
||||
public function __construct() {
|
||||
global $config;
|
||||
$templates = $config['templates']['web'];
|
||||
parent::__construct(
|
||||
ROOT.'/'. $templates['root'],
|
||||
$config['twig_cache']
|
||||
? ROOT.'/'.$templates['cache']
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
public function set_alternate($alt) {
|
||||
$this->alternate = $alt;
|
||||
}
|
||||
|
||||
public function render_page($template) {
|
||||
echo $this->_render_header();
|
||||
echo $this->_render_body($template);
|
||||
echo $this->_render_footer();
|
||||
exit;
|
||||
}
|
||||
|
||||
public function _render_header() {
|
||||
global $config;
|
||||
$this->apply_globals();
|
||||
|
||||
$vars = [
|
||||
'title' => $this->get_title(),
|
||||
'keywords' => $this->keywords,
|
||||
'description' => $this->description,
|
||||
'alternate' => $this->alternate,
|
||||
'static' => $this->get_head_html(),
|
||||
];
|
||||
return $this->do_render('header.twig', $vars);
|
||||
}
|
||||
|
||||
public function _render_body($template) {
|
||||
return $this->do_render($template, $this->vars);
|
||||
}
|
||||
|
||||
public function _render_footer() {
|
||||
$exec_time = microtime(true) - START_TIME;
|
||||
$exec_time = round($exec_time, 4);
|
||||
|
||||
$footer_vars = [
|
||||
'exec_time' => $exec_time,
|
||||
'js' => !empty($this->js) ? implode("\n", $this->js) : '',
|
||||
];
|
||||
return $this->do_render('footer.twig', $footer_vars);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Twig_MyExtension extends \Twig\Extension\AbstractExtension {
|
||||
|
||||
public function getFilters() {
|
||||
global $lang;
|
||||
|
||||
return array(
|
||||
new \Twig\TwigFilter('lang', 'lang'),
|
||||
|
||||
new \Twig\TwigFilter('lang', function($key, array $args = []) use (&$lang) {
|
||||
array_walk($args, function(&$item, $key) {
|
||||
$item = htmlescape($item);
|
||||
});
|
||||
array_unshift($args, $key);
|
||||
return call_user_func_array([$lang, 'get'], $args);
|
||||
}, ['is_variadic' => true]),
|
||||
|
||||
new \Twig\TwigFilter('plural', function($text, array $args = []) use (&$lang) {
|
||||
array_unshift($args, $text);
|
||||
return call_user_func_array([$lang, 'num'], $args);
|
||||
}, ['is_variadic' => true]),
|
||||
|
||||
new \Twig\TwigFilter('format_number', function($number, array $args = []) {
|
||||
array_unshift($args, $number);
|
||||
return call_user_func_array('formatNumber', $args);
|
||||
}, ['is_variadic' => true]),
|
||||
|
||||
new \Twig\TwigFilter('short_number', function($number, array $args = []) {
|
||||
array_unshift($args, $number);
|
||||
return call_user_func_array('shortNumber', $args);
|
||||
}, ['is_variadic']),
|
||||
|
||||
new \Twig\TwigFilter('format_time', function($ts, array $args = []) {
|
||||
array_unshift($args, $ts);
|
||||
return call_user_func_array('formatTime', $args);
|
||||
}, ['is_variadic' => true]),
|
||||
|
||||
new \Twig\TwigFilter('format_duration', function($seconds, array $args = []) {
|
||||
array_unshift($args, $seconds);
|
||||
return call_user_func_array('formatDuration', $args);
|
||||
}, ['is_variadic' => true]),
|
||||
);
|
||||
}
|
||||
|
||||
public function getTokenParsers() {
|
||||
return [new JsTagTokenParser()];
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return 'lang';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Based on https://stackoverflow.com/questions/26170727/how-to-create-a-twig-custom-tag-that-executes-a-callback
|
||||
class JsTagTokenParser extends \Twig\TokenParser\AbstractTokenParser {
|
||||
|
||||
public function parse(\Twig\Token $token) {
|
||||
$lineno = $token->getLine();
|
||||
$stream = $this->parser->getStream();
|
||||
|
||||
// recovers all inline parameters close to your tag name
|
||||
$params = array_merge([], $this->getInlineParams($token));
|
||||
|
||||
$continue = true;
|
||||
while ($continue) {
|
||||
// create subtree until the decideJsTagFork() callback returns true
|
||||
$body = $this->parser->subparse(array ($this, 'decideJsTagFork'));
|
||||
|
||||
// I like to put a switch here, in case you need to add middle tags, such
|
||||
// as: {% js %}, {% nextjs %}, {% endjs %}.
|
||||
$tag = $stream->next()->getValue();
|
||||
switch ($tag) {
|
||||
case 'endjs':
|
||||
$continue = false;
|
||||
break;
|
||||
default:
|
||||
throw new \Twig\Error\SyntaxError(sprintf('Unexpected end of template. Twig was looking for the following tags "endjs" to close the "mytag" block started at line %d)', $lineno), -1);
|
||||
}
|
||||
|
||||
// you want $body at the beginning of your arguments
|
||||
array_unshift($params, $body);
|
||||
|
||||
// if your endjs can also contains params, you can uncomment this line:
|
||||
// $params = array_merge($params, $this->getInlineParams($token));
|
||||
// and comment this one:
|
||||
$stream->expect(\Twig\Token::BLOCK_END_TYPE);
|
||||
}
|
||||
|
||||
return new JsTagNode(new \Twig\Node\Node($params), $lineno, $this->getTag());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recovers all tag parameters until we find a BLOCK_END_TYPE ( %} )
|
||||
*
|
||||
* @param \Twig\Token $token
|
||||
* @return array
|
||||
*/
|
||||
protected function getInlineParams(\Twig\Token $token) {
|
||||
$stream = $this->parser->getStream();
|
||||
$params = array ();
|
||||
while (!$stream->test(\Twig\Token::BLOCK_END_TYPE)) {
|
||||
$params[] = $this->parser->getExpressionParser()->parseExpression();
|
||||
}
|
||||
$stream->expect(\Twig\Token::BLOCK_END_TYPE);
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called at each tag name when subparsing, must return
|
||||
* true when the expected end tag is reached.
|
||||
*
|
||||
* @param \Twig\Token $token
|
||||
* @return bool
|
||||
*/
|
||||
public function decideJsTagFork(\Twig\Token $token) {
|
||||
return $token->test(['endjs']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Your tag name: if the parsed tag match the one you put here, your parse()
|
||||
* method will be called.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTag() {
|
||||
return 'js';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class JsTagNode extends \Twig\Node\Node {
|
||||
|
||||
public function __construct($params, $lineno = 0, $tag = null) {
|
||||
parent::__construct(['params' => $params], [], $lineno, $tag);
|
||||
}
|
||||
|
||||
public function compile(\Twig\Compiler $compiler) {
|
||||
$count = count($this->getNode('params'));
|
||||
|
||||
$compiler->addDebugInfo($this);
|
||||
$compiler
|
||||
->write('global $__tpl;')
|
||||
->raw(PHP_EOL);
|
||||
|
||||
for ($i = 0; ($i < $count); $i++) {
|
||||
// argument is not an expression (such as, a \Twig\Node\Textbody)
|
||||
// we should trick with output buffering to get a valid argument to pass
|
||||
// to the functionToCall() function.
|
||||
if (!($this->getNode('params')->getNode($i) instanceof \Twig\Node\Expression\AbstractExpression)) {
|
||||
$compiler
|
||||
->write('ob_start();')
|
||||
->raw(PHP_EOL);
|
||||
|
||||
$compiler
|
||||
->subcompile($this->getNode('params')->getNode($i));
|
||||
|
||||
$compiler
|
||||
->write('$js = ob_get_clean();')
|
||||
->raw(PHP_EOL);
|
||||
}
|
||||
}
|
||||
|
||||
$compiler
|
||||
->write('$__tpl->add_js($js);')
|
||||
->raw(PHP_EOL)
|
||||
->write('unset($js);')
|
||||
->raw(PHP_EOL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
*/
|
||||
function ajax_ok($data) {
|
||||
ajax_response(['response' => $data]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $error
|
||||
* @param int $code
|
||||
*/
|
||||
function ajax_error($error, $code = 200) {
|
||||
ajax_response(['error' => $error], $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @param int $code
|
||||
*/
|
||||
function ajax_response($data, $code = 200) {
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
http_response_code($code);
|
||||
echo jsonEncode($data);
|
||||
exit;
|
||||
}
|
255
localwebsite/functions.php
Normal file
255
localwebsite/functions.php
Normal file
@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
function param($key) {
|
||||
global $RouterInput;
|
||||
|
||||
$val = null;
|
||||
|
||||
if (isset($RouterInput[$key])) {
|
||||
$val = $RouterInput[$key];
|
||||
} else if (isset($_POST[$key])) {
|
||||
$val = $_POST[$key];
|
||||
} else if (isset($_GET[$key])) {
|
||||
$val = $_GET[$key];
|
||||
}
|
||||
|
||||
if (is_array($val)) {
|
||||
$val = implode($val);
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
function str_replace_once(string $needle, string $replace, string $haystack): string {
|
||||
$pos = strpos($haystack, $needle);
|
||||
if ($pos !== false) {
|
||||
$haystack = substr_replace($haystack, $replace, $pos, strlen($needle));
|
||||
}
|
||||
return $haystack;
|
||||
}
|
||||
|
||||
function htmlescape($s) {
|
||||
if (is_array($s)) {
|
||||
foreach ($s as $k => $v) {
|
||||
$s[$k] = htmlescape($v);
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function jsonEncode($obj) {
|
||||
return json_encode($obj, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
function jsonDecode($json) {
|
||||
return json_decode($json, true);
|
||||
}
|
||||
|
||||
function startsWith(string $haystack, string $needle): bool {
|
||||
return $needle === "" || strpos($haystack, $needle) === 0;
|
||||
}
|
||||
|
||||
function endsWith(string $haystack, string $needle): bool {
|
||||
return $needle === "" || substr($haystack, -strlen($needle)) === $needle;
|
||||
}
|
||||
|
||||
function exectime($format = null) {
|
||||
$time = round(microtime(true) - START_TIME, 4);
|
||||
if (!is_null($format)) {
|
||||
$time = sprintf($format, $time);
|
||||
}
|
||||
return $time;
|
||||
}
|
||||
|
||||
function stransi($s) {
|
||||
static $colors = [
|
||||
'black' => 0,
|
||||
'red' => 1,
|
||||
'green' => 2,
|
||||
'yellow' => 3,
|
||||
'blue' => 4,
|
||||
'magenta' => 5,
|
||||
'cyan' => 6,
|
||||
'white' => 7
|
||||
];
|
||||
static $valid_styles = ['bold', 'fgbright', 'bgbright'];
|
||||
|
||||
$s = preg_replace_callback('/<(?:e ([a-z, =]+)|\/e)>/', function($match) use ($colors, $valid_styles) {
|
||||
if (empty($match[1])) {
|
||||
return "\033[0m";
|
||||
} else {
|
||||
$codes = [];
|
||||
$args = preg_split('/ +/', $match[1]);
|
||||
$fg = null;
|
||||
$bg = null;
|
||||
$styles = [];
|
||||
foreach ($args as $arg) {
|
||||
list($argname, $argvalue) = explode('=', $arg);
|
||||
$err = false;
|
||||
if ($argname == 'fg' || $argname == 'bg') {
|
||||
if (isset($colors[$argvalue])) {
|
||||
$$argname = $colors[$argvalue];
|
||||
} else {
|
||||
$err = true;
|
||||
}
|
||||
} else if ($argname == 'style') {
|
||||
$argstyles = array_filter(explode(',', $argvalue));
|
||||
foreach ($argstyles as $style) {
|
||||
if (!in_array($style, $valid_styles)) {
|
||||
$err = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$err) {
|
||||
foreach ($argstyles as $style) {
|
||||
$styles[$style] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$err = true;
|
||||
}
|
||||
|
||||
if ($err) {
|
||||
trigger_error(__FUNCTION__.": unrecognized argument {$arg}", E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_null($fg)) {
|
||||
$codes[] = $fg + (isset($styles['fgbright']) ? 90 : 30);
|
||||
}
|
||||
if (!is_null($bg)) {
|
||||
$codes[] = $bg + (isset($styles['bgbright']) ? 100 : 40);
|
||||
}
|
||||
if (isset($styles['bold'])) {
|
||||
$codes[] = 1;
|
||||
}
|
||||
|
||||
return !empty($codes) ? "\033[".implode(';', $codes)."m" : '';
|
||||
}
|
||||
}, $s);
|
||||
return $s;
|
||||
}
|
||||
|
||||
function strgen($len = 10): string {
|
||||
$buf = '';
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$j = mt_rand(0, 61);
|
||||
if ($j >= 36) {
|
||||
$j += 13;
|
||||
} else if ($j >= 10) {
|
||||
$j += 7;
|
||||
}
|
||||
$buf .= chr(48 + $j);
|
||||
}
|
||||
return $buf;
|
||||
}
|
||||
|
||||
function setperm($file, $is_dir = null) {
|
||||
global $config;
|
||||
|
||||
// chgrp
|
||||
$gid = filegroup($file);
|
||||
$gname = posix_getgrgid($gid);
|
||||
if (!is_array($gname)) {
|
||||
debugError(__FUNCTION__.": posix_getgrgid() failed on $gid", $gname);
|
||||
} else {
|
||||
$gname = $gname['name'];
|
||||
}
|
||||
if ($gname != $config['group']) {
|
||||
if (!chgrp($file, $config['group'])) {
|
||||
debugError(__FUNCTION__.": chgrp() failed on $file");
|
||||
}
|
||||
}
|
||||
|
||||
// chmod
|
||||
$perms = fileperms($file);
|
||||
$need_perms = is_dir($file) ? $config['dirs_mode'] : $config['files_mode'];
|
||||
if (($perms & $need_perms) !== $need_perms) {
|
||||
if (!chmod($file, $need_perms)) {
|
||||
debugError(__FUNCTION__.": chmod() failed on $file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function redirect($url, $preserve_utm = true, $no_ajax = false) {
|
||||
if (PHP_SAPI != 'cli' && $_SERVER['REQUEST_METHOD'] == 'GET' && $preserve_utm) {
|
||||
$proxy_params = ['utm_source', 'utm_medium', 'utm_content', 'utm_campaign'];
|
||||
$params = [];
|
||||
foreach ($proxy_params as $p) {
|
||||
if (!empty($_GET[$p])) {
|
||||
$params[$p] = (string)$_GET[$p];
|
||||
}
|
||||
}
|
||||
if (!empty($params)) {
|
||||
if (($anchor_pos = strpos($url, '#')) !== false) {
|
||||
$anchor = substr($url, $anchor_pos+1);
|
||||
$url = substr($url, 0, $anchor_pos);
|
||||
}
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&').http_build_query($params);
|
||||
if ($anchor_pos !== false) {
|
||||
$url .= '#'.$anchor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: ' . $url);
|
||||
exit;
|
||||
}
|
||||
|
||||
function is_xhr_request(): bool {
|
||||
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest';
|
||||
}
|
||||
|
||||
function secondsToTime(int $n): string {
|
||||
$parts = [];
|
||||
|
||||
if ($n >= 86400) {
|
||||
$days = floor($n / 86400);
|
||||
$n %= 86400;
|
||||
$parts[] = "{$days}д";
|
||||
}
|
||||
|
||||
if ($n >= 3600) {
|
||||
$hours = floor($n / 3600);
|
||||
$n %= 3600;
|
||||
$parts[] = "{$hours}ч";
|
||||
}
|
||||
|
||||
if ($n >= 60) {
|
||||
$minutes = floor($n / 60);
|
||||
$n %= 60;
|
||||
$parts[] = "{$minutes}мин";
|
||||
}
|
||||
|
||||
if ($n)
|
||||
$parts[] = "{$n}сек";
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
function bytesToUnitsLabel(GMP $b): string {
|
||||
$ks = array('B', 'Kb', 'Mb', 'Gb', 'Tb');
|
||||
foreach ($ks as $i => $k) {
|
||||
if (gmp_cmp($b, gmp_pow(1024, $i + 1)) < 0) {
|
||||
if ($i == 0)
|
||||
return gmp_strval($b) . ' ' . $k;
|
||||
|
||||
$n = gmp_intval(gmp_div_q($b, gmp_pow(1024, $i)));
|
||||
return round($n, 2).' '.$k;
|
||||
}
|
||||
}
|
||||
|
||||
return gmp_strval($b);
|
||||
}
|
||||
|
||||
$ShutdownFunctions = [];
|
||||
|
||||
function append_shutdown_function(callable $f) {
|
||||
global $ShutdownFunctions;
|
||||
$ShutdownFunctions[] = $f;
|
||||
}
|
||||
|
||||
function prepend_shutdown_function(callable $f) {
|
||||
global $ShutdownFunctions;
|
||||
array_unshift($ShutdownFunctions, $f);
|
||||
}
|
20
localwebsite/handlers/FakeRequestHandler.php
Normal file
20
localwebsite/handlers/FakeRequestHandler.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
class FakeRequestHandler extends RequestHandler {
|
||||
|
||||
public function apacheNotFound() {
|
||||
http_response_code(404);
|
||||
$uri = htmlspecialchars($_SERVER['REQUEST_URI']);
|
||||
echo <<<EOF
|
||||
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
||||
<html><head>
|
||||
<title>404 Not Found</title>
|
||||
</head><body>
|
||||
<h1>Not Found</h1>
|
||||
<p>The requested URL {$uri} was not found on this server.</p>
|
||||
</body></html>
|
||||
EOF;
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
88
localwebsite/handlers/InverterHandler.php
Normal file
88
localwebsite/handlers/InverterHandler.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
class InverterHandler extends RequestHandler
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->tpl->add_static('inverter.js');
|
||||
}
|
||||
|
||||
public function GET_status_page()
|
||||
{
|
||||
global $config;
|
||||
$inv = new InverterdClient($config['inverterd_host'], $config['inverterd_port']);
|
||||
$inv->setFormat('json');
|
||||
$status = jsonDecode($inv->exec('get-status'))['data'];
|
||||
|
||||
$this->tpl->set([
|
||||
'status' => $status,
|
||||
'html' => $this->renderStatusHtml($status)
|
||||
]);
|
||||
$this->tpl->set_title('Инвертор');
|
||||
$this->tpl->render_page('inverter_page.twig');
|
||||
}
|
||||
|
||||
public function GET_status_ajax() {
|
||||
global $config;
|
||||
$inv = new InverterdClient($config['inverterd_host'], $config['inverterd_port']);
|
||||
$inv->setFormat('json');
|
||||
$status = jsonDecode($inv->exec('get-status'))['data'];
|
||||
ajax_ok(['html' => $this->renderStatusHtml($status)]);
|
||||
}
|
||||
|
||||
protected function renderStatusHtml(array $status)
|
||||
{
|
||||
$power_direction = strtolower($status['battery_power_direction']);
|
||||
$power_direction = preg_replace('/ge$/', 'ging', $power_direction);
|
||||
|
||||
$charging_rate = '';
|
||||
if ($power_direction == 'charging')
|
||||
$charging_rate = sprintf(' @ %s %s',
|
||||
$status['battery_charging_current']['value'],
|
||||
$status['battery_charging_current']['unit']);
|
||||
else if ($power_direction == 'discharging')
|
||||
$charging_rate = sprintf(' @ %s %s',
|
||||
$status['battery_discharging_current']['value'],
|
||||
$status['battery_discharging_current']['unit']);
|
||||
|
||||
$html = sprintf('<b>Battery:</b> %s %s',
|
||||
$status['battery_voltage']['value'],
|
||||
$status['battery_voltage']['unit']);
|
||||
$html .= sprintf(' (%s%s, ',
|
||||
$status['battery_capacity']['value'],
|
||||
$status['battery_capacity']['unit']);
|
||||
$html .= sprintf('%s%s)',
|
||||
$power_direction,
|
||||
$charging_rate);
|
||||
|
||||
$html .= "\n".sprintf('<b>Load:</b> %s %s',
|
||||
$status['ac_output_active_power']['value'],
|
||||
$status['ac_output_active_power']['unit']);
|
||||
$html .= sprintf(' (%s%%)',
|
||||
$status['output_load_percent']['value']);
|
||||
|
||||
if ($status['pv1_input_power']['value'] > 0)
|
||||
$html .= "\n".sprintf('<b>Input power:</b> %s %s',
|
||||
$status['pv1_input_power']['value'],
|
||||
$status['pv1_input_power']['unit']);
|
||||
|
||||
if ($status['grid_voltage']['value'] > 0 or $status['grid_freq']['value'] > 0) {
|
||||
$html .= "\n".sprintf('<b>Generator:</b> %s %s',
|
||||
$status['grid_voltage']['unit'],
|
||||
$status['grid_voltage']['value']);
|
||||
$html .= sprintf(', %s %s',
|
||||
$status['grid_freq']['value'],
|
||||
$status['grid_freq']['unit']);
|
||||
}
|
||||
|
||||
return nl2br($html);
|
||||
}
|
||||
|
||||
public function GET_status_page_update()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
52
localwebsite/handlers/MiscHandler.php
Normal file
52
localwebsite/handlers/MiscHandler.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
class MiscHandler extends RequestHandler
|
||||
{
|
||||
|
||||
public function GET_main() {
|
||||
$this->tpl->set_title('Главная');
|
||||
$this->tpl->render_page('index.twig');
|
||||
}
|
||||
|
||||
public function GET_phpinfo() {
|
||||
phpinfo();
|
||||
exit;
|
||||
}
|
||||
|
||||
public function GET_sensors_page() {
|
||||
global $config;
|
||||
|
||||
$clients = [];
|
||||
foreach ($config['si7021d_servers'] as $key => $params) {
|
||||
$cl = new Si7021dClient(...$params);
|
||||
$clients[$key] = $cl;
|
||||
|
||||
$cl->readSensor();
|
||||
}
|
||||
|
||||
$this->tpl->set(['sensors' => $clients]);
|
||||
$this->tpl->set_title('Датчики');
|
||||
$this->tpl->render_page('sensors.twig');
|
||||
}
|
||||
|
||||
public function GET_pump_page() {
|
||||
global $config;
|
||||
|
||||
list($set) = $this->input('set');
|
||||
$client = new GPIORelaydClient($config['pump_host'], $config['pump_port']);
|
||||
|
||||
if ($set == GPIORelaydClient::STATUS_ON || $set == GPIORelaydClient::STATUS_OFF) {
|
||||
$client->setStatus($set);
|
||||
redirect('/pump/');
|
||||
}
|
||||
|
||||
$status = $client->getStatus();
|
||||
|
||||
$this->tpl->set([
|
||||
'status' => $status
|
||||
]);
|
||||
$this->tpl->set_title('Насос');
|
||||
$this->tpl->render_page('pump.twig');
|
||||
}
|
||||
|
||||
}
|
192
localwebsite/handlers/ModemHandler.php
Normal file
192
localwebsite/handlers/ModemHandler.php
Normal file
@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
class ModemHandler extends RequestHandler
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->tpl->add_static('modem.js');
|
||||
}
|
||||
|
||||
public function GET_status_page() {
|
||||
global $config;
|
||||
|
||||
$this->tpl->set([
|
||||
'modems' => $config['modems'],
|
||||
'js_modems' => array_keys($config['modems']),
|
||||
]);
|
||||
|
||||
$this->tpl->set_title('Состояние модемов');
|
||||
$this->tpl->render_page('modem_status_page.twig');
|
||||
}
|
||||
|
||||
public function GET_status_get_ajax() {
|
||||
global $config;
|
||||
list($id) = $this->input('id');
|
||||
if (!isset($config['modems'][$id]))
|
||||
ajax_error('invalid modem id: '.$id);
|
||||
|
||||
$modem_data = self::getModemData(
|
||||
$config['modems'][$id]['ip'],
|
||||
$config['modems'][$id]['legacy_token_auth']);
|
||||
|
||||
ajax_ok([
|
||||
'html' => $this->tpl->render('modem_data.twig', [
|
||||
'loading' => false,
|
||||
'modem_data' => $modem_data
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function GET_routing_smallhome_page() {
|
||||
global $config;
|
||||
|
||||
list($error) = $this->input('error');
|
||||
$upstream = self::getCurrentSmallHomeUpstream();
|
||||
|
||||
$current_upstream = [
|
||||
'key' => $upstream,
|
||||
'label' => $config['modems'][$upstream]['label']
|
||||
];
|
||||
|
||||
$this->tpl->set([
|
||||
'error' => $error,
|
||||
'current' => $current_upstream,
|
||||
'modems' => $config['modems'],
|
||||
]);
|
||||
$this->tpl->set_title('Маршрутизация');
|
||||
$this->tpl->render_page('routing_page.twig');
|
||||
}
|
||||
|
||||
public function GET_routing_smallhome_switch() {
|
||||
global $config;
|
||||
list($new_upstream) = $this->input('upstream');
|
||||
if (!isset($config['modems'][$new_upstream]))
|
||||
redirect('/routing/?error='.urlencode('invalid upstream'));
|
||||
|
||||
$current_upstream = self::getCurrentSmallHomeUpstream();
|
||||
if ($current_upstream != $new_upstream) {
|
||||
if ($current_upstream != $config['routing_default'])
|
||||
MyOpenWrtUtils::ipsetDel($current_upstream, $config['routing_smallhome_ip']);
|
||||
if ($new_upstream != $config['routing_default'])
|
||||
MyOpenWrtUtils::ipsetAdd($new_upstream, $config['routing_smallhome_ip']);
|
||||
}
|
||||
|
||||
redirect('/routing/');
|
||||
}
|
||||
|
||||
public function GET_routing_ipsets_page() {
|
||||
list($error) = $this->input('error');
|
||||
|
||||
$ip_sets = MyOpenWrtUtils::ipsetListAll();
|
||||
$this->tpl->set([
|
||||
'sets' => $ip_sets,
|
||||
'error' => $error
|
||||
]);
|
||||
$this->tpl->set_title('Маршрутизация: IP sets');
|
||||
$this->tpl->render_page('routing_ipsets_page.twig');
|
||||
}
|
||||
|
||||
public function GET_routing_ipsets_del() {
|
||||
list($set, $ip) = $this->input('set, ip');
|
||||
self::validateIpsetsInput($set, $ip);
|
||||
|
||||
$output = MyOpenWrtUtils::ipsetDel($set, $ip);
|
||||
|
||||
$url = '/routing/ipsets/';
|
||||
if ($output != '')
|
||||
$url .= '?error='.urlencode($output);
|
||||
redirect($url);
|
||||
}
|
||||
|
||||
public function POST_routing_ipsets_add() {
|
||||
list($set, $ip) = $this->input('set, ip');
|
||||
self::validateIpsetsInput($set, $ip);
|
||||
|
||||
$output = MyOpenWrtUtils::ipsetAdd($set, $ip);
|
||||
|
||||
$url = '/routing/ipsets/';
|
||||
if ($output != '')
|
||||
$url .= '?error='.urlencode($output);
|
||||
redirect($url);
|
||||
}
|
||||
|
||||
public function GET_routing_dhcp_page() {
|
||||
$leases = MyOpenWrtUtils::getDHCPLeases();
|
||||
$this->tpl->set([
|
||||
'leases' => $leases
|
||||
]);
|
||||
$this->tpl->set_title('Маршрутизация: DHCP');
|
||||
$this->tpl->render_page('routing_dhcp_page.twig');
|
||||
}
|
||||
|
||||
public function GET_sms_page() {
|
||||
global $config;
|
||||
|
||||
list($selected) = $this->input('modem');
|
||||
if (!$selected)
|
||||
$selected = array_key_first($config['modems']);
|
||||
|
||||
$cfg = $config['modems'][$selected];
|
||||
$e3372 = new E3372($cfg['ip'], $cfg['legacy_token_auth']);
|
||||
$messages = $e3372->getSMSList();
|
||||
|
||||
$this->tpl->set([
|
||||
'modems_list' => array_keys($config['modems']),
|
||||
'modems' => $config['modems'],
|
||||
'selected_modem' => $selected,
|
||||
'messages' => $messages
|
||||
]);
|
||||
$this->tpl->set_title('Модемы: SMS-сообщения');
|
||||
$this->tpl->render_page('sms_page.twig');
|
||||
}
|
||||
|
||||
protected static function getModemData(string $ip, bool $need_auth = true): array {
|
||||
$modem = new E3372($ip, $need_auth);
|
||||
$signal = $modem->getDeviceSignal();
|
||||
$status = $modem->getMonitoringStatus();
|
||||
$traffic = $modem->getTrafficStats();
|
||||
return [
|
||||
'type' => e3372::getNetworkTypeLabel($status['CurrentNetworkType']),
|
||||
'level' => $status['SignalIcon'] ?? 0,
|
||||
'rssi' => $signal['rssi'],
|
||||
'sinr' => $signal['sinr'],
|
||||
'connected_time' => secondsToTime($traffic['CurrentConnectTime']),
|
||||
'downloaded' => bytesToUnitsLabel(gmp_init($traffic['CurrentDownload'])),
|
||||
'uploaded' => bytesToUnitsLabel(gmp_init($traffic['CurrentUpload'])),
|
||||
];
|
||||
}
|
||||
|
||||
protected static function getCurrentSmallHomeUpstream() {
|
||||
global $config;
|
||||
|
||||
$upstream = null;
|
||||
$ip_sets = MyOpenWrtUtils::ipsetListAll();
|
||||
foreach ($ip_sets as $set => $ips) {
|
||||
if (in_array($config['routing_smallhome_ip'], $ips)) {
|
||||
$upstream = $set;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_null($upstream))
|
||||
$upstream = $config['routing_default'];
|
||||
|
||||
return $upstream;
|
||||
}
|
||||
|
||||
protected static function validateIpsetsInput($set, $ip) {
|
||||
global $config;
|
||||
|
||||
if (!isset($config['modems'][$set]))
|
||||
redirect('/routing/ipsets/?error='.urlencode('invalid set: '.$set));
|
||||
|
||||
if (($slashpos = strpos($ip, '/')) !== false)
|
||||
$ip = substr($ip, 0, $slashpos);
|
||||
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP))
|
||||
redirect('/routing/ipsets/?error='.urlencode('invalid ip/network: '.$ip));
|
||||
}
|
||||
|
||||
}
|
41
localwebsite/handlers/RequestHandler.php
Normal file
41
localwebsite/handlers/RequestHandler.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
class RequestHandler extends request_handler {
|
||||
|
||||
/** @var web_tpl*/
|
||||
protected $tpl;
|
||||
|
||||
public function __construct() {
|
||||
global $__tpl;
|
||||
$__tpl = new web_tpl();
|
||||
$this->tpl = $__tpl;
|
||||
|
||||
$this->tpl->add_static('bootstrap.min.css');
|
||||
$this->tpl->add_static('bootstrap.min.js');
|
||||
$this->tpl->add_static('polyfills.js');
|
||||
$this->tpl->add_static('app.js');
|
||||
$this->tpl->add_static('app.css');
|
||||
}
|
||||
|
||||
public function dispatch(string $act) {
|
||||
global $config;
|
||||
$this->tpl->set_global([
|
||||
'__dev' => $config['is_dev'],
|
||||
]);
|
||||
return parent::dispatch($act);
|
||||
}
|
||||
|
||||
protected function method_not_found(string $method, string $act)
|
||||
{
|
||||
global $config;
|
||||
|
||||
if ($act != '404' && $config['is_dev'])
|
||||
debugError(get_called_class() . ": act {$method}_{$act} not found.");
|
||||
|
||||
if (!is_xhr_request())
|
||||
$this->tpl->render_not_found();
|
||||
else
|
||||
ajax_error('unknown act "'.$act.'"', 404);
|
||||
|
||||
}
|
||||
}
|
153
localwebsite/htdocs/assets/app.css
Normal file
153
localwebsite/htdocs/assets/app.css
Normal file
@ -0,0 +1,153 @@
|
||||
.signal_level {
|
||||
display: inline-block;
|
||||
}
|
||||
.signal_level > div {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #ddd;
|
||||
border-radius: 50%;
|
||||
float: left;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.signal_level > div.yes {
|
||||
background-color: #5acc61;
|
||||
}
|
||||
|
||||
|
||||
/** spinner.twig **/
|
||||
|
||||
.sk-fading-circle {
|
||||
margin-top: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sk-fading-circle .sk-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sk-fading-circle .sk-circle:before {
|
||||
content: '';
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 15%;
|
||||
height: 15%;
|
||||
background-color: #555;
|
||||
border-radius: 100%;
|
||||
-webkit-animation: sk-circleFadeDelay 1.2s infinite ease-in-out both;
|
||||
animation: sk-circleFadeDelay 1.2s infinite ease-in-out both;
|
||||
}
|
||||
.sk-fading-circle .sk-circle2 {
|
||||
-webkit-transform: rotate(30deg);
|
||||
-ms-transform: rotate(30deg);
|
||||
transform: rotate(30deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle3 {
|
||||
-webkit-transform: rotate(60deg);
|
||||
-ms-transform: rotate(60deg);
|
||||
transform: rotate(60deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle4 {
|
||||
-webkit-transform: rotate(90deg);
|
||||
-ms-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle5 {
|
||||
-webkit-transform: rotate(120deg);
|
||||
-ms-transform: rotate(120deg);
|
||||
transform: rotate(120deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle6 {
|
||||
-webkit-transform: rotate(150deg);
|
||||
-ms-transform: rotate(150deg);
|
||||
transform: rotate(150deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle7 {
|
||||
-webkit-transform: rotate(180deg);
|
||||
-ms-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle8 {
|
||||
-webkit-transform: rotate(210deg);
|
||||
-ms-transform: rotate(210deg);
|
||||
transform: rotate(210deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle9 {
|
||||
-webkit-transform: rotate(240deg);
|
||||
-ms-transform: rotate(240deg);
|
||||
transform: rotate(240deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle10 {
|
||||
-webkit-transform: rotate(270deg);
|
||||
-ms-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle11 {
|
||||
-webkit-transform: rotate(300deg);
|
||||
-ms-transform: rotate(300deg);
|
||||
transform: rotate(300deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle12 {
|
||||
-webkit-transform: rotate(330deg);
|
||||
-ms-transform: rotate(330deg);
|
||||
transform: rotate(330deg);
|
||||
}
|
||||
.sk-fading-circle .sk-circle2:before {
|
||||
-webkit-animation-delay: -1.1s;
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
.sk-fading-circle .sk-circle3:before {
|
||||
-webkit-animation-delay: -1s;
|
||||
animation-delay: -1s;
|
||||
}
|
||||
.sk-fading-circle .sk-circle4:before {
|
||||
-webkit-animation-delay: -0.9s;
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
.sk-fading-circle .sk-circle5:before {
|
||||
-webkit-animation-delay: -0.8s;
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
.sk-fading-circle .sk-circle6:before {
|
||||
-webkit-animation-delay: -0.7s;
|
||||
animation-delay: -0.7s;
|
||||
}
|
||||
.sk-fading-circle .sk-circle7:before {
|
||||
-webkit-animation-delay: -0.6s;
|
||||
animation-delay: -0.6s;
|
||||
}
|
||||
.sk-fading-circle .sk-circle8:before {
|
||||
-webkit-animation-delay: -0.5s;
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.sk-fading-circle .sk-circle9:before {
|
||||
-webkit-animation-delay: -0.4s;
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
.sk-fading-circle .sk-circle10:before {
|
||||
-webkit-animation-delay: -0.3s;
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.sk-fading-circle .sk-circle11:before {
|
||||
-webkit-animation-delay: -0.2s;
|
||||
animation-delay: -0.2s;
|
||||
}
|
||||
.sk-fading-circle .sk-circle12:before {
|
||||
-webkit-animation-delay: -0.1s;
|
||||
animation-delay: -0.1s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-circleFadeDelay {
|
||||
0%, 39%, 100% { opacity: 0; }
|
||||
40% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes sk-circleFadeDelay {
|
||||
0%, 39%, 100% { opacity: 0; }
|
||||
40% { opacity: 1; }
|
||||
}
|
44
localwebsite/htdocs/assets/app.js
Normal file
44
localwebsite/htdocs/assets/app.js
Normal file
@ -0,0 +1,44 @@
|
||||
var ajax = {
|
||||
get: function(url, data) {
|
||||
if (typeof data == 'object') {
|
||||
var index = 0;
|
||||
for (var key in data) {
|
||||
var val = data[key];
|
||||
url += index === 0 && url.indexOf('?') === -1 ? '?' : '&';
|
||||
url += encodeURIComponent(key) + '=' + encodeURIComponent(val);
|
||||
}
|
||||
}
|
||||
return this.raw(url);
|
||||
},
|
||||
|
||||
post: function(url, body) {
|
||||
var opts = {
|
||||
method: 'POST'
|
||||
};
|
||||
if (body)
|
||||
opts.body = body;
|
||||
return this.raw(url, opts);
|
||||
},
|
||||
|
||||
raw: function(url, options) {
|
||||
if (!options)
|
||||
options = {}
|
||||
|
||||
return fetch(url, Object.assign({
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
}, options))
|
||||
.then(resp => {
|
||||
return resp.json()
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
function extend(a, b) {
|
||||
return Object.assign(a, b);
|
||||
}
|
||||
|
||||
function ge(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
7
localwebsite/htdocs/assets/bootstrap.min.css
vendored
Normal file
7
localwebsite/htdocs/assets/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
localwebsite/htdocs/assets/bootstrap.min.js
vendored
Normal file
7
localwebsite/htdocs/assets/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
15
localwebsite/htdocs/assets/inverter.js
Normal file
15
localwebsite/htdocs/assets/inverter.js
Normal file
@ -0,0 +1,15 @@
|
||||
var Inverter = {
|
||||
poll: function () {
|
||||
setInterval(this._tick, 1000);
|
||||
},
|
||||
|
||||
_tick: function() {
|
||||
ajax.get('/inverter/status.ajax')
|
||||
.then(({response}) => {
|
||||
if (response) {
|
||||
var el = document.getElementById('inverter_status');
|
||||
el.innerHTML = response.html;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
29
localwebsite/htdocs/assets/modem.js
Normal file
29
localwebsite/htdocs/assets/modem.js
Normal file
@ -0,0 +1,29 @@
|
||||
var ModemStatus = {
|
||||
_modems: [],
|
||||
|
||||
init: function(modems) {
|
||||
for (var i = 0; i < modems.length; i++) {
|
||||
var modem = modems[i];
|
||||
this._modems.push(new ModemStatusUpdater(modem));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function ModemStatusUpdater(id) {
|
||||
this.id = id;
|
||||
this.elem = ge('modem_data_'+id);
|
||||
this.fetch();
|
||||
}
|
||||
extend(ModemStatusUpdater.prototype, {
|
||||
fetch: function() {
|
||||
ajax.get('/modem/status/get.ajax', {
|
||||
id: this.id
|
||||
}).then(({response}) => {
|
||||
var {html} = response;
|
||||
this.elem.innerHTML = html;
|
||||
|
||||
// TODO enqueue rerender
|
||||
});
|
||||
},
|
||||
});
|
560
localwebsite/htdocs/assets/polyfills.js
Normal file
560
localwebsite/htdocs/assets/polyfills.js
Normal file
@ -0,0 +1,560 @@
|
||||
if (typeof Object.assign != 'function') {
|
||||
// Must be writable: true, enumerable: false, configurable: true
|
||||
Object.defineProperty(Object, "assign", {
|
||||
value: function assign(target, varArgs) { // .length of function is 2
|
||||
'use strict';
|
||||
if (target == null) { // TypeError if undefined or null
|
||||
throw new TypeError('Cannot convert undefined or null to object');
|
||||
}
|
||||
|
||||
var to = Object(target);
|
||||
|
||||
for (var index = 1; index < arguments.length; index++) {
|
||||
var nextSource = arguments[index];
|
||||
|
||||
if (nextSource != null) { // Skip over if undefined or null
|
||||
for (var nextKey in nextSource) {
|
||||
// Avoid bugs when hasOwnProperty is shadowed
|
||||
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
|
||||
to[nextKey] = nextSource[nextKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return to;
|
||||
},
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
|
||||
if (!Object.keys) {
|
||||
Object.keys = (function() {
|
||||
'use strict';
|
||||
var hasOwnProperty = Object.prototype.hasOwnProperty,
|
||||
hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'),
|
||||
dontEnums = [
|
||||
'toString',
|
||||
'toLocaleString',
|
||||
'valueOf',
|
||||
'hasOwnProperty',
|
||||
'isPrototypeOf',
|
||||
'propertyIsEnumerable',
|
||||
'constructor'
|
||||
],
|
||||
dontEnumsLength = dontEnums.length;
|
||||
|
||||
return function(obj) {
|
||||
if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
|
||||
throw new TypeError('Object.keys called on non-object');
|
||||
}
|
||||
|
||||
var result = [], prop, i;
|
||||
|
||||
for (prop in obj) {
|
||||
if (hasOwnProperty.call(obj, prop)) {
|
||||
result.push(prop);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDontEnumBug) {
|
||||
for (i = 0; i < dontEnumsLength; i++) {
|
||||
if (hasOwnProperty.call(obj, dontEnums[i])) {
|
||||
result.push(dontEnums[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}());
|
||||
}
|
||||
|
||||
// const PromisePolyfill = require('es6-promise').Promise;
|
||||
// if (!window.Promise) {
|
||||
// window.Promise = PromisePolyfill
|
||||
// }
|
||||
|
||||
// https://tc39.github.io/ecma262/#sec-array.prototype.find
|
||||
if (!Array.prototype.find) {
|
||||
Object.defineProperty(Array.prototype, 'find', {
|
||||
value: function(predicate) {
|
||||
// 1. Let O be ? ToObject(this value).
|
||||
if (this == null) {
|
||||
throw TypeError('"this" is null or not defined');
|
||||
}
|
||||
|
||||
var o = Object(this);
|
||||
|
||||
// 2. Let len be ? ToLength(? Get(O, "length")).
|
||||
var len = o.length >>> 0;
|
||||
|
||||
// 3. If IsCallable(predicate) is false, throw a TypeError exception.
|
||||
if (typeof predicate !== 'function') {
|
||||
throw TypeError('predicate must be a function');
|
||||
}
|
||||
|
||||
// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
var thisArg = arguments[1];
|
||||
|
||||
// 5. Let k be 0.
|
||||
var k = 0;
|
||||
|
||||
// 6. Repeat, while k < len
|
||||
while (k < len) {
|
||||
// a. Let Pk be ! ToString(k).
|
||||
// b. Let kValue be ? Get(O, Pk).
|
||||
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
|
||||
// d. If testResult is true, return kValue.
|
||||
var kValue = o[k];
|
||||
if (predicate.call(thisArg, kValue, k, o)) {
|
||||
return kValue;
|
||||
}
|
||||
// e. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
|
||||
// 7. Return undefined.
|
||||
return undefined;
|
||||
},
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.prototype.findIndex) {
|
||||
Array.prototype.findIndex = function(predicate) {
|
||||
if (this == null) {
|
||||
throw new TypeError('Array.prototype.findIndex called on null or undefined');
|
||||
}
|
||||
if (typeof predicate !== 'function') {
|
||||
throw new TypeError('predicate must be a function');
|
||||
}
|
||||
var list = Object(this);
|
||||
var length = list.length >>> 0;
|
||||
var thisArg = arguments[1];
|
||||
var value;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
value = list[i];
|
||||
if (predicate.call(thisArg, value, i, list)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
|
||||
if (!Array.prototype.filter) {
|
||||
Array.prototype.filter = function(fun/*, thisArg*/) {
|
||||
'use strict';
|
||||
|
||||
if (this === void 0 || this === null) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
var t = Object(this);
|
||||
var len = t.length >>> 0;
|
||||
if (typeof fun !== 'function') {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
var res = [];
|
||||
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (i in t) {
|
||||
var val = t[i];
|
||||
|
||||
// NOTE: Technically this should Object.defineProperty at
|
||||
// the next index, as push can be affected by
|
||||
// properties on Object.prototype and Array.prototype.
|
||||
// But that method's new, and collisions should be
|
||||
// rare, so use the more-compatible alternative.
|
||||
if (fun.call(thisArg, val, i, t)) {
|
||||
res.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
if (!String.prototype.trim) {
|
||||
String.prototype.trim = function () {
|
||||
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
||||
};
|
||||
}
|
||||
|
||||
if (!Function.prototype.bind) {
|
||||
Function.prototype.bind = function (oThis) {
|
||||
if (typeof this !== "function") {
|
||||
// closest thing possible to the ECMAScript 5 internal IsCallable function
|
||||
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
|
||||
}
|
||||
|
||||
var aArgs = Array.prototype.slice.call(arguments, 1),
|
||||
fToBind = this,
|
||||
fNOP = function () {},
|
||||
fBound = function () {
|
||||
return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
|
||||
};
|
||||
|
||||
fNOP.prototype = this.prototype;
|
||||
fBound.prototype = new fNOP();
|
||||
|
||||
return fBound;
|
||||
};
|
||||
}
|
||||
|
||||
Function.prototype.pbind = function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
args.unshift(window);
|
||||
return this.bind.apply(this, args);
|
||||
};
|
||||
|
||||
// Fix for Samsung Browser
|
||||
if (!Function.prototype.ToString) {
|
||||
Function.prototype.ToString = function () {
|
||||
return this.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (!String.prototype.startsWith) {
|
||||
String.prototype.startsWith = function(searchString, position){
|
||||
position = position || 0;
|
||||
return this.substr(position, searchString.length) === searchString;
|
||||
};
|
||||
}
|
||||
|
||||
if (!String.prototype.endsWith) {
|
||||
String.prototype.endsWith = function(searchString, position) {
|
||||
var subjectString = this.toString();
|
||||
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
|
||||
position = subjectString.length;
|
||||
}
|
||||
position -= searchString.length;
|
||||
var lastIndex = subjectString.lastIndexOf(searchString, position);
|
||||
return lastIndex !== -1 && lastIndex === position;
|
||||
};
|
||||
}
|
||||
|
||||
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
|
||||
if (!Array.prototype.includes) {
|
||||
Object.defineProperty(Array.prototype, 'includes', {
|
||||
value: function(searchElement, fromIndex) {
|
||||
|
||||
// 1. Let O be ? ToObject(this value).
|
||||
if (this == null) {
|
||||
throw new TypeError('"this" is null or not defined');
|
||||
}
|
||||
|
||||
var o = Object(this);
|
||||
|
||||
// 2. Let len be ? ToLength(? Get(O, "length")).
|
||||
var len = o.length >>> 0;
|
||||
|
||||
// 3. If len is 0, return false.
|
||||
if (len === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. Let n be ? ToInteger(fromIndex).
|
||||
// (If fromIndex is undefined, this step produces the value 0.)
|
||||
var n = fromIndex | 0;
|
||||
|
||||
// 5. If n ≥ 0, then
|
||||
// a. Let k be n.
|
||||
// 6. Else n < 0,
|
||||
// a. Let k be len + n.
|
||||
// b. If k < 0, let k be 0.
|
||||
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
|
||||
|
||||
// 7. Repeat, while k < len
|
||||
while (k < len) {
|
||||
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
|
||||
// b. If SameValueZero(searchElement, elementK) is true, return true.
|
||||
// c. Increase k by 1.
|
||||
// NOTE: === provides the correct "SameValueZero" comparison needed here.
|
||||
if (o[k] === searchElement) {
|
||||
return true;
|
||||
}
|
||||
k++;
|
||||
}
|
||||
|
||||
// 8. Return false
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Шаги алгоритма ECMA-262, 5-е издание, 15.4.4.21
|
||||
// Ссылка (en): http://es5.github.io/#x15.4.4.21
|
||||
// Ссылка (ru): http://es5.javascript.ru/x15.4.html#x15.4.4.21
|
||||
if (!Array.prototype.reduce) {
|
||||
Array.prototype.reduce = function(callback/*, initialValue*/) {
|
||||
'use strict';
|
||||
if (this == null) {
|
||||
throw new TypeError('Array.prototype.reduce called on null or undefined');
|
||||
}
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
var t = Object(this), len = t.length >>> 0, k = 0, value;
|
||||
if (arguments.length >= 2) {
|
||||
value = arguments[1];
|
||||
} else {
|
||||
while (k < len && ! (k in t)) {
|
||||
k++;
|
||||
}
|
||||
if (k >= len) {
|
||||
throw new TypeError('Reduce of empty array with no initial value');
|
||||
}
|
||||
value = t[k++];
|
||||
}
|
||||
for (; k < len; k++) {
|
||||
if (k in t) {
|
||||
value = callback(value, t[k], k, t);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Array.prototype.pushOnce = function(value) {
|
||||
if (!this.includes(value)) {
|
||||
this.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
Array.prototype.removeOnce = function(value) {
|
||||
let index = this.indexOf(value)
|
||||
if (index !== -1) {
|
||||
this.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Production steps of ECMA-262, Edition 5, 15.4.4.18
|
||||
// Reference: http://es5.github.io/#x15.4.4.18
|
||||
if (!Array.prototype.forEach) {
|
||||
|
||||
Array.prototype.forEach = function(callback/*, thisArg*/) {
|
||||
|
||||
var T, k;
|
||||
|
||||
if (this == null) {
|
||||
throw new TypeError('this is null or not defined');
|
||||
}
|
||||
|
||||
// 1. Let O be the result of calling toObject() passing the
|
||||
// |this| value as the argument.
|
||||
var O = Object(this);
|
||||
|
||||
// 2. Let lenValue be the result of calling the Get() internal
|
||||
// method of O with the argument "length".
|
||||
// 3. Let len be toUint32(lenValue).
|
||||
var len = O.length >>> 0;
|
||||
|
||||
// 4. If isCallable(callback) is false, throw a TypeError exception.
|
||||
// See: http://es5.github.com/#x9.11
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let
|
||||
// T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = arguments[1];
|
||||
}
|
||||
|
||||
// 6. Let k be 0.
|
||||
k = 0;
|
||||
|
||||
// 7. Repeat while k < len.
|
||||
while (k < len) {
|
||||
|
||||
var kValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator.
|
||||
// b. Let kPresent be the result of calling the HasProperty
|
||||
// internal method of O with argument Pk.
|
||||
// This step can be combined with c.
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
|
||||
// i. Let kValue be the result of calling the Get internal
|
||||
// method of O with argument Pk.
|
||||
kValue = O[k];
|
||||
|
||||
// ii. Call the Call internal method of callback with T as
|
||||
// the this value and argument list containing kValue, k, and O.
|
||||
callback.call(T, kValue, k, O);
|
||||
}
|
||||
// d. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
// 8. return undefined.
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (!Array.isArray) {
|
||||
Array.isArray = function(arg) {
|
||||
return Object.prototype.toString.call(arg) === '[object Array]';
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Production steps of ECMA-262, Edition 6, 22.1.2.1
|
||||
if (!Array.from) {
|
||||
Array.from = (function () {
|
||||
var toStr = Object.prototype.toString;
|
||||
var isCallable = function (fn) {
|
||||
return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
|
||||
};
|
||||
var toInteger = function (value) {
|
||||
var number = Number(value);
|
||||
if (isNaN(number)) { return 0; }
|
||||
if (number === 0 || !isFinite(number)) { return number; }
|
||||
return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
|
||||
};
|
||||
var maxSafeInteger = Math.pow(2, 53) - 1;
|
||||
var toLength = function (value) {
|
||||
var len = toInteger(value);
|
||||
return Math.min(Math.max(len, 0), maxSafeInteger);
|
||||
};
|
||||
|
||||
// The length property of the from method is 1.
|
||||
return function from(arrayLike/*, mapFn, thisArg */) {
|
||||
// 1. Let C be the this value.
|
||||
var C = this;
|
||||
|
||||
// 2. Let items be ToObject(arrayLike).
|
||||
var items = Object(arrayLike);
|
||||
|
||||
// 3. ReturnIfAbrupt(items).
|
||||
if (arrayLike == null) {
|
||||
throw new TypeError('Array.from requires an array-like object - not null or undefined');
|
||||
}
|
||||
|
||||
// 4. If mapfn is undefined, then let mapping be false.
|
||||
var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
|
||||
var T;
|
||||
if (typeof mapFn !== 'undefined') {
|
||||
// 5. else
|
||||
// 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
|
||||
if (!isCallable(mapFn)) {
|
||||
throw new TypeError('Array.from: when provided, the second argument must be a function');
|
||||
}
|
||||
|
||||
// 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 2) {
|
||||
T = arguments[2];
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Let lenValue be Get(items, "length").
|
||||
// 11. Let len be ToLength(lenValue).
|
||||
var len = toLength(items.length);
|
||||
|
||||
// 13. If IsConstructor(C) is true, then
|
||||
// 13. a. Let A be the result of calling the [[Construct]] internal method
|
||||
// of C with an argument list containing the single item len.
|
||||
// 14. a. Else, Let A be ArrayCreate(len).
|
||||
var A = isCallable(C) ? Object(new C(len)) : new Array(len);
|
||||
|
||||
// 16. Let k be 0.
|
||||
var k = 0;
|
||||
// 17. Repeat, while k < len… (also steps a - h)
|
||||
var kValue;
|
||||
while (k < len) {
|
||||
kValue = items[k];
|
||||
if (mapFn) {
|
||||
A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
|
||||
} else {
|
||||
A[k] = kValue;
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
// 18. Let putStatus be Put(A, "length", len, true).
|
||||
A.length = len;
|
||||
// 20. Return A.
|
||||
return A;
|
||||
};
|
||||
}());
|
||||
}
|
||||
|
||||
// Production steps of ECMA-262, Edition 5, 15.4.4.17
|
||||
// Reference: https://es5.github.io/#x15.4.4.17
|
||||
if (!Array.prototype.some) {
|
||||
Array.prototype.some = function(fun, thisArg) {
|
||||
'use strict';
|
||||
|
||||
if (this == null) {
|
||||
throw new TypeError('Array.prototype.some called on null or undefined');
|
||||
}
|
||||
|
||||
if (typeof fun !== 'function') {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
var t = Object(this);
|
||||
var len = t.length >>> 0;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (i in t && fun.call(thisArg, t[i], i, t)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* String.padEnd()
|
||||
* version 1.0.1
|
||||
* Feature Chrome Firefox Internet Explorer Opera Safari Edge
|
||||
* Basic support 57 48 (No) 44 10 15
|
||||
* -------------------------------------------------------------------------------
|
||||
*/
|
||||
if (!String.prototype.padEnd) {
|
||||
String.prototype.padEnd = function padEnd(targetLength, padString) {
|
||||
targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
|
||||
padString = String(typeof padString !== 'undefined' ? padString : ' ');
|
||||
if (this.length > targetLength) {
|
||||
return String(this);
|
||||
} else {
|
||||
targetLength = targetLength - this.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
|
||||
}
|
||||
return String(this) + padString.slice(0, targetLength);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* String.padStart()
|
||||
* version 1.0.1
|
||||
* Feature Chrome Firefox Internet Explorer Opera Safari Edge
|
||||
* Basic support 57 51 (No) 44 10 15
|
||||
* -------------------------------------------------------------------------------
|
||||
*/
|
||||
if (!String.prototype.padStart) {
|
||||
String.prototype.padStart = function padStart(targetLength, padString) {
|
||||
targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
|
||||
padString = String(typeof padString !== 'undefined' ? padString : ' ');
|
||||
if (this.length > targetLength) {
|
||||
return String(this);
|
||||
} else {
|
||||
targetLength = targetLength - this.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
|
||||
}
|
||||
return padString.slice(0, targetLength) + String(this);
|
||||
}
|
||||
};
|
||||
}
|
BIN
localwebsite/htdocs/favicon.ico
Normal file
BIN
localwebsite/htdocs/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
44
localwebsite/htdocs/index.php
Normal file
44
localwebsite/htdocs/index.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/../init.php';
|
||||
|
||||
global $lang;
|
||||
|
||||
$router = new router;
|
||||
|
||||
// modem
|
||||
$router->add('modem/status/', 'Modem status_page');
|
||||
$router->add('modem/status/get.ajax', 'Modem status_get_ajax');
|
||||
|
||||
$router->add('routing/', 'Modem routing_smallhome_page');
|
||||
$router->add('routing/switch-small-home/', 'Modem routing_smallhome_switch');
|
||||
$router->add('routing/{ipsets,dhcp}/', 'Modem routing_${1}_page');
|
||||
$router->add('routing/ipsets/{add,del}/', 'Modem routing_ipsets_${1}');
|
||||
|
||||
$router->add('modem/sms/', 'Modem sms_page');
|
||||
// $router->add('modem/set.ajax', 'Modem ctl_set_ajax');
|
||||
|
||||
// inverter
|
||||
$router->add('inverter/', 'Inverter status_page');
|
||||
$router->add('inverter/status.ajax', 'Inverter status_ajax');
|
||||
|
||||
// misc
|
||||
$router->add('/', 'Misc main');
|
||||
$router->add('sensors/', 'Misc sensors_page');
|
||||
$router->add('pump/', 'Misc pump_page');
|
||||
$router->add('phpinfo/', 'Misc phpinfo');
|
||||
|
||||
|
||||
$route = routerFind($router);
|
||||
if ($route === false)
|
||||
(new FakeRequestHandler)->dispatch('404');
|
||||
|
||||
list($handler, $act, $RouterInput) = $route;
|
||||
|
||||
$handler_class = $handler.'Handler';
|
||||
if (!class_exists($handler_class)) {
|
||||
debugError('index.php: class '.$handler_class.' not found');
|
||||
(new FakeRequestHandler)->dispatch('404');
|
||||
}
|
||||
|
||||
(new $handler_class)->dispatch($act);
|
71
localwebsite/init.php
Normal file
71
localwebsite/init.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
mb_internal_encoding('UTF-8');
|
||||
mb_regex_encoding('UTF-8');
|
||||
|
||||
register_shutdown_function(function() {
|
||||
global $ShutdownFunctions;
|
||||
if (!empty($ShutdownFunctions)) {
|
||||
foreach ($ShutdownFunctions as $f)
|
||||
$f();
|
||||
}
|
||||
});
|
||||
|
||||
spl_autoload_register(function($class) {
|
||||
if (endsWith($class, 'Handler'))
|
||||
$path = ROOT.'/handlers/'.$class.'.php';
|
||||
|
||||
// engine classes
|
||||
else if (in_array($class, ['request_handler', 'router', 'model', 'debug']))
|
||||
$path = ROOT.'/engine/'.$class.'.php';
|
||||
|
||||
else if ($class == 'Lang')
|
||||
$path = ROOT.'/engine/lang.php';
|
||||
|
||||
else if (endsWith($class, '_tpl'))
|
||||
$path = ROOT.'/engine/tpl.php';
|
||||
|
||||
// other classes
|
||||
else
|
||||
$path = ROOT.'/classes/'.$class.'.php';
|
||||
|
||||
if (strpos($path, '\\') !== false)
|
||||
$path = str_replace('\\', '/', $path);
|
||||
|
||||
if (is_file($path))
|
||||
require_once $path;
|
||||
});
|
||||
|
||||
define('ROOT', __DIR__);
|
||||
define('START_TIME', microtime(true));
|
||||
|
||||
set_include_path(get_include_path().PATH_SEPARATOR.ROOT);
|
||||
|
||||
$config = require ROOT.'/config.php';
|
||||
if (!is_file(ROOT.'/config.local.php'))
|
||||
die('config.local.php not found');
|
||||
$config = array_merge($config, require_once ROOT.'/config.local.php');
|
||||
|
||||
require_once ROOT.'/functions.php';
|
||||
|
||||
// it's better to start logging as early as possible
|
||||
$debug = debug::getInstance(
|
||||
function($errno, $errfile, $errlne, $errstr) {
|
||||
// it's not our fault that some vendor package uses something that's deprecated
|
||||
// so let's not spam our logs
|
||||
if ($errno == E_USER_DEPRECATED && startsWith($errfile, ROOT.'/vendor/'))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
$debug->setMessagesStoreType(debug::STORE_FILE);
|
||||
$debug->setErrorsStoreType(debug::STORE_FILE);
|
||||
$debug->enable();
|
||||
unset($debug);
|
||||
|
||||
// composer
|
||||
require_once ROOT.'/../vendor/autoload.php';
|
1
localwebsite/templates-web/404.twig
Normal file
1
localwebsite/templates-web/404.twig
Normal file
@ -0,0 +1 @@
|
||||
Page Not Found
|
8
localwebsite/templates-web/footer.twig
Normal file
8
localwebsite/templates-web/footer.twig
Normal file
@ -0,0 +1,8 @@
|
||||
{% if js %}
|
||||
<script>{{ js|raw }}</script>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!-- generated in {{ exec_time}} -->
|
10
localwebsite/templates-web/header.twig
Normal file
10
localwebsite/templates-web/header.twig
Normal file
@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
{{ static|raw }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-3">
|
21
localwebsite/templates-web/index.twig
Normal file
21
localwebsite/templates-web/index.twig
Normal file
@ -0,0 +1,21 @@
|
||||
<div class="container py-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item active" aria-current="page">Главная</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h6>Интернет</h6>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item"><a href="/modem/status/">Состояние</a></li>
|
||||
<li class="list-group-item"><a href="/routing/">Маршрутизация</a></li>
|
||||
<li class="list-group-item"><a href="/modem/sms/">SMS-сообщения</a></li>
|
||||
</ul>
|
||||
|
||||
<h6 class="mt-4">Другое</h6>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item"><a href="/inverter/">Инвертор</a></li>
|
||||
<li class="list-group-item"><a href="/pump/">Насос</a></li>
|
||||
<li class="list-group-item"><a href="/sensors/">Датчики</a></li>
|
||||
</ul>
|
||||
</div>
|
15
localwebsite/templates-web/inverter_page.twig
Normal file
15
localwebsite/templates-web/inverter_page.twig
Normal file
@ -0,0 +1,15 @@
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">Главная</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Инвертор</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h6 class="text-primary">Статус</h6>
|
||||
<div id="inverter_status">
|
||||
{{ html|raw }}
|
||||
</div>
|
||||
|
||||
{% js %}
|
||||
Inverter.poll();
|
||||
{% endjs %}
|
12
localwebsite/templates-web/modem_data.twig
Normal file
12
localwebsite/templates-web/modem_data.twig
Normal file
@ -0,0 +1,12 @@
|
||||
{% if not loading %}
|
||||
<span class="text-secondary">Сигнал:</span> {% include 'signal_level.twig' with {'level': modem_data.level} %}<br>
|
||||
<span class="text-secondary">Тип сети:</span> <b>{{ modem_data.type }}</b><br>
|
||||
<span class="text-secondary">RSSI:</span> {{ modem_data.rssi }}<br/>
|
||||
{% if modem_data.sinr %}
|
||||
<span class="text-secondary">SINR:</span> {{ modem_data.sinr }}<br/>
|
||||
{% endif %}
|
||||
<span class="text-secondary">Время соединения:</span> {{ modem_data.connected_time }}<br>
|
||||
<span class="text-secondary">Принято/передано:</span> {{ modem_data.downloaded }} / {{ modem_data.uploaded }}
|
||||
{% else %}
|
||||
{% include 'spinner.twig' %}
|
||||
{% endif %}
|
19
localwebsite/templates-web/modem_status_page.twig
Normal file
19
localwebsite/templates-web/modem_status_page.twig
Normal file
@ -0,0 +1,19 @@
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">Главная</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Модемы</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{% for modem_key, modem in modems %}
|
||||
<h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ modem.label }}</h6>
|
||||
<div id="modem_data_{{ modem_key }}">
|
||||
{% include 'modem_data.twig' with {
|
||||
loading: true
|
||||
} %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% js %}
|
||||
ModemStatus.init({{ js_modems|json_encode|raw }});
|
||||
{% endjs %}
|
18
localwebsite/templates-web/pump.twig
Normal file
18
localwebsite/templates-web/pump.twig
Normal file
@ -0,0 +1,18 @@
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">Главная</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Насос</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<form action="/pump/" method="get">
|
||||
<input type="hidden" name="set" value="{{ status == 'on' ? 'off' : 'on' }}" />
|
||||
Сейчас насос
|
||||
{% if status == 'on' %}
|
||||
<span class="text-success"><b>включен</b></span>.<br><br>
|
||||
<button type="submit" class="btn btn-primary">Выключить</button>
|
||||
{% else %}
|
||||
<span class="text-danger"><b>выключен</b></span>.<br><br>
|
||||
<button type="submit" class="btn btn-primary">Включить</button>
|
||||
{% endif %}
|
||||
</form>
|
11
localwebsite/templates-web/routing_dhcp_page.twig
Normal file
11
localwebsite/templates-web/routing_dhcp_page.twig
Normal file
@ -0,0 +1,11 @@
|
||||
{% include 'routing_header.twig' with {
|
||||
selected_tab: 'dhcp'
|
||||
} %}
|
||||
|
||||
{% for lease in leases %}
|
||||
<div class="mt-3">
|
||||
<b>{{ lease.hostname }}</b> <span class="text-secondary">(exp: {{ lease.time_s }})</span><br/>
|
||||
{{ lease.ip }}<br>
|
||||
<span class="text-secondary">{{ lease.mac }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
24
localwebsite/templates-web/routing_header.twig
Normal file
24
localwebsite/templates-web/routing_header.twig
Normal file
@ -0,0 +1,24 @@
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">Главная</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Маршрутизация</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{% set routing_tabs = [
|
||||
{tab: 'smallhome', url: '/routing/', label: 'Маленький дом'},
|
||||
{tab: 'ipsets', url: '/routing/ipsets/', label: 'Правила'},
|
||||
{tab: 'dhcp', url: '/routing/dhcp/', label: 'DHCP'}
|
||||
] %}
|
||||
|
||||
<nav>
|
||||
<div class="nav nav-tabs" id="nav-tab">
|
||||
{% for tab in routing_tabs %}
|
||||
<a href="{{ tab.url }}" class="text-decoration-none"><button class="nav-link{% if tab.tab == selected_tab %} active{% endif %}" type="button">{{ tab.label }}</button></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% if error %}
|
||||
<div class="mt-4 alert alert-danger"><b>Ошибка:</b> {{ error }}</div>
|
||||
{% endif %}
|
29
localwebsite/templates-web/routing_ipsets_page.twig
Normal file
29
localwebsite/templates-web/routing_ipsets_page.twig
Normal file
@ -0,0 +1,29 @@
|
||||
{% include 'routing_header.twig' with {
|
||||
selected_tab: 'ipsets'
|
||||
} %}
|
||||
|
||||
<div class="mt-2 text-secondary">
|
||||
Таблицы расположены в порядке применения правил iptables.
|
||||
</div>
|
||||
|
||||
{% for set, ips in sets %}
|
||||
<h6 class="text-primary mt-4">{{ set }}</h6>
|
||||
|
||||
{% if ips %}
|
||||
{% for ip in ips %}
|
||||
<div>{{ ip }} (<a href="/routing/ipsets/del/?set={{ set }}&ip={{ ip }}" onclick="return confirm('Подтвердите удаление {{ ip }} из {{ set }}.')">удалить</a>)</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-secondary">Нет записей.</span>
|
||||
{% endif %}
|
||||
|
||||
<div style="max-width: 300px">
|
||||
<form method="post" action="/routing/ipsets/add/">
|
||||
<input type="hidden" name="set" value="{{ set }}">
|
||||
<div class="input-group mt-2">
|
||||
<input type="text" name="ip" placeholder="x.x.x.x/y" class="form-control">
|
||||
<button type="submit" class="btn btn-outline-primary">Добавить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
17
localwebsite/templates-web/routing_page.twig
Normal file
17
localwebsite/templates-web/routing_page.twig
Normal file
@ -0,0 +1,17 @@
|
||||
{% include 'routing_header.twig' with {
|
||||
selected_tab: 'smallhome'
|
||||
} %}
|
||||
|
||||
<div class="mt-3 mb-3">
|
||||
Текущий апстрим: <b>{{ current.label }}</b>
|
||||
</div>
|
||||
|
||||
{% for key, modem in modems %}
|
||||
{% if key != current.key %}
|
||||
<div class="pt-1 pb-2">
|
||||
<a href="/routing/switch-small-home/?upstream={{ key }}">
|
||||
<button type="button" class="btn btn-primary">Переключить на <b>{{ modem.label }}</b></button>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
12
localwebsite/templates-web/sensors.twig
Normal file
12
localwebsite/templates-web/sensors.twig
Normal file
@ -0,0 +1,12 @@
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">Главная</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Датчики</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{% for key, sensor in sensors %}
|
||||
<h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ sensor.name }}</h6>
|
||||
<span class="text-secondary">Температура:</span> <b>{{ sensor.temp }}</b> °C<br>
|
||||
<span class="text-secondary">Влажность:</span> <b>{{ sensor.humidity }}</b>%
|
||||
{% endfor %}
|
5
localwebsite/templates-web/signal_level.twig
Normal file
5
localwebsite/templates-web/signal_level.twig
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="signal_level">
|
||||
{% for i in 0..4 %}
|
||||
<div{% if i < level %} class="yes"{% endif %}></div>
|
||||
{% endfor %}
|
||||
</div>
|
27
localwebsite/templates-web/sms_page.twig
Normal file
27
localwebsite/templates-web/sms_page.twig
Normal file
@ -0,0 +1,27 @@
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">Главная</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">SMS-сообщения</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<nav>
|
||||
<div class="nav nav-tabs" id="nav-tab">
|
||||
{% for modem in modems_list %}
|
||||
{% if selected_modem != modem %}<a href="/modem/sms/?modem={{ modem }}" class="text-decoration-none">{% endif %}
|
||||
<button class="nav-link{% if modem == selected_modem %} active{% endif %}" type="button">{{ modems[modem].short_label }}</button>
|
||||
{% if selected_modem != modem %}</a>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<h6 class="text-primary mt-4">Последние входящие</h6>
|
||||
|
||||
{% for m in messages %}
|
||||
<div class="mt-3">
|
||||
<b>{{ m.phone }}</b> <span class="text-secondary">({{ m.date }})</span><br/>
|
||||
{{ m.content }}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-secondary">Сообщений нет.</span>
|
||||
{% endfor %}
|
14
localwebsite/templates-web/spinner.twig
Normal file
14
localwebsite/templates-web/spinner.twig
Normal file
@ -0,0 +1,14 @@
|
||||
<div class="sk-fading-circle">
|
||||
<div class="sk-circle1 sk-circle"></div>
|
||||
<div class="sk-circle2 sk-circle"></div>
|
||||
<div class="sk-circle3 sk-circle"></div>
|
||||
<div class="sk-circle4 sk-circle"></div>
|
||||
<div class="sk-circle5 sk-circle"></div>
|
||||
<div class="sk-circle6 sk-circle"></div>
|
||||
<div class="sk-circle7 sk-circle"></div>
|
||||
<div class="sk-circle8 sk-circle"></div>
|
||||
<div class="sk-circle9 sk-circle"></div>
|
||||
<div class="sk-circle10 sk-circle"></div>
|
||||
<div class="sk-circle11 sk-circle"></div>
|
||||
<div class="sk-circle12 sk-circle"></div>
|
||||
</div>
|
Loading…
x
Reference in New Issue
Block a user