mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 04:13:56 +08:00
🐤 Sync 2026-03-30 12:52:58
This commit is contained in:
1 parent
238cb2aa39
commit
9941499a61
63 files changed
+3456
-4029
No files matched your search
@@ -1,6 +1,6 @@
|
||||
#
|
||||
# Copyright (C) 2006-2017 OpenWrt.org
|
||||
# Copyright (C) 2022-2025 sirpdboy <herboy2008@gmail.com>
|
||||
# Copyright (C) 2022-2026 sirpdboy <herboy2008@gmail.com>
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
@@ -17,8 +17,8 @@ LUCI_DESCRIPTION:=LuCI support for Easy timecontrol for nftables(Internet time c
|
||||
LUCI_DEPENDS:=+bc +nftables +bash +conntrack
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=3.2.1
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=3.2.4
|
||||
PKG_RELEASE:=3
|
||||
PKG_MAINTAINER:=sirpdboy <herboy2008@gmail.com>
|
||||
|
||||
define Build/Compile
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
/*
|
||||
|
||||
* Copyright (C) 2022-2026 sirpdboy <herboy2008@gmail.com>
|
||||
*/
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
@@ -31,13 +36,12 @@ function checkTimeControlProcess() {
|
||||
}
|
||||
}
|
||||
|
||||
return { running: running, pid: pid };
|
||||
return { running: running, pid: null };
|
||||
}).catch(function() {
|
||||
return { running: false, pid: null };
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染服务状态显示
|
||||
function renderServiceStatus(isRunning, pid) {
|
||||
var statusText = isRunning ? _('RUNNING') : _('NOT RUNNING');
|
||||
var color = isRunning ? 'green' : 'red';
|
||||
@@ -114,16 +118,20 @@ return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('timecontrol'),
|
||||
network.getDevices()
|
||||
network.getHostHints()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var m, s, o;
|
||||
var hostList = [];
|
||||
let hosts = data[1]?.hosts;
|
||||
|
||||
m = new form.Map('timecontrol', _('Internet Time Control'),
|
||||
_('Users can limit their internet usage time through MAC and IP, with available IP ranges such as 192.168.110.00 to 192.168.10.200') + '<br/>' +
|
||||
_('黑名单模式时间控制方式:') + '<br/>' +
|
||||
_('1. 时间段控制: 指定的机器在设定时间段内可以上网,其他时间不能上网') + '<br/>' +
|
||||
_('2. 允许上机时长: 指定的机器上线后可以上网指定时长,超过时长后不能上网') + '<br/>' +
|
||||
_('3. 组合控制: 在时间段内+时长限制(在允许的时间段内限制上网时长)') + '<br/>' +
|
||||
_('Suggested feedback:') + ' <a href="https://github.com/sirpdboy/luci-app-timecontrol.git" target="_blank">GitHub @timecontrol</a>');
|
||||
|
||||
s = m.section(form.TypedSection);
|
||||
@@ -171,7 +179,6 @@ return view.extend({
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
s = m.section(form.TypedSection, 'timecontrol');
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
@@ -202,50 +209,113 @@ return view.extend({
|
||||
o = s.option(form.Flag, 'enable', _('Enabled'));
|
||||
o.rmempty = false;
|
||||
o.default = '1';
|
||||
|
||||
|
||||
o = s.option(form.Value, 'mac', _('IP/MAC Address'));
|
||||
o.rmempty = false;
|
||||
o.placeholder = '192.168.10.100 or 00:11:22:33:44:55';
|
||||
|
||||
getHostList().then(function(hosts) {
|
||||
hostList = hosts;
|
||||
o.value('', _('-- Please select or enter manually --'));
|
||||
if (hosts) {
|
||||
var hostOptions = {};
|
||||
|
||||
if (hosts.length > 0) {
|
||||
hosts.forEach(function(host) {
|
||||
var displayName = '';
|
||||
if (host.name) {
|
||||
displayName = host.name + ' - ';
|
||||
}
|
||||
displayName += host.ipv4 + ' (' + host.mac + ')';
|
||||
|
||||
// 添加IP选项
|
||||
o.value(host.ipv4, displayName);
|
||||
|
||||
// 添加MAC选项
|
||||
var macDisplay = host.mac;
|
||||
if (host.name) {
|
||||
macDisplay += ' - ' + host.name;
|
||||
}
|
||||
if (host.ipv4) {
|
||||
macDisplay += ' (' + host.ipv4 + ')';
|
||||
}
|
||||
o.value(host.mac, macDisplay);
|
||||
});
|
||||
Object.keys(hosts).forEach(function(mac) {
|
||||
var host = hosts[mac];
|
||||
var name = host.name || _(' ');
|
||||
var ips = L.toArray(host.ipaddrs || host.ipv4 || []);
|
||||
|
||||
if (ips.length > 0) {
|
||||
ips.forEach(function(ip) {
|
||||
var macDisplay = 'MAC: %s (%s - %s)'.format(mac,ip, name);
|
||||
hostOptions['mac:' + mac] = macDisplay;
|
||||
var ipDisplay = 'IP: %s (%s - %s)'.format(ip, mac, name);
|
||||
hostOptions['ip:' + ip] = ipDisplay;
|
||||
});
|
||||
}
|
||||
});
|
||||
var sortedKeys = Object.keys(hostOptions).sort(function(a, b) {
|
||||
return hostOptions[a].localeCompare(hostOptions[b]);
|
||||
});
|
||||
|
||||
sortedKeys.forEach(function(key) {
|
||||
if (key.startsWith('ip:')) {
|
||||
o.value(key.substring(3), hostOptions[key]);
|
||||
}
|
||||
});
|
||||
|
||||
sortedKeys.forEach(function(key) {
|
||||
if (key.startsWith('mac:')) {
|
||||
o.value(key.substring(4), hostOptions[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 时间控制方式选择
|
||||
o = s.option(cbiRichListValue, 'time_mode', _('Time Control Mode'));
|
||||
o.value('period', _('Time Period Control (allow in period)'));
|
||||
o.value('duration', _('Allow Duration Control (allow limited time)'));
|
||||
o.value('combined', _('Combined Control (allow in period + limit duration)'));
|
||||
o.default = 'period';
|
||||
o.rmempty = false;
|
||||
o.onchange = function(ev, mode) {
|
||||
var row = this.map.findElement('id', this.cbid(this.section_id));
|
||||
if (row) {
|
||||
// 显示/隐藏相关字段
|
||||
var startTime = row.querySelector('[data-field="timestart"]');
|
||||
var endTime = row.querySelector('[data-field="timeend"]');
|
||||
var duration = row.querySelector('[data-field="duration"]');
|
||||
var useDuration = row.querySelector('[data-field="use_duration"]');
|
||||
var resetCycle = row.querySelector('[data-field="reset_cycle"]');
|
||||
|
||||
if (startTime) startTime.parentElement.style.display =
|
||||
(mode === 'period' || mode === 'combined') ? '' : 'none';
|
||||
if (endTime) endTime.parentElement.style.display =
|
||||
(mode === 'period' || mode === 'combined') ? '' : 'none';
|
||||
if (duration) duration.parentElement.style.display =
|
||||
(mode === 'duration' || mode === 'combined') ? '' : 'none';
|
||||
if (useDuration) useDuration.parentElement.style.display =
|
||||
(mode === 'combined') ? '' : 'none';
|
||||
if (resetCycle) resetCycle.parentElement.style.display =
|
||||
(mode === 'duration' || mode === 'combined') ? '' : 'none';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'timestart', _('Start Control Time'));
|
||||
// 时间段控制字段
|
||||
o = s.option(form.Value, 'timestart', _('Allow Start Time'));
|
||||
o.placeholder = '00:00';
|
||||
o.default = '00:00';
|
||||
o.rmempty = false;
|
||||
o.depends({ 'time_mode': 'period', '!contains': true });
|
||||
o.depends({ 'time_mode': 'combined', '!contains': true });
|
||||
|
||||
o = s.option(form.Value, 'timeend', _('Stop Control Time'));
|
||||
o = s.option(form.Value, 'timeend', _('Allow End Time'));
|
||||
o.placeholder = '00:00';
|
||||
o.default = '00:00';
|
||||
o.rmempty = false;
|
||||
o.depends({ 'time_mode': 'period', '!contains': true });
|
||||
o.depends({ 'time_mode': 'combined', '!contains': true });
|
||||
|
||||
o = s.option(form.ListValue, 'week', _('Week Day (1~7)'));
|
||||
// 持续时间控制字段
|
||||
o = s.option(form.Value, 'duration', _('Allowed Duration (minutes)'));
|
||||
o.placeholder = '60';
|
||||
o.default = '60';
|
||||
o.datatype = 'min(1)';
|
||||
o.depends({ 'time_mode': 'duration', '!contains': true });
|
||||
o.depends({ 'time_mode': 'combined', '!contains': true });
|
||||
o.description = _('设备上线后允许上网的分钟数,超过后将被禁止上网');
|
||||
|
||||
// 重置周期
|
||||
o = s.option(cbiRichListValue, 'reset_cycle', _('Reset Cycle'));
|
||||
o.value('daily', _('Daily Reset'));
|
||||
o.value('weekly', _('Weekly Reset'));
|
||||
o.value('monthly', _('Monthly Reset'));
|
||||
o.value('never', _('Never Reset (until manual reset)'));
|
||||
o.default = 'daily';
|
||||
o.depends({ 'time_mode': 'duration', '!contains': true });
|
||||
o.depends({ 'time_mode': 'combined', '!contains': true });
|
||||
o.description = _('时长重置周期');
|
||||
|
||||
// 组合控制:是否在时间段内启用时长限制
|
||||
o = s.option(form.Flag, 'use_duration', _('Enable Duration Limit in Period'));
|
||||
o.default = '0';
|
||||
o.depends({ 'time_mode': 'combined', '!contains': true });
|
||||
o.description = _('在允许的时间段内限制上网时长');
|
||||
|
||||
o = s.option(form.Value, 'week', _('Week Day (1~7)'));
|
||||
o.value('0', _('Everyday'));
|
||||
o.value('1', _('Monday'));
|
||||
o.value('2', _('Tuesday'));
|
||||
@@ -258,6 +328,8 @@ return view.extend({
|
||||
o.value('6,7', _('Rest Day'));
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
o.description = _('允许上网的星期');
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
/*
|
||||
|
||||
* Copyright (C) 2022-2026 sirpdboy <herboy2008@gmail.com>
|
||||
*/
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require fs';
|
||||
@@ -10,7 +14,6 @@
|
||||
return view.extend({
|
||||
render: function () {
|
||||
var css = `
|
||||
/* 日志框文本区域 */
|
||||
#log_textarea pre {
|
||||
padding: 10px; /* 内边距 */
|
||||
border-bottom: 1px solid #ddd; /* 边框颜色 */
|
||||
@@ -20,15 +23,34 @@ return view.extend({
|
||||
word-wrap: break-word;
|
||||
overflow-y: auto;
|
||||
}
|
||||
/* 5s 自动刷新文字 */
|
||||
.cbi-section small {
|
||||
margin-left: 1rem;
|
||||
font-size: small;
|
||||
color: #666; /* 深灰色文字 */
|
||||
}
|
||||
.log-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 1200px;
|
||||
overflow-y: auto;
|
||||
border-radius: 3px;
|
||||
margin-top: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
.log-line {
|
||||
padding: 3px 0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.log-line:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.log-timestamp {
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
var log_textarea = E('div', { 'id': 'log_textarea' },
|
||||
var log_container = E('div', { 'class': 'log-container', 'id': 'log_container' },
|
||||
E('img', {
|
||||
'src': L.resource(['icons/loading.gif']),
|
||||
'alt': _('Loading...'),
|
||||
@@ -38,6 +60,76 @@ return view.extend({
|
||||
|
||||
var log_path = '/var/log/timecontrol.log';
|
||||
var lastLogContent = '';
|
||||
var lastScrollTop = 0;
|
||||
var isScrolledToTop = true;
|
||||
|
||||
// 解析日志行的时间戳,用于排序
|
||||
function parseLogTimestamp(logLine) {
|
||||
// 假设日志格式为: [2024-01-01 12:00:00] INFO: some message
|
||||
var timestampMatch = logLine.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
|
||||
if (timestampMatch) {
|
||||
return new Date(timestampMatch[1]).getTime();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function reverseLogLines(logContent) {
|
||||
if (!logContent || logContent.trim() === '') {
|
||||
return logContent;
|
||||
}
|
||||
|
||||
var lines = logContent.split('\n');
|
||||
|
||||
lines = lines.filter(function(line) {
|
||||
return line.trim() !== '';
|
||||
});
|
||||
|
||||
lines.sort(function(a, b) {
|
||||
var timeA = parseLogTimestamp(a);
|
||||
var timeB = parseLogTimestamp(b);
|
||||
return timeB - timeA; // 降序排列
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatLogLines(logContent, isNewContent) {
|
||||
if (!logContent || logContent.trim() === '') {
|
||||
return E('div', { 'class': 'log-line' }, _('Log is clean.'));
|
||||
}
|
||||
|
||||
var lines = logContent.split('\n');
|
||||
var formattedLines = [];
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (line === '') continue;
|
||||
|
||||
var timestampMatch = line.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
|
||||
var timestampSpan = null;
|
||||
var messageSpan = null;
|
||||
var lineClass = 'log-line';
|
||||
|
||||
if (timestampMatch) {
|
||||
timestampSpan = E('span', {
|
||||
'class': 'log-timestamp',
|
||||
'title': timestampMatch[1]
|
||||
}, timestampMatch[0] + ' ');
|
||||
messageSpan = E('span', {}, line.substring(timestampMatch[0].length + 1));
|
||||
} else {
|
||||
messageSpan = E('span', {}, line);
|
||||
}
|
||||
|
||||
var lineDiv = E('div', { 'class': lineClass }, [
|
||||
timestampSpan,
|
||||
messageSpan
|
||||
].filter(function(el) { return el !== null; }));
|
||||
|
||||
formattedLines.push(lineDiv);
|
||||
}
|
||||
|
||||
return E('div', {}, formattedLines);
|
||||
}
|
||||
|
||||
var clear_log_button = E('div', {}, [
|
||||
E('button', {
|
||||
@@ -53,9 +145,10 @@ return view.extend({
|
||||
button.disabled = false;
|
||||
button.textContent = _('Clear Logs');
|
||||
// 立即刷新日志显示框
|
||||
var log = E('pre', { 'wrap': 'pre' }, [_('Log is clean.')]);
|
||||
dom.content(log_textarea, log);
|
||||
lastLogContent = '';
|
||||
var logContent = _('Log is clean.');
|
||||
lastLogContent = logContent;
|
||||
dom.content(log_container, formatLogLines(logContent, false));
|
||||
isScrolledToTop = true; // 清空日志后,保持在顶部
|
||||
})
|
||||
.catch(function () {
|
||||
button.textContent = _('Failed to clear log.');
|
||||
@@ -65,34 +158,62 @@ return view.extend({
|
||||
}
|
||||
}, _('Clear Logs'))
|
||||
]);
|
||||
log_container.addEventListener('scroll', function() {
|
||||
lastScrollTop = this.scrollTop;
|
||||
isScrolledToTop = this.scrollTop <= 1;
|
||||
});
|
||||
|
||||
poll.add(L.bind(function () {
|
||||
return fs.read_direct(log_path, 'text')
|
||||
.then(function (res) {
|
||||
var newContent = res.trim() || _('Log is clean.');
|
||||
|
||||
if (newContent !== lastLogContent) {
|
||||
var log = E('pre', { 'wrap': 'pre' }, [newContent]);
|
||||
dom.content(log_textarea, log);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
lastLogContent = newContent;
|
||||
var logContent = res.trim();
|
||||
if (logContent === '') {
|
||||
logContent = _('Log is clean.');
|
||||
}
|
||||
|
||||
// 检查内容是否有变化
|
||||
if (logContent !== lastLogContent) {
|
||||
var isNewContent = lastLogContent !== '' && lastLogContent !== _('Log is clean.');
|
||||
var reversedLog = reverseLogLines(logContent);
|
||||
// 格式化为HTML
|
||||
var formattedLog = formatLogLines(reversedLog, isNewContent);
|
||||
|
||||
var prevScrollHeight = log_container.scrollHeight;
|
||||
var prevScrollTop = log_container.scrollTop;
|
||||
|
||||
dom.content(log_container, formattedLog);
|
||||
lastLogContent = logContent;
|
||||
|
||||
if (isScrolledToTop || isNewContent) {
|
||||
log_container.scrollTop = 0;
|
||||
} else {
|
||||
var newScrollHeight = log_container.scrollHeight;
|
||||
var heightDiff = newScrollHeight - prevScrollHeight;
|
||||
log_container.scrollTop = prevScrollTop + heightDiff;
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
var log;
|
||||
var logContent;
|
||||
if (err.toString().includes('NotFoundError')) {
|
||||
log = E('pre', { 'wrap': 'pre' }, [_('Log file does not exist.')]);
|
||||
logContent = _('Log file does not exist.');
|
||||
} else {
|
||||
log = E('pre', { 'wrap': 'pre' }, [_('Unknown error: %s').format(err)]);
|
||||
logContent = _('Unknown error: %s').format(err);
|
||||
}
|
||||
|
||||
if (logContent !== lastLogContent) {
|
||||
dom.content(log_container, formatLogLines(logContent, false));
|
||||
lastLogContent = logContent;
|
||||
}
|
||||
dom.content(log_textarea, log);
|
||||
});
|
||||
}));
|
||||
|
||||
// 启动轮询
|
||||
poll.start();
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('style', [css]),
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
clear_log_button,
|
||||
log_textarea,
|
||||
log_container,
|
||||
E('small', {}, _('Refresh every 5 seconds.').format(L.env.pollinterval)),
|
||||
E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' })
|
||||
]),
|
||||
@@ -100,7 +221,7 @@ return view.extend({
|
||||
E('span', {}, [
|
||||
_('© github '),
|
||||
E('a', {
|
||||
'href': 'https://github.com/sirpdboy/luci-app-timecontrol',
|
||||
'href': 'https://github.com/sirpdboy',
|
||||
'target': '_blank',
|
||||
'style': 'text-decoration: none;'
|
||||
}, 'by sirpdboy')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
#
|
||||
# Copyright (C) 2023 sirpdboy herboy2008@gmail.com
|
||||
# Copyright (C) 2022-2026 sirpdboy herboy2008@gmail.com
|
||||
#
|
||||
|
||||
START=99
|
||||
@@ -21,9 +22,9 @@ _timecontrol_start() {
|
||||
if [ "$(grep -c 'option enable .1.' /etc/config/$NAME 2>/dev/null)" -gt "0" ]; then
|
||||
touch $LOCK
|
||||
timecontrol start
|
||||
sleep 2
|
||||
start_instance
|
||||
else
|
||||
echo "INFO: ûÓÐÈκιÜÖÆµÄÉ豸£¡"
|
||||
stop_service
|
||||
fi
|
||||
}
|
||||
@@ -40,11 +41,11 @@ service_triggers() {
|
||||
|
||||
stop_service(){
|
||||
kill -9 $(busybox ps -w | grep 'timecontrolctrl' | grep -v 'grep' | awk '{print $1}') >/dev/null 2>&1
|
||||
killall timecontrolctrl 2>/dev/null
|
||||
rm -f $LOCK 2>/dev/null
|
||||
timecontrol stop
|
||||
}
|
||||
|
||||
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
EEOF
|
||||
}
|
||||
chmod +x /etc/init.d/timecontrol /usr/bin/timecontrol*
|
||||
chmod +x /etc/init.d/timecontrol /usr/bin/timecontrol* /usr/libexec/timecontrol-call
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@timecontrol[-1]
|
||||
add ucitrack timecontrol
|
||||
@@ -16,5 +16,7 @@ uci -q batch <<-EOF >/dev/null
|
||||
EOF
|
||||
|
||||
[ -s /etc/config/timecontrol ] || echo "config timecontrol" > /etc/config/timecontrol
|
||||
|
||||
/etc/init.d/rpcd restart
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
||||
File diff suppressed because it is too large.
Load diff
+117
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 时间控制日志查看工具
|
||||
|
||||
NAME=timecontrol
|
||||
LOG_FILE="/var/log/$NAME.log"
|
||||
STATUS_LOG="/var/lib/$NAME/status.log"
|
||||
CONNECTION_LOG="/var/lib/$NAME/connections.log"
|
||||
|
||||
show_realtime_log() {
|
||||
echo "正在显示实时日志,按 Ctrl+C 退出..."
|
||||
echo ""
|
||||
tail -f "$LOG_FILE" | while read line; do
|
||||
# 高亮显示重要信息
|
||||
if echo "$line" | grep -q "STATUS-CHANGE\|TIME_EXCEEDED\|RESET"; then
|
||||
echo -e "\033[1;31m$line\033[0m" # 红色显示重要变更
|
||||
elif echo "$line" | grep -q "ALLOW_ACCESS\|解除限制"; then
|
||||
echo -e "\033[1;32m$line\033[0m" # 绿色显示允许访问
|
||||
elif echo "$line" | grep -q "BLOCK_ACCESS\|添加限制"; then
|
||||
echo -e "\033[1;33m$line\033[0m" # 黄色显示禁止访问
|
||||
else
|
||||
echo "$line"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
show_status_log() {
|
||||
echo "最近状态变更记录:"
|
||||
echo "────────────────────────────────────────────────────────────────────"
|
||||
if [ -f "$STATUS_LOG" ]; then
|
||||
tail -n 20 "$STATUS_LOG" | while read line; do
|
||||
local time=$(echo "$line" | cut -d']' -f1 | sed 's/\[//')
|
||||
local message=$(echo "$line" | cut -d']' -f2-)
|
||||
printf "%-20s %s\n" "$time" "$message"
|
||||
done
|
||||
else
|
||||
echo "暂无状态记录"
|
||||
fi
|
||||
}
|
||||
|
||||
show_connection_log() {
|
||||
echo "设备连接记录:"
|
||||
echo "────────────────────────────────────────────────────────────────────"
|
||||
echo "时间 设备 状态"
|
||||
echo "────────────────────────────────────────────────────────────────────"
|
||||
|
||||
if [ -f "$CONNECTION_LOG" ]; then
|
||||
tail -n 20 "$CONNECTION_LOG" | while read line; do
|
||||
local timestamp=$(echo "$line" | cut -d',' -f1)
|
||||
local target=$(echo "$line" | cut -d',' -f2)
|
||||
local action=$(echo "$line" | cut -d',' -f3)
|
||||
|
||||
local time_str=$(date -d "@$timestamp" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$timestamp")
|
||||
local action_text=""
|
||||
|
||||
case "$action" in
|
||||
"connect") action_text="上线" ;;
|
||||
"disconnect") action_text="下线" ;;
|
||||
*) action_text="$action" ;;
|
||||
esac
|
||||
|
||||
printf "%-20s %-18s %s\n" "$time_str" "$target" "$action_text"
|
||||
done
|
||||
else
|
||||
echo "暂无连接记录"
|
||||
fi
|
||||
}
|
||||
|
||||
show_summary() {
|
||||
local summary_file="/tmp/timecontrol_status.txt"
|
||||
if [ -f "$summary_file" ]; then
|
||||
cat "$summary_file"
|
||||
else
|
||||
echo "状态摘要文件不存在,正在生成..."
|
||||
timecontrol status
|
||||
fi
|
||||
}
|
||||
|
||||
show_help() {
|
||||
echo "时间控制日志查看工具"
|
||||
echo ""
|
||||
echo "用法: timecontrol-log {realtime|status|connections|summary|help}"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " realtime - 实时显示日志(彩色高亮)"
|
||||
echo " status - 显示状态变更记录"
|
||||
echo " connections - 显示设备连接记录"
|
||||
echo " summary - 显示状态摘要"
|
||||
echo " help - 显示此帮助"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " timecontrol-log realtime # 实时监控"
|
||||
echo " timecontrol-log status # 查看状态变更"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
"realtime")
|
||||
show_realtime_log
|
||||
;;
|
||||
"status")
|
||||
show_status_log
|
||||
;;
|
||||
"connections")
|
||||
show_connection_log
|
||||
;;
|
||||
"summary")
|
||||
show_summary
|
||||
;;
|
||||
"help"|"")
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo "未知命令: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,41 +1,60 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Copyright (C) 2006 OpenWrt.org
|
||||
# Copyright 2022-2025 sirpdboy <herboy2008@gmail.com>
|
||||
# Copyright 2022-2026 sirpdboy <herboy2008@gmail.com>
|
||||
|
||||
NAME=timecontrol
|
||||
LOG_FILE="/var/log/timecontrol.log"
|
||||
DEBUG=1
|
||||
|
||||
# 时长数据库目录
|
||||
DURATION_DIR="/var/lib/timecontrol"
|
||||
DURATION_DB="$DURATION_DIR/duration.db"
|
||||
CONNECTION_LOG="$DURATION_DIR/connections.log"
|
||||
|
||||
# 状态文件
|
||||
IDLIST="/var/$NAME.idlist"
|
||||
TMPID="/var/$NAME.tmpid"
|
||||
STATUS_DB="$DURATION_DIR/status.db"
|
||||
|
||||
idlist=$(uci show $NAME | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]')
|
||||
|
||||
config_get_type() {
|
||||
local ret=$(uci -q get "${NAME}.${1}")
|
||||
echo "${ret:=$2}"
|
||||
# 初始化目录
|
||||
init_dirs() {
|
||||
mkdir -p "$DURATION_DIR"
|
||||
touch "$DURATION_DB" "$CONNECTION_LOG" "$STATUS_DB" 2>/dev/null
|
||||
}
|
||||
|
||||
config_n_get() {
|
||||
local ret=$(uci -q get "${NAME}.${1}.${2}")
|
||||
echo "${ret:=$3}"
|
||||
# 日志函数
|
||||
dbg() {
|
||||
[ "$DEBUG" -eq 1 ] && {
|
||||
local d="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "[$d] CTRL-DEBUG: $@" >> "$LOG_FILE"
|
||||
}
|
||||
}
|
||||
|
||||
info() {
|
||||
local d="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "[$d] CTRL-INFO: $@" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
# 配置文件读取
|
||||
config_t_get() {
|
||||
local index=${3:-0}
|
||||
local ret=$(uci -q get "${NAME}.@${1}[${index}].${2}")
|
||||
echo "${ret:=${4}}"
|
||||
local index=${3:-0}
|
||||
local ret=$(uci -q get "${NAME}.@${1}[${index}].${2}")
|
||||
echo "${ret:=${4}}"
|
||||
}
|
||||
|
||||
config_t_set() {
|
||||
local index=${4:-0}
|
||||
local ret=$(uci -q set "${NAME}.@${1}[${index}].${2}=${3}")
|
||||
# 获取启用设备
|
||||
get_enabled_devices() {
|
||||
uci show $NAME 2>/dev/null | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' | sort -n
|
||||
}
|
||||
|
||||
# 时间检查函数
|
||||
is_time_in_range() {
|
||||
local start_time=$1
|
||||
local end_time=$2
|
||||
local current_time=$(date +%H:%M)
|
||||
|
||||
if [ "$start_time" = "$end_time" ]; then
|
||||
return 0
|
||||
return 0
|
||||
elif [ "$start_time" \< "$end_time" ]; then
|
||||
[ "$current_time" \> "$start_time" ] && [ "$current_time" \< "$end_time" ] && return 0
|
||||
else
|
||||
@@ -48,60 +67,269 @@ is_weekday_in_range() {
|
||||
local configured_weekdays=$1
|
||||
local current_weekday=$(date +%u)
|
||||
|
||||
[ "$configured_weekdays" = "0" ] && return 0
|
||||
|
||||
for ww in $(echo $configured_weekdays | sed 's/,/ /g'); do
|
||||
if [ "$current_weekday" = "$ww" ] || [ "$ww" = "0" ]; then
|
||||
return 0
|
||||
fi
|
||||
[ "$current_weekday" = "$ww" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
check_device() {
|
||||
local id=$1
|
||||
local start_time=$(config_t_get device timestart $id )
|
||||
local end_time=$(config_t_get device timeend $id )
|
||||
local weekdays=$(config_t_get device week $id )
|
||||
|
||||
if is_weekday_in_range "$weekdays" && is_time_in_range "$start_time" "$end_time"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
update_device_status() {
|
||||
local id=$1
|
||||
local action=$2
|
||||
|
||||
if [ "$action" = "add" ]; then
|
||||
timecontrol add $id
|
||||
echo "!${id}!" >> $IDLIST
|
||||
elif [ "$action" = "del" ]; then
|
||||
timecontrol del $id
|
||||
sed -i "/!$id!/d" $IDLIST >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
process_devices() {
|
||||
[ -s $IDLIST ] || touch $IDLIST
|
||||
|
||||
for id in $idlist; do
|
||||
if check_device $id; then
|
||||
if ! grep -q "!${id}!" $IDLIST; then
|
||||
update_device_status $id "add"
|
||||
fi
|
||||
else
|
||||
if grep -q "!${id}!" $IDLIST; then
|
||||
update_device_status $id "del"
|
||||
fi
|
||||
# 时长管理
|
||||
record_connection_time() {
|
||||
local target="$1"
|
||||
local action="$2"
|
||||
local timestamp=$(date +%s)
|
||||
|
||||
echo "$timestamp,$target,$action" >> "$CONNECTION_LOG"
|
||||
|
||||
if [ "$action" = "connect" ]; then
|
||||
if ! grep -q "^$target," "$DURATION_DB" 2>/dev/null; then
|
||||
echo "$target,$timestamp,0,0" >> "$DURATION_DB"
|
||||
dbg "初始化时长: $target"
|
||||
fi
|
||||
done
|
||||
|
||||
cat $IDLIST | sort | uniq > $TMPID
|
||||
mv $TMPID $IDLIST
|
||||
elif [ "$action" = "disconnect" ]; then
|
||||
if grep -q "^$target," "$DURATION_DB" 2>/dev/null; then
|
||||
local last_connect=$(grep "^$target," "$DURATION_DB" | cut -d',' -f2)
|
||||
local total_used=$(grep "^$target," "$DURATION_DB" | cut -d',' -f3)
|
||||
local last_reset=$(grep "^$target," "$DURATION_DB" | cut -d',' -f4)
|
||||
|
||||
local session_duration=$((timestamp - last_connect))
|
||||
local new_total=$((total_used + session_duration))
|
||||
|
||||
sed -i "/^$target,/d" "$DURATION_DB"
|
||||
echo "$target,$timestamp,$new_total,$last_reset" >> "$DURATION_DB"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
while :; do
|
||||
process_devices
|
||||
sleep 60
|
||||
done
|
||||
get_connection_time() {
|
||||
local target="$1"
|
||||
local current_time=$(date +%s)
|
||||
|
||||
if grep -q "^$target," "$DURATION_DB" 2>/dev/null; then
|
||||
local last_connect=$(grep "^$target," "$DURATION_DB" | cut -d',' -f2)
|
||||
local total_used=$(grep "^$target," "$DURATION_DB" | cut -d',' -f3)
|
||||
|
||||
if tail -n 5 "$CONNECTION_LOG" 2>/dev/null | grep -q "^[0-9]*,$target,connect$"; then
|
||||
local session_duration=$((current_time - last_connect))
|
||||
total_used=$((total_used + session_duration))
|
||||
fi
|
||||
|
||||
echo "$total_used"
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# 重置检查
|
||||
should_reset_duration() {
|
||||
local target="$1"
|
||||
local reset_cycle="$2"
|
||||
local last_reset_file="$DURATION_DIR/last_reset_$target"
|
||||
|
||||
[ ! -f "$last_reset_file" ] && return 0
|
||||
|
||||
local last_reset=$(cat "$last_reset_file" 2>/dev/null)
|
||||
local current_time=$(date +%s)
|
||||
|
||||
case "$reset_cycle" in
|
||||
"daily")
|
||||
local last_date=$(date -d "@$last_reset" +%Y%m%d 2>/dev/null || echo "0")
|
||||
local current_date=$(date +%Y%m%d)
|
||||
[ "$last_date" != "$current_date" ]
|
||||
;;
|
||||
"weekly")
|
||||
local last_week=$(date -d "@$last_reset" +%Y%W 2>/dev/null || echo "0")
|
||||
local current_week=$(date +%Y%W)
|
||||
[ "$last_week" != "$current_week" ]
|
||||
;;
|
||||
"monthly")
|
||||
local last_month=$(date -d "@$last_reset" +%Y%m 2>/dev/null || echo "0")
|
||||
local current_month=$(date +%Y%m)
|
||||
[ "$last_month" != "$current_month" ]
|
||||
;;
|
||||
*)
|
||||
false
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
reset_duration_counter() {
|
||||
local target="$1"
|
||||
local reset_cycle="$2"
|
||||
local current_time=$(date +%s)
|
||||
|
||||
if grep -q "^$target," "$DURATION_DB" 2>/dev/null; then
|
||||
sed -i "/^$target,/d" "$DURATION_DB"
|
||||
fi
|
||||
|
||||
echo "$target,$current_time,0,$current_time" >> "$DURATION_DB"
|
||||
echo "$current_time" > "$DURATION_DIR/last_reset_$target"
|
||||
|
||||
info "重置时长: $target (周期: $reset_cycle)"
|
||||
}
|
||||
|
||||
# 主检查函数
|
||||
check_device_control() {
|
||||
local id="$1"
|
||||
local target=$(config_t_get device mac "$id")
|
||||
local time_mode=$(config_t_get device time_mode "$id" "period")
|
||||
local weekdays=$(config_t_get device week "$id" "0")
|
||||
|
||||
# 检查星期
|
||||
is_weekday_in_range "$weekdays" || {
|
||||
dbg "星期不允许: $target"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$time_mode" in
|
||||
"period")
|
||||
local start_time=$(config_t_get device timestart "$id" "00:00")
|
||||
local end_time=$(config_t_get device timeend "$id" "00:00")
|
||||
is_time_in_range "$start_time" "$end_time" || {
|
||||
dbg "时间段外: $target ($start_time-$end_time)"
|
||||
return 1
|
||||
}
|
||||
dbg "时间段内: $target"
|
||||
return 0
|
||||
;;
|
||||
|
||||
"duration")
|
||||
local duration=$(config_t_get device duration "$id" "60")
|
||||
local reset_cycle=$(config_t_get device reset_cycle "$id" "daily")
|
||||
|
||||
# 重置检查
|
||||
if should_reset_duration "$target" "$reset_cycle"; then
|
||||
reset_duration_counter "$target" "$reset_cycle"
|
||||
fi
|
||||
|
||||
# 记录连接
|
||||
local last_action=$(grep ",$target," "$CONNECTION_LOG" 2>/dev/null | tail -1 | cut -d',' -f3)
|
||||
if [ "$last_action" != "connect" ]; then
|
||||
record_connection_time "$target" "connect"
|
||||
fi
|
||||
|
||||
# 检查时长
|
||||
local used_seconds=$(get_connection_time "$target")
|
||||
local used_minutes=$((used_seconds / 60))
|
||||
local max_minutes=$duration
|
||||
|
||||
dbg "时长检查: $target 已用=${used_minutes}分钟, 限制=${max_minutes}分钟"
|
||||
|
||||
if [ "$used_minutes" -ge "$max_minutes" ]; then
|
||||
dbg "已超时: $target"
|
||||
return 1
|
||||
fi
|
||||
dbg "未超时: $target"
|
||||
return 0
|
||||
;;
|
||||
|
||||
"combined")
|
||||
local start_time=$(config_t_get device timestart "$id" "00:00")
|
||||
local end_time=$(config_t_get device timeend "$id" "00:00")
|
||||
local use_duration=$(config_t_get device use_duration "$id" "0")
|
||||
|
||||
# 时间段检查
|
||||
is_time_in_range "$start_time" "$end_time" || {
|
||||
dbg "时间段外: $target"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 时长检查
|
||||
if [ "$use_duration" = "1" ]; then
|
||||
local duration=$(config_t_get device duration "$id" "60")
|
||||
local reset_cycle=$(config_t_get device reset_cycle "$id" "daily")
|
||||
|
||||
if should_reset_duration "$target" "$reset_cycle"; then
|
||||
reset_duration_counter "$target" "$reset_cycle"
|
||||
fi
|
||||
|
||||
local last_action=$(grep ",$target," "$CONNECTION_LOG" 2>/dev/null | tail -1 | cut -d',' -f3)
|
||||
if [ "$last_action" != "connect" ]; then
|
||||
record_connection_time "$target" "connect"
|
||||
fi
|
||||
|
||||
local used_seconds=$(get_connection_time "$target")
|
||||
local used_minutes=$((used_seconds / 60))
|
||||
local max_minutes=$duration
|
||||
|
||||
if [ "$used_minutes" -ge "$max_minutes" ]; then
|
||||
dbg "时间段内但已超时: $target"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
dbg "时间段内允许: $target"
|
||||
return 0
|
||||
;;
|
||||
|
||||
*)
|
||||
# 默认时间段控制
|
||||
local start_time=$(config_t_get device timestart "$id" "00:00")
|
||||
local end_time=$(config_t_get device timeend "$id" "00:00")
|
||||
is_time_in_range "$start_time" "$end_time"
|
||||
return $?
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 更新设备状态
|
||||
update_device_status() {
|
||||
local id="$1"
|
||||
local should_allow="$2" # 0=允许, 1=禁止
|
||||
local target=$(config_t_get device mac "$id")
|
||||
local comment=$(config_t_get device comment "$id" "设备$id")
|
||||
|
||||
# 检查当前状态
|
||||
local current_blocked=0
|
||||
if [ -f "$IDLIST" ] && grep -q "!${id}!" "$IDLIST" 2>/dev/null; then
|
||||
current_blocked=1
|
||||
fi
|
||||
|
||||
dbg "设备状态: $target, 应该允许=$should_allow, 当前阻止=$current_blocked"
|
||||
|
||||
if [ "$should_allow" -eq 0 ]; then
|
||||
# 应该允许
|
||||
if [ "$current_blocked" -eq 1 ]; then
|
||||
dbg "解除阻止: $target"
|
||||
timecontrol del "$id"
|
||||
sed -i "/!$id!/d" "$IDLIST" 2>/dev/null
|
||||
info "允许上网: $comment ($target)"
|
||||
record_connection_time "$target" "disconnect"
|
||||
fi
|
||||
else
|
||||
# 应该阻止
|
||||
if [ "$current_blocked" -eq 0 ]; then
|
||||
dbg "添加阻止: $target"
|
||||
timecontrol add "$id"
|
||||
if ! grep -q "!$id!" "$IDLIST" 2>/dev/null; then
|
||||
echo "!$id!" >> "$IDLIST"
|
||||
fi
|
||||
info "阻止上网: $comment ($target)"
|
||||
record_connection_time "$target" "disconnect"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 主处理循环
|
||||
main_loop() {
|
||||
info "时间控制守护进程启动"
|
||||
init_dirs
|
||||
while :; do
|
||||
dbg "开始检查设备"
|
||||
[ `uci show $NAME 2>/dev/null | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' | sort -n | wc -l` eq 0 ] && timecontrol stop && break
|
||||
for id in $(get_enabled_devices); do
|
||||
if check_device_control "$id"; then
|
||||
update_device_status "$id" 0 # 允许
|
||||
else
|
||||
update_device_status "$id" 1 # 阻止
|
||||
fi
|
||||
done
|
||||
|
||||
sleep 60
|
||||
done
|
||||
}
|
||||
|
||||
# 启动
|
||||
main_loop
|
||||
@@ -1,11 +1,4 @@
|
||||
{ "admin/control": {
|
||||
"title": "Control",
|
||||
"order": 44,
|
||||
"action": {
|
||||
"type": "firstchild",
|
||||
"recurse": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"admin/control/timecontrol": {
|
||||
"title": "Time Control",
|
||||
"order": 10,
|
||||
|
||||
Reference in new issue
Block a user