diff --git a/README.md b/README.md index 6585d10..5191764 100644 --- a/README.md +++ b/README.md @@ -8,34 +8,38 @@ Internet-detector is an application for checking the availability of the Interne **Features:** - It can run continuously as a system service or only in an open web interface. - Checking the availability of a host using ping or by connecting via TCP to a specified port. - - Execution of custom shell scripts when connecting and disconnecting the Internet. - LED indication of Internet availability. ![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/internet-led.jpg) + - Performing actions when connecting and disconnecting the Internet (Restarting network, modem or device. Executing custom shell scripts). + - Sending email notification when Internet access is restored. - The daemon is written entirely in Lua using the nixio library. ## Installation notes **OpenWrt 19.07:** - wget --no-check-certificate -O /tmp/internet-detector_0.4-3_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/19.07/internet-detector_0.4-3_all.ipk - opkg install /tmp/internet-detector_0.4-3_all.ipk - rm /tmp/internet-detector_0.4-3_all.ipk + wget --no-check-certificate -O /tmp/internet-detector_0.5-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/19.07/internet-detector_0.5-0_all.ipk + opkg install /tmp/internet-detector_0.5-0_all.ipk + rm /tmp/internet-detector_0.5-0_all.ipk /etc/init.d/internet-detector start /etc/init.d/internet-detector enable - wget --no-check-certificate -O /tmp/luci-app-internet-detector_0.4-3_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/19.07/luci-app-internet-detector_0.4-3_all.ipk - opkg install /tmp/luci-app-internet-detector_0.4-3_all.ipk - rm /tmp/luci-app-internet-detector_0.4-3_all.ipk + wget --no-check-certificate -O /tmp/luci-app-internet-detector_0.5-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/19.07/luci-app-internet-detector_0.5-0_all.ipk + opkg install /tmp/luci-app-internet-detector_0.5-0_all.ipk + rm /tmp/luci-app-internet-detector_0.5-0_all.ipk /etc/init.d/rpcd restart +Email notification: + + opkg install mailsend + i18n-ru: - wget --no-check-certificate -O /tmp/luci-i18n-internet-detector-ru_0.4-3_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/19.07/luci-i18n-internet-detector-ru_0.4-3_all.ipk - opkg install /tmp/luci-i18n-internet-detector-ru_0.4-3_all.ipk - rm /tmp/luci-i18n-internet-detector-ru_0.4-3_all.ipk + wget --no-check-certificate -O /tmp/luci-i18n-internet-detector-ru_0.5-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/19.07/luci-i18n-internet-detector-ru_0.5-0_all.ipk + opkg install /tmp/luci-i18n-internet-detector-ru_0.5-0_all.ipk + rm /tmp/luci-i18n-internet-detector-ru_0.5-0_all.ipk ## Screenshots: ![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/01.jpg) -![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/03.jpg) -![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/04.jpg) +![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/02.jpg) diff --git a/internet-detector/Makefile b/internet-detector/Makefile index 8c0adfe..5827138 100644 --- a/internet-detector/Makefile +++ b/internet-detector/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=internet-detector -PKG_VERSION:=0.4 -PKG_RELEASE:=3 +PKG_VERSION:=0.5 +PKG_RELEASE:=0 PKG_MAINTAINER:=gSpot include $(INCLUDE_DIR)/package.mk @@ -41,14 +41,18 @@ define Package/$(PKG_NAME)/install $(INSTALL_CONF) ./files/etc/config/internet-detector $(1)/etc/config/internet-detector $(INSTALL_DIR) $(1)/etc/internet-detector $(INSTALL_BIN) ./files/etc/internet-detector/down-script $(1)/etc/internet-detector/down-script - $(INSTALL_BIN) ./files/etc/internet-detector/run-script $(1)/etc/internet-detector/run-script $(INSTALL_BIN) ./files/etc/internet-detector/up-script $(1)/etc/internet-detector/up-script $(INSTALL_DIR) $(1)/etc/init.d $(INSTALL_BIN) ./files/etc/init.d/internet-detector $(1)/etc/init.d/internet-detector $(INSTALL_DIR) $(1)/usr/bin $(INSTALL_BIN) ./files/usr/bin/internet-detector $(1)/usr/bin/internet-detector $(INSTALL_DIR) $(1)/usr/lib/internet-detector + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_email.lua $(1)/usr/lib/internet-detector/mod_email.lua $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_led_control.lua $(1)/usr/lib/internet-detector/mod_led_control.lua + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_modem_restart.lua $(1)/usr/lib/internet-detector/mod_modem_restart.lua + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_network_restart.lua $(1)/usr/lib/internet-detector/mod_network_restart.lua + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_reboot.lua $(1)/usr/lib/internet-detector/mod_reboot.lua + $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_user_scripts.lua $(1)/usr/lib/internet-detector/mod_user_scripts.lua endef $(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/internet-detector/files/etc/config/internet-detector b/internet-detector/files/etc/config/internet-detector index 3e217ad..82f70bc 100644 --- a/internet-detector/files/etc/config/internet-detector +++ b/internet-detector/files/etc/config/internet-detector @@ -4,22 +4,43 @@ config main 'config' list hosts '1.1.1.1' option check_type '0' option tcp_port '53' - -config main 'ui_config' - option interval_up '6' - option interval_down '1' - option connection_attempts '1' - option connection_timeout '1' - -config main 'service_config' - option interval_up '30' - option interval_down '5' - option connection_attempts '2' - option connection_timeout '2' - option enable_logger '1' - option enable_up_script '0' - option enable_down_script '0' - option enable_run_script '0' + option ui_interval_up '6' + option ui_interval_down '1' + option ui_connection_attempts '1' + option ui_connection_timeout '1' + option service_interval_up '30' + option service_interval_down '5' + option service_connection_attempts '2' + option service_connection_timeout '2' + option service_enable_logger '1' config module 'mod_led_control' option enabled '0' + +config module 'mod_reboot' + option enabled '0' + option dead_period '3600' + option force_reboot_delay '300' + +config module 'mod_network_restart' + option enabled '0' + option dead_period '900' + option attempts '1' + option restart_timeout '0' + +config module 'mod_modem_restart' + option enabled '0' + option dead_period '600' + option any_band '1' + +config module 'mod_email' + option enabled '0' + option alive_period '0' + option mail_smtp 'smtp.gmail.com' + option mail_smtp_port '587' + option mail_security 'tls' + +config module 'mod_user_scripts' + option enabled '0' + option alive_period '0' + option dead_period '0' diff --git a/internet-detector/files/etc/init.d/internet-detector b/internet-detector/files/etc/init.d/internet-detector index a6fdc8f..35ebf41 100755 --- a/internet-detector/files/etc/init.d/internet-detector +++ b/internet-detector/files/etc/init.d/internet-detector @@ -1,6 +1,6 @@ #!/bin/sh /etc/rc.common -START=99 +START=97 STOP=01 ID="/usr/bin/internet-detector" diff --git a/internet-detector/files/etc/internet-detector/run-script b/internet-detector/files/etc/internet-detector/run-script deleted file mode 100755 index 4ee28c6..0000000 --- a/internet-detector/files/etc/internet-detector/run-script +++ /dev/null @@ -1,4 +0,0 @@ -# Shell commands that are executed every time the Internet is checked for availability -# -# $1 - (0|1) - internet status: 0 is up, 1 is down -# diff --git a/internet-detector/files/usr/bin/internet-detector b/internet-detector/files/usr/bin/internet-detector index 3b088bd..f050650 100755 --- a/internet-detector/files/usr/bin/internet-detector +++ b/internet-detector/files/usr/bin/internet-detector @@ -14,38 +14,36 @@ -- Default settings local Config = { - ["mode"] = 2, - ["enableLogger"] = true, - ["enableUpScript"] = false, - ["enableDownScript"] = false, - ["enableRunScript"] = false, - ["intervalUp"] = 30, - ["intervalDown"] = 5, - ["connectionAttempts"] = 1, - ["UIConnectionAttempts"] = 1, - ["hosts"] = { + mode = 2, + enableLogger = true, + intervalUp = 30, + intervalDown = 5, + connectionAttempts = 2, + connectionTimeout = 2, + UIConnectionAttempts = 1, + UIConnectionTimeout = 1, + hosts = { [1] = "8.8.8.8", [2] = "1.1.1.1", }, - ["parsedHosts"] = {}, - ["appName"] = "internet-detector", - ["commonDir"] = "/tmp/run", - ["pingCmd"] = "ping", - ["pingParams"] = "-c 1", - ["connectionTimeout"] = 3, - ["UIConnectionTimeout"] = 1, - ["tcpPort"] = 53, - ["checkType"] = 0, -- 0: TCP, 1: ping - ["loggerLevel"] = "info", - ["modules"] = {}, + tcpPort = 53, + pingPacketSize = 56, + iface = nil, + checkType = 0, -- 0: TCP, 1: ping + hostname = "OpenWrt", + appName = "internet-detector", + commonDir = "/tmp/run", + debugLog = "/tmp/internet-detector.debug", + pingCmd = "/bin/ping", + pingParams = "-c 1", + debug = false, + modules = {}, + parsedHosts = {}, } -Config.configDir = "/etc/" .. Config.appName -Config.modulesDir = "/usr/lib/" .. Config.appName -Config.upScript = Config.configDir .. "/" .. "up-script" -Config.downScript = Config.configDir .. "/" .. "down-script" -Config.runScript = Config.configDir .. "/" .. "run-script" -Config.pidFile = Config.commonDir .. "/" .. Config.appName .. ".pid" -Config.statusFile = Config.commonDir .. "/" .. Config.appName .. ".status" +Config.configDir = string.format("/etc/%s", Config.appName) +Config.modulesDir = string.format("/usr/lib/%s", Config.appName) +Config.pidFile = string.format("%s/%s.pid", Config.commonDir, Config.appName) +Config.statusFile = string.format("%s/%s.status", Config.commonDir, Config.appName) -- Importing packages @@ -69,31 +67,48 @@ end local uciCursor = uci.cursor() Config.mode = tonumber(uciCursor:get( Config.appName, "config", "mode")) +Config.enableLogger = (tonumber(uciCursor:get( + Config.appName, "config", "service_enable_logger")) ~= 0) +Config.intervalUp = tonumber(uciCursor:get( + Config.appName, "config", "service_interval_up")) +Config.intervalDown = tonumber(uciCursor:get( + Config.appName, "config", "service_interval_down")) +Config.connectionAttempts = tonumber(uciCursor:get( + Config.appName, "config", "service_connection_attempts")) +Config.connectionTimeout = tonumber(uciCursor:get( + Config.appName, "config", "service_connection_timeout")) +Config.UIConnectionAttempts = tonumber(uciCursor:get( + Config.appName, "config", "ui_connection_attempts")) +Config.UIConnectionTimeout = tonumber(uciCursor:get( + Config.appName, "config", "ui_connection_timeout")) Config.hosts = uciCursor:get(Config.appName, "config", "hosts") + +local tcpPort = uciCursor:get( + Config.appName, "config", "tcp_port") +if tcpPort ~= nil then + Config.tcpPort = tonumber(tcpPort) +end + +local pingPacketSize = uciCursor:get( + Config.appName, "config", "ping_packet_size") +if pingPacketSize ~= nil then + Config.pingPacketSize = tonumber(pingPacketSize) +end + +local iface = uciCursor:get( + Config.appName, "config", "iface") +if iface ~= nil then + Config.iface = iface +end + Config.checkType = tonumber(uciCursor:get( Config.appName, "config", "check_type")) -Config.tcpPort = tonumber(uciCursor:get( - Config.appName, "config", "tcp_port")) -Config.UIConnectionAttempts = tonumber(uciCursor:get( - Config.appName, "ui_config", "connection_attempts")) -Config.UIConnectionTimeout = tonumber(uciCursor:get( - Config.appName, "ui_config", "connection_timeout")) -Config.enableLogger = (tonumber(uciCursor:get( - Config.appName, "service_config", "enable_logger")) ~= 0) -Config.enableUpScript = (tonumber(uciCursor:get( - Config.appName, "service_config", "enable_up_script")) ~= 0) -Config.enableDownScript = (tonumber(uciCursor:get( - Config.appName, "service_config", "enable_down_script")) ~= 0) -Config.enableRunScript = (tonumber(uciCursor:get( - Config.appName, "service_config", "enable_run_script")) ~= 0) -Config.intervalUp = tonumber(uciCursor:get( - Config.appName, "service_config", "interval_up")) -Config.intervalDown = tonumber(uciCursor:get( - Config.appName, "service_config", "interval_down")) -Config.connectionAttempts = tonumber(uciCursor:get( - Config.appName, "service_config", "connection_attempts")) -Config.connectionTimeout = tonumber(uciCursor:get( - Config.appName, "service_config", "connection_timeout")) + +local hostname = uciCursor:get("system", "@[0]", "hostname") +if hostname ~= nil then + Config.hostname = hostname +end + local function writeValueToFile(filePath, str) local retValue = false @@ -124,7 +139,7 @@ local function writeLogMessage(level, msg) end local function loadModules() - package.path = string.format("%s;%s/?.lua", package.path, Config.modulesDir) + package.path = string.format("%s;%s/?.lua", package.path, Config.modulesDir) Config.modules = {} uciCursor:foreach( Config.appName, @@ -134,6 +149,7 @@ local function loadModules() if mod_name and s.enabled == "1" then local m = prequire(mod_name) if m then + m.config = Config m.syslog = writeLogMessage m.writeValue = writeValueToFile m.readValue = readValueFromFile @@ -145,40 +161,43 @@ local function loadModules() ) end -local function runExternalScript(scriptPath, inetStat) - if inetStat == nil then - inetStat = "" - end - if nixio.fs.access(scriptPath, "x") then - local fh = io.popen( - string.format('/bin/sh -c "%s %s" &', scriptPath, inetStat), "r") - fh:close() - end -end - local function parseHost(host) - local port - local addr = host:match("^[^:]+") - if host:find(":") then - port = host:match("[^:]+$") - end - return addr, port + local addr, port = host:match("^([^:]+):?(%d*)") + return addr, tonumber(port) or false end local function parseHosts() Config.parsedHosts = {} for k, v in ipairs(Config.hosts) do local addr, port = parseHost(v) - Config.parsedHosts[k] = {[1] = addr, [2] = (tonumber(port) or false)} + Config.parsedHosts[k] = { addr = addr, port = port } end end local function pingHost(host) - return os.execute(string.format("%s %s -W %d %s > /dev/null 2>&1", - Config.pingCmd, Config.pingParams, Config.connectionTimeout, host)) + local ping = string.format( + "%s %s -W %d -s %d%s %s > /dev/null 2>&1", + Config.pingCmd, + Config.pingParams, + Config.connectionTimeout, + Config.pingPacketSize, + Config.iface and (" -I " .. Config.iface) or "", + host + ) + local retCode = os.execute(ping) + + -- Debug + if Config.debug then + io.stdout:write(string.format( + "--- Ping ---\ntime = %s\n%s\nretCode = %s\n", os.time(), ping, retCode) + ) + io.stdout:flush() + end + + return retCode end -local function tcpConnectToHost(host, port) +local function TCPConnectionToHost(host, port) local retCode = 1 local addrInfo = nixio.getaddrinfo(host, "any") if addrInfo then @@ -187,7 +206,31 @@ local function tcpConnectToHost(host, port) local socket = nixio.socket(family, "stream") socket:setopt("socket", "sndtimeo", Config.connectionTimeout) socket:setopt("socket", "rcvtimeo", Config.connectionTimeout) + if Config.iface then + socket:setopt("socket", "bindtodevice", Config.iface) + end local success = socket:connect(host, port or Config.tcpPort) + + -- Debug + if Config.debug then + local sockAddr, sockPort = socket:getsockname() + local peerAddr, peerPort = socket:getpeername() + io.stdout:write(string.format( + "--- TCP ---\ntime = %s\nconnectionTimeout = %s\niface = %s\nhost:port = %s:%s\nsockname = %s:%s\npeername = %s:%s\nsuccess = %s\n", + os.time(), + Config.connectionTimeout, + tostring(Config.iface), + host, + port or Config.tcpPort, + tostring(sockAddr), + tostring(sockPort), + tostring(peerAddr), + tostring(peerPort), + tostring(success)) + ) + io.stdout:flush() + end + socket:close() retCode = success and 0 or 1 end @@ -196,11 +239,11 @@ local function tcpConnectToHost(host, port) end local function checkHosts() - local checkFunc = (Config.checkType == 1) and pingHost or tcpConnectToHost + local checkFunc = (Config.checkType == 1) and pingHost or TCPConnectionToHost local retCode = 1 for k, v in ipairs(Config.parsedHosts) do for i = 1, Config.connectionAttempts do - if checkFunc(v[1], v[2]) == 0 then + if checkFunc(v.addr, v.port) == 0 then retCode = 0 break end @@ -213,46 +256,48 @@ local function checkHosts() end local function main() - local lastStatus - local currentStatus + local lastStatus, currentStatus, timeNow, timeDiff, lastTime local interval = Config.intervalUp + local counter = 0 while true do - currentStatus = checkHosts() - if not nixio.fs.access(Config.statusFile, "r") then - writeValueToFile(Config.statusFile, currentStatus) - end - - if currentStatus == 0 then - interval = Config.intervalUp - if lastStatus ~= nil and currentStatus ~= lastStatus then + if counter == 0 or counter >= interval then + currentStatus = checkHosts() + if not nixio.fs.access(Config.statusFile, "r") then writeValueToFile(Config.statusFile, currentStatus) - writeLogMessage("notice", "internet connected") - if Config.enableUpScript then - runExternalScript(Config.upScript) - end - end - else - interval = Config.intervalDown - if lastStatus ~= nil and currentStatus ~= lastStatus then - writeValueToFile(Config.statusFile, currentStatus) - writeLogMessage("notice", "internet disconnected") - if Config.enableDownScript then - runExternalScript(Config.downScript) + end + + if currentStatus == 0 then + interval = Config.intervalUp + if lastStatus ~= nil and currentStatus ~= lastStatus then + writeValueToFile(Config.statusFile, currentStatus) + writeLogMessage("notice", "Internet connected") + end + else + interval = Config.intervalDown + if lastStatus ~= nil and currentStatus ~= lastStatus then + writeValueToFile(Config.statusFile, currentStatus) + writeLogMessage("notice", "Internet disconnected") end end + + counter = 0 end + timeDiff = 0 for _, e in ipairs(Config.modules) do - e:run(currentStatus, lastStatus) + timeNow = nixio.sysinfo().uptime + if lastTime then + timeDiff = timeDiff + timeNow - lastTime + else + timeDiff = 1 + end + lastTime = timeNow + e:run(currentStatus, lastStatus, timeDiff) end - - if Config.enableRunScript then - runExternalScript(Config.runScript, currentStatus) - end - lastStatus = currentStatus - nixio.nanosleep(interval) + nixio.nanosleep(1) + counter = counter + 1 end end @@ -369,6 +414,30 @@ local function run() ) end + -- Debug + if Config.debug then + + local function inspectTable() + local tables = {}, f + f = function(t, prefix) + tables[t] = true + for k, v in pairs(t) do + io.stdout:write(string.format( + "%s%s = %s\n", prefix, k, tostring(v)) + ) + if type(v) == "table" and not tables[v] then + f(v, string.format("%s%s.", prefix, k)) + end + end + end + return f + end + + io.stdout:write("--- Config ---\n") + inspectTable()(Config, "Config.") + io.stdout:flush() + end + main() if Config.enableLogger then nixio.closelog() @@ -382,7 +451,7 @@ local function noDaemon() run() end -local function daemon() +local function daemon(debug) if not preRun() then return end @@ -392,12 +461,16 @@ local function daemon() if nixio.fork() == 0 then nixio.chdir("/") nixio.umask(0) - local devnull = "/dev/null" + local output = "/dev/null" + if debug then + output = Config.debugLog + Config.debug = true + end io.stdout:flush() io.stderr:flush() - nixio.dup(io.open(devnull, "r"), io.stdin) - nixio.dup(io.open(devnull, "a+"), io.stdout) - nixio.dup(io.open(devnull, "a+"), io.stderr) + nixio.dup(io.open("/dev/null", "r"), io.stdin) + nixio.dup(io.open(output, "a+"), io.stdout) + nixio.dup(io.open(output, "a+"), io.stderr) run() end os.exit(0) @@ -416,16 +489,18 @@ parseHosts() local function help() return string.format( - "Usage: %s [start|no-daemon|stop|restart|status|inet-status|poll [] []|--help]", + "Usage: %s [start|stop|restart|no-daemon|debug|status|inet-status|poll [] []|--help]", arg[0] ) end -local helpArgs = {["-h"] = true, ["--help"] = true, ["help"] = true} +local helpArgs = { ["-h"] = true, ["--help"] = true, ["help"] = true } if arg[1] == "start" or #arg == 0 then daemon() elseif arg[1] == "no-daemon" then noDaemon() +elseif arg[1] == "debug" then + daemon(true) elseif arg[1] == "stop" then stop() elseif arg[1] == "restart" then diff --git a/internet-detector/files/usr/lib/internet-detector/mod_email.lua b/internet-detector/files/usr/lib/internet-detector/mod_email.lua new file mode 100644 index 0000000..5f960c1 --- /dev/null +++ b/internet-detector/files/usr/lib/internet-detector/mod_email.lua @@ -0,0 +1,138 @@ +--[[ + Dependences: + mailsend +--]] +local nixio = require("nixio") + +local Module = { + name = "mod_email", + config = {}, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + alivePeriod = 0, + hostAlias = "OpenWrt", + mta = "/usr/bin/mailsend", + mailRecipient = "email@gmail.com", + mailSender = "email@gmail.com", + mailUser = "email@gmail.com", + mailPassword = "password", + mailSmtp = "smtp.gmail.com", + mailSmtpPort = '587', + mailSecurity = "tls", + _enabled = false, + _aliveCounter = 0, + _msgSent = true, + _disconnected = true, + _lastDisconnection = nil, + _lastConnection = nil, +} + +function Module:init(t) + self.alivePeriod = tonumber(t.alive_period) + if t.host_alias then + self.hostAlias = t.host_alias + else + self.hostAlias = self.config.hostname + end + + self.mailRecipient = t.mail_recipient + self.mailSender = t.mail_sender + self.mailUser = t.mail_user + self.mailPassword = t.mail_password + self.mailSmtp = t.mail_smtp + self.mailSmtpPort = t.mail_smtp_port + self.mailSecurity = t.mail_security + + if nixio.fs.access(self.mta, "x") then + self._enabled = true + else + self._enabled = false + self.syslog("warning", string.format("%s: %s is not available", self.name, self.mta)) + end + + if (not self.mailRecipient or + not self.mailSender or + not self.mailUser or + not self.mailPassword or + not self.mailSmtp or + not self.mailSmtpPort) then + self._enabled = false + self.syslog("warning", string.format( + "%s: Insufficient data to connect to the SMTP server", self.name)) + end +end + +function Module:sendMessage(msg) + local verboseArg = "" + -- Debug + if self.config.debug then + verboseArg = " -v" + io.stdout:write("--- mod_email ---\n") + io.stdout:flush() + end + local securityArgs = "-starttls -auth-login" + if self.mailSecurity == "ssl" then + securityArgs = "-ssl -auth" + end + local mtaCmd = string.format( + '%s%s %s -smtp "%s" -port %s -cs utf-8 -user "%s" -pass "%s" -f "%s" -t "%s" -sub "%s" -M "%s"', + self.mta, verboseArg, securityArgs, self.mailSmtp, self.mailSmtpPort, + self.mailUser, self.mailPassword, self.mailSender, self.mailRecipient, + string.format("%s notification", self.hostAlias), + string.format("%s:\n%s", self.hostAlias, msg)) + + if os.execute(mtaCmd) ~= 0 then + self.syslog("err", string.format( + "%s: An error occured while sending message", self.name)) + else + self.syslog("info", string.format( + "%s: Message sent to %s", self.name, self.mailRecipient)) + end +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if not self._enabled then + return + end + + if currentStatus == 1 then + self._aliveCounter = 0 + self._msgSent = false + self._lastConnection = nil + if not self._disconnected then + self._disconnected = true + if not self._lastDisconnection then + self._lastDisconnection = os.date("%Y.%m.%d %H:%M:%S", os.time()) + end + end + else + if not self._msgSent then + + if not self._lastConnection then + self._lastConnection = os.date("%Y.%m.%d %H:%M:%S", os.time()) + end + + if self._aliveCounter >= self.alivePeriod then + local message = {} + if self._lastDisconnection then + message[#message + 1] = string.format( + "Internet disconnected: %s", self._lastDisconnection) + self._lastDisconnection = nil + end + if self._lastConnection then + message[#message + 1] = string.format( + "Internet connected: %s", self._lastConnection) + self:sendMessage(table.concat(message, ", ")) + self._msgSent = true + end + else + self._aliveCounter = self._aliveCounter + timeDiff + end + end + + self._disconnected = false + end +end + +return Module diff --git a/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua b/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua index 30d31df..c828059 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua +++ b/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua @@ -3,16 +3,19 @@ local nixio = require("nixio") local Module = { name = "mod_led_control", - sysLedsDir = "/sys/class/leds", + config = {}, syslog = function(level, msg) return true end, writeValue = function(filePath, str) return false end, readValue = function(filePath) return nil end, + runInterval = 5, + sysLedsDir = "/sys/class/leds", ledName = nil, _enabled = false, _ledDir = nil, _ledMaxBrightnessFile = nil, _ledBrightnessFile = nil, _ledMaxBrightness = nil, + _counter = 0, } function Module:resetLeds() @@ -33,14 +36,14 @@ function Module:init(t) if not self.ledName then return end - self._ledDir = string.format("%s/%s", self.sysLedsDir, self.ledName) - self._ledMaxBrightnessFile = self._ledDir .. "/max_brightness" - self._ledBrightnessFile = self._ledDir .. "/brightness" + self._ledDir = string.format("%s/%s", self.sysLedsDir, self.ledName) + self._ledMaxBrightnessFile = string.format("%s/max_brightness", self._ledDir) + self._ledBrightnessFile = string.format("%s/brightness", self._ledDir) self._ledMaxBrightness = self.readValue(self._ledMaxBrightnessFile) or 1 if (not nixio.fs.access(self._ledDir, "r") or not nixio.fs.access(self._ledBrightnessFile, "r", "w")) then self._enabled = false - self.syslog("warning", string.format('%s: "%s" is not available', self.name, self.ledName)) + self.syslog("warning", string.format("%s: LED '%s' is not available", self.name, self.ledName)) else self._enabled = true -- Reset all LEDs @@ -63,19 +66,25 @@ function Module:off() self.writeValue(self._ledBrightnessFile, 0) end -function Module:run(currentStatus, lastStatus) +function Module:run(currentStatus, lastStatus, timeDiff) if not self._enabled then return end - if currentStatus == 0 then - if not self:getCurrentState() then - self:on() - end - else - if self:getCurrentState() then - self:off() + if self._counter == 0 or self._counter >= self.runInterval or currentStatus ~= lastStatus then + + if currentStatus == 0 then + if not self:getCurrentState() then + self:on() + end + else + if self:getCurrentState() then + self:off() + end end + + self._counter = 0 end + self._counter = self._counter + timeDiff end return Module diff --git a/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua b/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua new file mode 100644 index 0000000..6a124c8 --- /dev/null +++ b/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua @@ -0,0 +1,84 @@ +--[[ + Dependences: + modemmanager +--]] +local nixio = require("nixio") + +local Module = { + name = "mod_modem_restart", + config = {}, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + mmcli = "/usr/bin/mmcli", + mmInit = "/etc/init.d/modemmanager", + deadPeriod = 0, + iface = nil, + anyBand = false, + _enabled = false, + _deadCounter = 0, + _restarted = false, +} + +function Module:toggleIface(flag) + return os.execute( + string.format("%s %s", (flag and "/sbin/ifup" or "/sbin/ifdown"), self.iface) + ) +end + +function Module:restartMM() + if self.anyBand then + self.syslog("info", string.format( + "%s: resetting current-bands to 'any'", self.name)) + os.execute(string.format("%s -m any --set-current-bands=any", self.mmcli)) + end + + self.syslog("info", string.format("%s: reconnecting modem", self.name)) + os.execute(string.format("%s restart", self.mmInit)) + + if self.iface then + self.syslog("info", string.format( + "%s: restarting network interface '%s'", self.name, self.iface)) + self:toggleIface(false) + self:toggleIface(true) + end +end + +function Module:init(t) + self.deadPeriod = tonumber(t.dead_period) + self.iface = t.iface + self.anyBand = (tonumber(t.any_band) ~= 0) + + if not nixio.fs.access(self.mmcli, "x") then + self.anyBand = false + end + + if nixio.fs.access(self.mmInit, "x") then + self._enabled = true + else + self._enabled = false + self.syslog("warning", string.format( + "%s: modemmanager service is not available", self.name)) + end +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if not self._enabled then + return + end + if currentStatus == 1 then + if not self._restarted then + if self._deadCounter >= self.deadPeriod then + self:restartMM() + self._restarted = true + else + self._deadCounter = self._deadCounter + timeDiff + end + end + else + self._deadCounter = 0 + self._restarted = false + end +end + +return Module diff --git a/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua b/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua new file mode 100644 index 0000000..b6d1440 --- /dev/null +++ b/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua @@ -0,0 +1,91 @@ + +local nixio = require("nixio") + +local Module = { + name = "mod_network_restart", + config = {}, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + iface = false, + attempts = 0, + deadPeriod = 0, + restartTimeout = 0, + _attemptsCounter = 0, + _deadCounter = 0, +} + +function Module:toggleFunc(flag) + return +end + +function Module:toggleDevice(flag) + local ip = "/sbin/ip" + if nixio.fs.access(ip, "x") then + return os.execute( + string.format("%s link set dev %s %s", ip, self.iface, (flag and "up" or "down")) + ) + end +end + +function Module:toggleIface(flag) + return os.execute( + string.format("%s %s", (flag and "/sbin/ifup" or "/sbin/ifdown"), self.iface) + ) +end + +function Module:ifaceUp() + self:toggleFunc(true) +end + +function Module:ifaceDown() + self:toggleFunc(false) +end + +function Module:networkRestart() + return os.execute("/etc/init.d/network restart") +end + +function Module:init(t) + local iface = t.iface + if iface then + self.iface = iface + if self.iface:match("^@") then + self.iface = self.iface:gsub("^@", "") + self.toggleFunc = self.toggleIface + else + self.toggleFunc = self.toggleDevice + end + end + self.attempts = tonumber(t.attempts) + self.deadPeriod = tonumber(t.dead_period) + self.restartTimeout = tonumber(t.restart_timeout) +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if currentStatus == 1 then + if self.attempts == 0 or self._attemptsCounter < self.attempts then + if self._deadCounter >= self.deadPeriod then + if self.iface then + self.syslog("info", string.format( + "%s: restarting network interface '%s'", self.name, self.iface)) + self:ifaceDown() + nixio.nanosleep(self.restartTimeout) + self:ifaceUp() + else + self.syslog("info", string.format("%s: restarting network", self.name)) + self:networkRestart() + end + self._deadCounter = 0 + self._attemptsCounter = self._attemptsCounter + 1 + else + self._deadCounter = self._deadCounter + timeDiff + end + end + else + self._attemptsCounter = 0 + self._deadCounter = 0 + end +end + +return Module diff --git a/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua b/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua new file mode 100644 index 0000000..ff55f12 --- /dev/null +++ b/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua @@ -0,0 +1,46 @@ + +local nixio = require("nixio") + +local Module = { + name = "mod_reboot", + config = {}, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + deadPeriod = 0, + forceRebootDelay = 0, + _deadCounter = 0, +} + +function Module:rebootDevice() + self.syslog("warning", string.format("%s: reboot", self.name)) + os.execute("/sbin/reboot &") + + if self.forceRebootDelay > 0 then + nixio.nanosleep(self.forceRebootDelay) + self.syslog("warning", string.format("%s: force reboot", self.name)) + self.writeValue("/proc/sys/kernel/sysrq", "1") + self.writeValue("/proc/sysrq-trigger", "b") + end +end + +function Module:init(t) + self.deadPeriod = tonumber(t.dead_period) + self.forceRebootDelay = tonumber(t.force_reboot_delay) +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if currentStatus == 1 then + if self._deadCounter >= self.deadPeriod then + self:rebootDevice() + self._deadCounter = 0 + else + self._deadCounter = self._deadCounter + timeDiff + end + + else + self._deadCounter = 0 + end +end + +return Module diff --git a/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua b/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua new file mode 100644 index 0000000..c5b67f1 --- /dev/null +++ b/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua @@ -0,0 +1,63 @@ + +local nixio = require("nixio") + +local Module = { + name = "mod_user_scripts", + config = {}, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + deadPeriod = 0, + alivePeriod = 0, + upScript = "", + downScript = "", + _deadCounter = 0, + _aliveCounter = 0, + _upScriptExecuted = true, + _downScriptExecuted = true, +} + +function Module:runExternalScript(scriptPath) + if nixio.fs.access(scriptPath, "x") then + os.execute(string.format('/bin/sh -c "%s" &', scriptPath)) + end +end + +function Module:init(t) + self.deadPeriod = tonumber(t.dead_period) + self.alivePeriod = tonumber(t.alive_period) + if self.config.configDir then + self.upScript = string.format("%s/up-script", self.config.configDir) + self.downScript = string.format("%s/down-script", self.config.configDir) + end +end + +function Module:run(currentStatus, lastStatus, timeDiff) + if currentStatus == 1 then + self._aliveCounter = 0 + self._downScriptExecuted = false + + if not self._upScriptExecuted then + if self._deadCounter >= self.deadPeriod then + self:runExternalScript(self.downScript) + self._upScriptExecuted = true + else + self._deadCounter = self._deadCounter + timeDiff + end + end + else + self._deadCounter = 0 + self._upScriptExecuted = false + + if not self._downScriptExecuted then + if self._aliveCounter >= self.alivePeriod then + self:runExternalScript(self.upScript) + self._downScriptExecuted = true + else + self._aliveCounter = self._aliveCounter + timeDiff + end + end + end +end + +return Module diff --git a/luci-app-internet-detector/Makefile b/luci-app-internet-detector/Makefile index fa56857..df28ee1 100644 --- a/luci-app-internet-detector/Makefile +++ b/luci-app-internet-detector/Makefile @@ -4,8 +4,8 @@ include $(TOPDIR)/rules.mk -PKG_VERSION:=0.4 -PKG_RELEASE:=3 +PKG_VERSION:=0.5 +PKG_RELEASE:=0 LUCI_TITLE:=LuCI support for internet-detector LUCI_DEPENDS:=+internet-detector LUCI_PKGARCH:=all diff --git a/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js b/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js index 68fad8f..e89b9c1 100644 --- a/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js +++ b/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js @@ -4,17 +4,83 @@ 'require rpc'; 'require uci'; 'require ui'; +'require tools.widgets as widgets' const btnStyleEnabled = 'btn cbi-button-save'; const btnStyleDisabled = 'btn cbi-button-reset'; const btnStyleApply = 'btn cbi-button-apply'; +var Timefield = ui.Textfield.extend({ + secToString: function(value) { + let string = '0'; + if(/^\d+$/.test(value)) { + value = Number(value); + if(value >= 3600 && (value % 3600) === 0) { + string = String(value / 3600) + 'h'; + } + else if(value >= 60 && (value % 60) === 0) { + string = String(value / 60) + 'm'; + } + else { + string = String(value) + 's'; + }; + }; + return string; + }, + + render: function() { + let frameEl = E('div', { 'id': this.options.id }), + inputEl = E('input', { + 'id' : this.options.id ? 'widget.' + this.options.id : null, + 'name' : this.options.name, + 'type' : 'text', + 'class' : 'cbi-input-text', + 'readonly' : this.options.readonly ? '' : null, + 'disabled' : this.options.disabled ? '' : null, + 'maxlength' : this.options.maxlength, + 'placeholder': this.options.placeholder, + 'value' : this.secToString(this.value), + }); + frameEl.appendChild(inputEl); + return this.bind(frameEl); + }, + + getValue: function() { + let rawValue = this.node.querySelector('input').value, + value = 0, + res = rawValue.match(/^(\d+)([hms]?)$/); + if(res) { + if(res[2] === 'h') { + value = Number(res[1]) * 3600; + } + else if(res[2] === 'm') { + value = Number(res[1]) * 60; + } + else if(!res[2] || res[2] === 's') { + value = Number(res[1]); + } + else { + value = 0; + }; + } else { + value = 0; + }; + return String(value); + }, + + setValue: function(value) { + let inputEl = this.node.querySelector('input'); + inputEl.value = this.secToString(value); + }, +}); + return L.view.extend({ + appName : 'internet-detector', execPath : '/usr/bin/internet-detector', upScriptPath : '/etc/internet-detector/up-script', downScriptPath : '/etc/internet-detector/down-script', - runScriptPath : '/etc/internet-detector/run-script', ledsPath : '/sys/class/leds', + mtaPath : '/usr/bin/mailsend', pollInterval : L.env.pollinterval, appStatus : 'stoped', initStatus : null, @@ -29,7 +95,9 @@ return L.view.extend({ uiCheckIntervalUp : null, uiCheckIntervalDown : null, currentAppMode : '0', - leds : null, + leds : [], + mm : false, + mta : false, callInitStatus: rpc.declare({ object: 'luci', @@ -38,19 +106,6 @@ return L.view.extend({ expect: { '': {} } }), - getInitStatus: function() { - return this.callInitStatus('internet-detector').then(res => { - if(res) { - return res['internet-detector'].enabled; - } else { - throw _('Command failed'); - } - }).catch(e => { - ui.addNotification(null, - E('p', _('Failed to get %s init status: %s').format('internet-detector', e))); - }); - }, - callInitAction: rpc.declare({ object: 'luci', method: 'setInitAction', @@ -58,26 +113,155 @@ return L.view.extend({ expect: { result: false } }), + getInitStatus: function() { + return this.callInitStatus(this.appName).then(res => { + if(res) { + return res[this.appName].enabled; + } else { + throw _('Command failed'); + } + }).catch(e => { + ui.addNotification(null, + E('p', _('Failed to get %s init status: %s').format(this.appName, e))); + }); + }, + handleServiceAction: function(action) { - return this.callInitAction('internet-detector', action).then(success => { + return this.callInitAction(this.appName, action).then(success => { if(!success) { throw _('Command failed'); }; return true; }).catch(e => { ui.addNotification(null, - E('p', _('Service action failed "%s %s": %s').format('internet-detector', action, e))); + E('p', _('Service action failed "%s %s": %s').format(this.appName, action, e))); }); }, serviceRestart: function(ev) { L.Poll.stop(); return this.handleServiceAction('restart').then(() => { - this.servicePoll(); + window.setTimeout(() => this.servicePoll(), 1000); L.Poll.start(); }); }, + CBITimeInput: form.Value.extend({ + __name__ : 'CBI.TimeInput', + + renderWidget: function(section_id, option_index, cfgvalue) { + let value = (cfgvalue != null) ? cfgvalue : this.default, + widget = new Timefield(value, { + id : this.cbid(section_id), + optional : this.optional || this.rmempty, + maxlength : 3, + placeholder: _('Type a time string'), + validate : L.bind( + function(section, value) { + return (/^$|^\d+[hms]?$/.test(value)) ? true : _('Expecting:') + + ` ${_('One of the following:')}\n - ${_('hours')}: 2h\n - ${_('minutes')}: 10m\n - ${_('seconds')}: 30s\n`; + }, + this, + section_id + ), + disabled : (this.readonly != null) ? this.readonly : this.map.readonly, + }); + return widget.render(); + }, + }), + + CBIBlockService: form.Value.extend({ + __name__ : 'CBI.BlockService', + + __init__ : function(map, section, ctx) { + this.map = map; + this.section = section; + this.ctx = ctx; + this.optional = true; + this.rmempty = true; + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + this.ctx.serviceButton = E('button', { + 'class': btnStyleApply, + 'click': ui.createHandlerFn(this.ctx, this.ctx.serviceRestart), + }, _('Restart')); + this.ctx.initButton = E('button', { + 'class': (!this.ctx.initStatus) ? btnStyleDisabled : btnStyleEnabled, + 'click': ui.createHandlerFn(this, () => { + return this.ctx.handleServiceAction( + (!this.ctx.initStatus) ? 'enable' : 'disable' + ).then(success => { + if(!success) { + return; + }; + if(!this.ctx.initStatus) { + this.ctx.initButton.textContent = _('Enabled'); + this.ctx.initButton.className = btnStyleEnabled; + this.ctx.initStatus = true; + } + else { + this.ctx.initButton.textContent = _('Disabled'); + this.ctx.initButton.className = btnStyleDisabled; + this.ctx.initStatus = false; + }; + }); + }), + }, (!this.ctx.initStatus) ? _('Disabled') : _('Enabled')); + + this.ctx.setInternetStatus(true); + + let serviceItems = ''; + if(this.ctx.currentAppMode === '2') { + serviceItems = E([ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, + _('Service') + ), + E('div', { 'class': 'cbi-value-field' }, + this.ctx.serviceStatusLabel + ), + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, + _('Restart service') + ), + E('div', { 'class': 'cbi-value-field' }, + this.ctx.serviceButton + ), + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, + _('Run service at startup') + ), + E('div', { 'class': 'cbi-value-field' }, + this.ctx.initButton + ), + ]), + ]); + }; + + let internetStatus = (this.ctx.currentAppMode !== '0') ? + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, + _('Internet status') + ), + E('div', { 'class': 'cbi-value-field' }, [ + this.ctx.inetStatusLabel, + (!this.ctx.inetStatus) ? this.ctx.inetStatusSpinner : '', + ]), + ]) + : ''; + + return E('div', { 'class': 'cbi-section' }, + E('div', { 'class': 'cbi-section-node' }, [ + internetStatus, + serviceItems, + ]) + ); + }, + }), + fileEditDialog: L.Class.extend({ __init__: function(file, title, description, callback, fileExists=false) { this.file = file; @@ -88,7 +272,7 @@ return L.view.extend({ }, load: function() { - return fs.read(this.file); + return L.resolveDefault(fs.read(this.file), ''); }, render: function(content) { @@ -105,7 +289,7 @@ return L.view.extend({ 'wrap': 'off', 'spellcheck': 'false', }, - content || '') + content) ) ), ]), @@ -201,20 +385,6 @@ return L.view.extend({ }; }, - CBIBlockTitle: form.DummyValue.extend({ - string: null, - - renderWidget: function(section_id, option_index, cfgvalue) { - this.title = this.description = null; - return E([ - E('label', { 'class': 'cbi-value-title' }), - E('div', { 'class': 'cbi-value-field' }, - E('b', {}, this.string) - ), - ]); - }, - }), - servicePoll: function() { return Promise.all([ fs.exec(this.execPath, [ 'status' ]), @@ -222,7 +392,6 @@ return L.view.extend({ ]).then(stat => { let curAppStatus = (stat[0].code === 0) ? stat[0].stdout.trim() : null; let curInetStatus = (stat[1].code === 0) ? stat[1].stdout.trim() : null; - if(this.inetStatus === curInetStatus && this.appStatus === curAppStatus) { return; }; @@ -262,92 +431,14 @@ return L.view.extend({ }); }, - serviceControlWidget: function() { - this.serviceButton = E('button', { - 'class': btnStyleApply, - 'click': ui.createHandlerFn(this, this.serviceRestart), - }, _('Restart')); - this.initButton = E('button', { - 'class': (!this.initStatus) ? btnStyleDisabled : btnStyleEnabled, - 'click': ui.createHandlerFn(this, () => { - return this.handleServiceAction( - (!this.initStatus) ? 'enable' : 'disable' - ).then(success => { - if(!success) { - return; - }; - if(!this.initStatus) { - this.initButton.textContent = _('Enabled'); - this.initButton.className = btnStyleEnabled; - this.initStatus = true; - } - else { - this.initButton.textContent = _('Disabled'); - this.initButton.className = btnStyleDisabled; - this.initStatus = false; - }; - }); - }), - }, (!this.initStatus) ? _('Disabled') : _('Enabled')); - - this.setInternetStatus(true); - - let serviceItems = ''; - if(this.currentAppMode === '2') { - serviceItems = E([ - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, - _('Service') - ), - E('div', { 'class': 'cbi-value-field' }, - this.serviceStatusLabel - ), - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, - _('Restart service') - ), - E('div', { 'class': 'cbi-value-field' }, - this.serviceButton - ), - ]), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, - _('Run service at startup') - ), - E('div', { 'class': 'cbi-value-field' }, - this.initButton - ), - ]), - ]); - }; - - let internetStatus = (this.currentAppMode !== '0') ? - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, - _('Internet status') - ), - E('div', { 'class': 'cbi-value-field' }, [ - this.inetStatusLabel, - (!this.inetStatus) ? this.inetStatusSpinner : '', - ]), - ]) - : ''; - - return E('div', { 'class': 'cbi-section fade-in' }, - E('div', { 'class': 'cbi-section-node' }, [ - internetStatus, - serviceItems, - ]) - ); - }, - load: function() { return Promise.all([ fs.exec(this.execPath, [ 'status' ]), this.getInitStatus(), - fs.list(this.ledsPath), - uci.load('internet-detector'), + L.resolveDefault(fs.list(this.ledsPath), []), + this.callInitStatus('modemmanager'), + L.resolveDefault(fs.stat(this.mtaPath), null), + uci.load(this.appName), ]).catch(e => { ui.addNotification(null, E('p', _('An error has occurred') + ': %s'.format(e.message))); }); @@ -360,73 +451,93 @@ return L.view.extend({ this.appStatus = (data[0].code === 0) ? data[0].stdout.trim() : null; this.initStatus = data[1]; this.leds = data[2]; - this.currentAppMode = uci.get('internet-detector', 'config', 'mode'); - this.uiCheckIntervalUp = Number(uci.get('internet-detector', 'ui_config', 'interval_up')); - this.uiCheckIntervalDown = Number(uci.get('internet-detector', 'ui_config', 'interval_down')); + if(data[3].modemmanager) { + this.mm = true; + }; + if(data[4]) { + this.mta = true; + }; + this.currentAppMode = uci.get(this.appName, 'config', 'mode'); + this.uiCheckIntervalUp = Number(uci.get(this.appName, 'config', 'ui_interval_up')); + this.uiCheckIntervalDown = Number(uci.get(this.appName, 'config', 'ui_interval_down')); - let upScriptEditDialog = new this.fileEditDialog( - this.upScriptPath, - _('up-script'), - _('Shell commands that run when connected to the Internet'), - ); - let downScriptEditDialog = new this.fileEditDialog( - this.downScriptPath, - _('down-script'), - _('Shell commands to run when disconnected from the Internet'), - ); - let runScriptEditDialog = new this.fileEditDialog( - this.runScriptPath, - _('run-script'), - _("Shell commands that are executed every time the Internet is checked for availability"), - ); + let s, o, ss; + let m = new form.Map(this.appName, + _('Internet detector'), + _('Checking Internet availability.')); - /* UCI sections */ + /* Service widget */ - let s, o; + s = m.section(form.NamedSection, 'config', 'main'); + o = s.option(this.CBIBlockService, this); - //// Main configuration - let mMain = new form.Map('internet-detector'); - s = mMain.section(form.NamedSection, 'config'); + /* Main settings */ + + s = m.section(form.NamedSection, 'config', 'main'); + + s.tab('main_configuration', _('Main settings')); // mode - o = s.option(form.ListValue, + let mode = s.taboption('main_configuration', form.ListValue, 'mode', _('Internet detector mode')); - o.value('0', _('Disabled')); - o.value('1', _('Web UI only')); - o.value('2', _('Service')); - o.description = '%s;
%s;
%s;'.format( - _('Disabled: detector is completely off'), - _('Web UI only: detector works only when the Web UI is open (UI detector)'), - _('Service: detector always runs as a system service') + mode.value('0', _('Disabled')); + mode.value('1', _('Web UI only')); + mode.value('2', _('Service')); + mode.description = '%s
%s
%s'.format( + _('Disabled: detector is completely off.'), + _('Web UI only: detector works only when the Web UI is open (UI detector).'), + _('Service: detector always runs as a system service.') ); // hosts - o = s.option(form.DynamicList, - 'hosts', _('Hosts')); - o.description = _('Hosts to check Internet availability. Hosts are polled (in list order) until at least one of them responds'); - o.datatype = 'or(host,hostport)'; + o = s.taboption('main_configuration', form.DynamicList, + 'hosts', _('Hosts'), + _('Hosts to check Internet availability. Hosts are polled (in list order) until at least one of them responds.') + ); + o.datatype = 'or(host,hostport)'; // check_type - o = s.option(form.ListValue, - 'check_type', _('Check type')); - o.description = _('Host availability check type'); + o = s.taboption('main_configuration', form.ListValue, + 'check_type', _('Check type'), + _('Host availability check type.') + ); o.value(0, _('TCP port connection')); o.value(1, _('Ping host')); // tcp_port - o = s.option(form.Value, - 'tcp_port', _('TCP port')); - o.description = _('Default port value for TCP connections'); - o.rmempty = false; - o.datatype = "port"; + o = s.taboption('main_configuration', form.Value, + 'tcp_port', _('TCP port'), + _('Default port value for TCP connections.') + ); + o.datatype = 'port'; + o.default = '53'; + o.depends({ check_type: '0' }); + + // ping_packet_size + o = s.taboption('main_configuration', form.ListValue, + 'ping_packet_size', _('Ping packet size')); + o.value(1, _('Small: 1 byte')); + o.value(32, _('Windows: 32 bytes')); + o.value(56, _('Standard: 56 bytes')); + o.value(248, _('Big: 248 bytes')); + o.value(1492, _('Huge: 1492 bytes')); + o.value(9000, _('Jumbo: 9000 bytes')); + o.default = '56'; + o.depends({ check_type: '1' }); + + // iface + o = s.taboption('main_configuration', widgets.DeviceSelect, + 'iface', _('Interface'), + _('Network interface for Internet access. If not specified, the default interface is used.') + ); + o.noaliases = true; - //// UI detector configuration + /* UI detector configuration */ - let mUi = new form.Map('internet-detector'); - s = mUi.section(form.NamedSection, 'ui_config'); + s.tab('ui_settings', _('UI detector configuration')); let makeUIIntervalOptions = L.bind(function(list) { list.value(1, '%d %s'.format(this.pollInterval, _('sec'))); @@ -438,38 +549,41 @@ return L.view.extend({ }, this); // interval_up - o = s.option(form.ListValue, - 'interval_up', _('Alive interval')); - o.description = _('Hosts polling interval when the Internet is up'); + o = s.taboption('ui_settings', form.ListValue, + 'ui_interval_up', _('Alive interval'), + _('Hosts polling interval when the Internet is up.') + ); makeUIIntervalOptions(o); // interval_down - o = s.option(form.ListValue, - 'interval_down', _('Dead interval')); - o.description = _('Hosts polling interval when the Internet is down'); + o = s.taboption('ui_settings', form.ListValue, + 'ui_interval_down', _('Dead interval'), + _('Hosts polling interval when the Internet is down.') + ); makeUIIntervalOptions(o); // connection_attempts - o = s.option(form.ListValue, - 'connection_attempts', _('Connection attempts')); - o.description = _('Maximum number of attempts to connect to each host'); + o = s.taboption('ui_settings', form.ListValue, + 'ui_connection_attempts', _('Connection attempts'), + _('Maximum number of attempts to connect to each host.') + ); o.value(1); o.value(2); o.value(3); // connection_timeout - o = s.option(form.ListValue, - 'connection_timeout', _('Connection timeout')); - o.description = _('Maximum timeout for waiting for a response from the host'); - o.value(1, "1 " + _('sec')); - o.value(2, "2 " + _('sec')); - o.value(3, "3 " + _('sec')); + o = s.taboption('ui_settings', form.ListValue, + 'ui_connection_timeout', _('Connection timeout'), + _('Maximum timeout for waiting for a response from the host.') + ); + o.value(1, '1 ' + _('sec')); + o.value(2, '2 ' + _('sec')); + o.value(3, '3 ' + _('sec')); - //// Service configuration + /* Service configuration */ - let mService = new form.Map('internet-detector'); - s = mService.section(form.NamedSection, 'service_config'); + s.tab('service_settings', _('Service configuration')); function makeIntervalOptions(list) { list.value(2, '2 ' + _('sec')); @@ -486,21 +600,24 @@ return L.view.extend({ } // interval_up - o = s.option(form.ListValue, - 'interval_up', _('Alive interval')); - o.description = _('Hosts polling interval when the Internet is up'); + o = s.taboption('service_settings', form.ListValue, + 'service_interval_up', _('Alive interval'), + _('Hosts polling interval when the Internet is up.') + ); makeIntervalOptions(o); // interval_down - o = s.option(form.ListValue, - 'interval_down', _('Dead interval')); - o.description = _('Hosts polling interval when the Internet is down'); + o = s.taboption('service_settings', form.ListValue, + 'service_interval_down', _('Dead interval'), + _('Hosts polling interval when the Internet is down.') + ); makeIntervalOptions(o); // connection_attempts - o = s.option(form.ListValue, - 'connection_attempts', _('Connection attempts')); - o.description = _('Maximum number of attempts to connect to each host'); + o = s.taboption('service_settings', form.ListValue, + 'service_connection_attempts', _('Connection attempts'), + _('Maximum number of attempts to connect to each host.') + ); o.value(1); o.value(2); o.value(3); @@ -508,163 +625,355 @@ return L.view.extend({ o.value(5); // connection_timeout - o = s.option( form.ListValue, - 'connection_timeout', _('Connection timeout')); - o.description = _('Maximum timeout for waiting for a response from the host'); - o.value(1, "1 " + _('sec')); - o.value(2, "2 " + _('sec')); - o.value(3, "3 " + _('sec')); - o.value(4, "4 " + _('sec')); - o.value(5, "5 " + _('sec')); - o.value(6, "6 " + _('sec')); - o.value(7, "7 " + _('sec')); - o.value(8, "8 " + _('sec')); - o.value(9, "9 " + _('sec')); - o.value(10, "10 " + _('sec')); + o = s.taboption('service_settings', form.ListValue, + 'service_connection_timeout', _('Connection timeout'), + _('Maximum timeout for waiting for a response from the host.') + ); + o.value(1, '1 ' + _('sec')); + o.value(2, '2 ' + _('sec')); + o.value(3, '3 ' + _('sec')); + o.value(4, '4 ' + _('sec')); + o.value(5, '5 ' + _('sec')); + o.value(6, '6 ' + _('sec')); + o.value(7, '7 ' + _('sec')); + o.value(8, '8 ' + _('sec')); + o.value(9, '9 ' + _('sec')); + o.value(10, '10 ' + _('sec')); // enable_logger - o = s.option(form.Flag, - 'enable_logger', _('Enable logging')); - o.description = _('Write messages to the system log'); - o.rmempty = false; - - // enable_up_script - o = s.option(form.Flag, - 'enable_up_script', _('Enable up-script')); - o.description = _('Execute commands when the Internet is connected'); - o.rmempty = false; - - // up_script edit dialog - o = s.option(form.Button, - '_up_script_btn', _('Edit up-script')); - o.onclick = () => upScriptEditDialog.show(); - o.inputtitle = _('Edit'); - o.inputstyle = 'edit btn'; - - // enable_down_script - o = s.option(form.Flag, - 'enable_down_script', _('Enable down-script')); - o.description = _('Execute commands when the Internet is disconnected'); - o.rmempty = false; - - // down_script edit dialog - o = s.option(form.Button, - '_down_script_btn', _('Edit down-script')); - o.onclick = () => downScriptEditDialog.show(); - o.inputtitle = _('Edit'); - o.inputstyle = 'edit btn'; - - // enable_run_script - o = s.option(form.Flag, - 'enable_run_script', _('Enable run-script')); - o.description = _('Execute commands every time the Internet is checked for availability'); - o.rmempty = false; - - // run_script edit dialog - o = s.option(form.Button, - '_run_script_btn', _('Edit run-script')); - o.onclick = () => runScriptEditDialog.show(); - o.inputtitle = _('Edit'); - o.inputstyle = 'edit btn'; + o = s.taboption('service_settings', form.Flag, + 'service_enable_logger', _('Enable logging'), + _('Write messages to the system log.') + ); + o.rmempty = false; /* Modules */ - //// LED control + s = m.section(form.NamedSection, 'config', 'main', + _('Service modules'), + _('Performing actions when connecting and disconnecting the Internet (available in the "Service" mode).')); - let mLed = new form.Map('internet-detector'); - s = mLed.section(form.NamedSection, 'mod_led_control'); + // LED control - o = s.option(this.CBIBlockTitle, '_dummy'); - o.string = _('LED control') + ':'; + s.tab('led_control', _('LED control')); - if(this.leds && this.leds.length > 0) { + o = s.taboption('led_control', form.SectionValue, 'mod_led_control', form.NamedSection, + 'mod_led_control', 'mod_led_control', + _('LED control'), + _('LED is on when Internet is available.')) + ss = o.subsection; + + if(this.leds.length > 0) { // enabled - o = s.option(form.Flag, 'enabled', - _('Enable LED control')); - o.rmempty = false; - o.description = _('LED is on when Internet is available.'); + o = ss.option(form.Flag, 'enabled', + _('Enable')); + o.rmempty = false; // led_name - o = s.option(form.ListValue, 'led_name', + o = ss.option(form.ListValue, 'led_name', _('LED Name')); - o.depends('enabled', '1'); + o.depends({ enabled: '1' }); this.leds.sort((a, b) => a.name > b.name); this.leds.forEach(e => o.value(e.name)); } else { - o = s.option(form.DummyValue, '_dummy'); + o = ss.option(form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('No LEDs available...') + '
'; }; + // Reboot device - /* Rendering */ + s.tab('reboot_device', _('Reboot device')); - let settingsNode = E('div', { 'class': 'cbi-section fade-in' }, - E('div', { 'class': 'cbi-section-node' }, - E('div', { 'class': 'cbi-value' }, - E('em', { 'class': 'spinning' }, _('Collecting data...')) - ) - ) + o = s.taboption('reboot_device', form.SectionValue, 'mod_reboot', form.NamedSection, + 'mod_reboot', 'mod_reboot', + _('Reboot device'), + _('Device will be rebooted when the Internet is disconnected.')); + ss = o.subsection; + + // enabled + o = ss.option(form.Flag, 'enabled', + _('Enable')); + o.rmempty = false; + + // dead_period + o = ss.option(this.CBITimeInput, + 'dead_period', _('Dead period'), + _('Longest period of time without Internet access until the device is rebooted.') + ); + o.rmempty = false; + + // force_reboot_delay + o = ss.option(form.ListValue, + 'force_reboot_delay', _('Forced reboot delay'), + _('Waiting for a reboot to complete before performing a forced reboot.') + ); + o.value(0, _('Disable forced reboot')); + o.value(60, '1 ' + _('min')); + o.value(120, '2 ' + _('min')); + o.value(300, '5 ' + _('min')); + o.value(600, '10 ' + _('min')); + o.value(1800, '30 ' + _('min')); + o.value(3600, '1 ' + _('hour')); + + // Restart network + + s.tab('restart_network', _('Restart network')); + + o = s.taboption('restart_network', form.SectionValue, 'mod_network_restart', form.NamedSection, + 'mod_network_restart', 'mod_network_restart', + _('Restart network'), + _('Network will be restarted when the Internet is disconnected.')); + ss = o.subsection; + + // enabled + o = ss.option(form.Flag, 'enabled', + _('Enable')); + o.rmempty = false; + + // dead_period + o = ss.option(this.CBITimeInput, + 'dead_period', _('Dead period'), + _('Longest period of time without Internet access before network restart.') + ); + o.rmempty = false; + + // attempts + o = ss.option(form.ListValue, + 'attempts', _('Restart attempts'), + _('Maximum number of network restart attempts before Internet access is available.') + ); + o.value(1); + o.value(2); + o.value(3); + o.value(4); + o.value(5); + + // iface + o = ss.option(widgets.DeviceSelect, 'iface', _('Interface'), + _('Network interface to restart. If not specified, then the network service is restarted.') ); - Promise.all([ - mMain.render(), - mUi.render(), - mService.render(), - mLed.render(), - ]).then(maps => { - let settingsTabs = E('div', { 'class': 'fade-in' }); - let tabsContainer = E('div', { 'class': 'cbi-section cbi-section-node-tabbed' }); - settingsTabs.append(tabsContainer); + // restart_timeout + o = ss.option(form.ListValue, + 'restart_timeout', _('Restart timeout'), + _('Timeout between stopping and starting the interface.') + ); + o.value(0, '0 ' + _('sec')); + o.value(1, '1 ' + _('sec')); + o.value(2, '2 ' + _('sec')); + o.value(3, '3 ' + _('sec')); + o.value(4, '4 ' + _('sec')); + o.value(5, '5 ' + _('sec')); + o.value(6, '6 ' + _('sec')); + o.value(7, '7 ' + _('sec')); + o.value(8, '8 ' + _('sec')); + o.value(9, '9 ' + _('sec')); + o.value(10, '10 ' + _('sec')); - // Main settings tab - let mainTab = E('div', { - 'data-tab' : 0, - 'data-tab-title': _('Main settings'), - }, [ - this.serviceControlWidget(), - maps[0], - ]); - tabsContainer.append(mainTab); + // Restart modem - // UI detector configuration tab - let uiTab = E('div', { - 'data-tab' : 1, - 'data-tab-title': _('UI detector configuration'), - }, maps[1]); - tabsContainer.append(uiTab); + s.tab('restart_modem', _('Restart modem')); - // Service configuration tab - let serviceTab = E('div', { - 'data-tab' : 2, - 'data-tab-title': _('Service configuration'), - }, [ - maps[2], - maps[3], - ]); - tabsContainer.append(serviceTab); + o = s.taboption('restart_modem', form.SectionValue, 'mod_modem_restart', form.NamedSection, + 'mod_modem_restart', 'mod_modem_restart', + _('Restart modem'), + _('Modem will be restarted when the Internet is disconnected.')); + ss = o.subsection; - ui.tabs.initTabGroup(tabsContainer.children); - settingsNode.replaceWith(settingsTabs); + if(this.mm) { - if(this.currentAppMode !== '0') { - L.Poll.add( - L.bind((this.currentAppMode === '2') ? this.servicePoll : this.uiPoll, this), - this.pollInterval - ); - }; - }).catch(e => ui.addNotification(null, E('p', {}, e.message))); + // enabled + o = ss.option(form.Flag, 'enabled', + _('Enable'), + ); + o.rmempty = false; - return E([ - E('h2', { 'class': 'fade-in' }, _('Internet detector')), - E('div', { 'class': 'cbi-section-descr fade-in' }, - _('Checking Internet availability.')), - settingsNode, - ]); + // dead_period + o = ss.option(this.CBITimeInput, + 'dead_period', _('Dead period'), + _('Longest period of time without Internet access before modem restart.') + ); + o.rmempty = false; + + // any_band + o = ss.option(form.Flag, + 'any_band', _('Unlock modem bands'), + _('Set the modem to be allowed to use any band.') + ); + o.rmempty = false; + + // iface + o = ss.option(widgets.NetworkSelect, 'iface', _('Interface'), + _('ModemManger interface. If specified, it will be restarted after restarting ModemManager.') + ); + o.multiple = false; + o.nocreate = true; + o.rmempty = true; + + } else { + o = ss.option(form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('ModemManager is not available...') + + '
'; + }; + + // Email notification + + s.tab('email', _('Email notification')); + + o = s.taboption('email', form.SectionValue, 'mod_email', form.NamedSection, + 'mod_email', 'mod_email', + _('Email notification'), + _('An email will be sent when the internet connection is restored after being disconnected.')); + ss = o.subsection; + + if(this.mta) { + + // enabled + o = ss.option(form.Flag, 'enabled', + _('Enable')); + o.rmempty = false; + + // alive_period + o = ss.option(this.CBITimeInput, + 'alive_period', _('Alive period'), + _('Longest period of time after connecting to the Internet before sending a message.') + ); + o.rmempty = false; + + // host_alias + o = ss.option(form.Value, 'host_alias', + _('Host alias'), + _('Host identifier in messages. If not specified, hostname will be used.')); + + // mail_recipient + o = ss.option(form.Value, + 'mail_recipient', _('Recipient')); + o.description = _('Email address of the recipient.'); + + // mail_sender + o = ss.option(form.Value, + 'mail_sender', _('Sender')); + o.description = _('Email address of the sender.'); + + // mail_user + o = ss.option(form.Value, + 'mail_user', _('User')); + o.description = _('Username for SMTP authentication.'); + + // mail_password + o = ss.option(form.Value, + 'mail_password', _('Password')); + o.description = _('Password for SMTP authentication.'); + o.password = true; + + // mail_smtp + o = ss.option(form.Value, + 'mail_smtp', _('SMTP server')); + o.description = _('Hostname/IP address of the SMTP server.'); + o.datatype = 'host'; + o.default = 'smtp.gmail.com'; + + // mail_smtp_port + o = ss.option(form.Value, + 'mail_smtp_port', _('SMTP server port')); + o.datatype = 'port'; + o.default = '587'; + + // mail_security + o = ss.option(form.ListValue, + 'mail_security', _('Security')); + o.description = '%s
%s'.format( + _('TLS: use STARTTLS if the server supports it.'), + _('SSL: SMTP over SSL.'), + ); + o.value('tls', 'TLS'); + o.value('ssl', 'SSL'); + o.default = 'tls'; + + } else { + o = ss.option(form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('Mailsend is not available...') + + '
'; + }; + + // User scripts + + let upScriptEditDialog = new this.fileEditDialog( + this.upScriptPath, + _('up-script'), + _('Shell commands that run when connected to the Internet.'), + ); + let downScriptEditDialog = new this.fileEditDialog( + this.downScriptPath, + _('down-script'), + _('Shell commands to run when disconnected from the Internet.'), + ); + + s.tab('user_scripts', _('User scripts')); + + o = s.taboption('user_scripts', form.SectionValue, 'mod_user_scripts', form.NamedSection, + 'mod_user_scripts', 'mod_user_scripts', + _('User scripts'), + _('Shell commands to run when connected or disconnected from the Internet.')); + ss = o.subsection; + + // enabled + o = ss.option(form.Flag, 'enabled', + _('Enable')); + o.rmempty = false; + + // up_script edit dialog + o = ss.option(form.Button, + '_up_script_btn', _('Edit up-script'), + _('Shell commands that run when connected to the Internet.') + ); + o.onclick = () => upScriptEditDialog.show(); + o.inputtitle = _('Edit'); + o.inputstyle = 'edit btn'; + + // alive_period + o = ss.option(this.CBITimeInput, + 'alive_period', _('Alive period'), + _('Longest period of time after connecting to Internet before "up-script" runs.') + ); + o.rmempty = false; + + // down_script edit dialog + o = ss.option(form.Button, + '_down_script_btn', _('Edit down-script'), + _('Shell commands to run when disconnected from the Internet.') + ); + o.onclick = () => downScriptEditDialog.show(); + o.inputtitle = _('Edit'); + o.inputstyle = 'edit btn'; + + // dead_period + o = ss.option(this.CBITimeInput, + 'dead_period', _('Dead period'), + _('Longest period of time after disconnecting from Internet before "down-script" runs.') + ); + o.rmempty = false; + + + if(this.currentAppMode !== '0') { + L.Poll.add( + L.bind((this.currentAppMode === '2') ? this.servicePoll : this.uiPoll, this), + this.pollInterval + ); + }; + + let map_promise = m.render(); + map_promise.then(node => node.classList.add('fade-in')); + return map_promise; }, handleSaveApply: function(ev, mode) { diff --git a/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js b/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js index 88026c2..cbc720e 100644 --- a/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js +++ b/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js @@ -4,6 +4,7 @@ return L.Class.extend({ title : _('Internet'), + appName : 'internet-detector', execPath : '/usr/bin/internet-detector', inetStatus : null, @@ -13,10 +14,10 @@ return L.Class.extend({ 'uiCheckIntervalDown' in window && 'currentAppMode' in window )) { - await uci.load('internet-detector').then(data => { - window.uiCheckIntervalUp = Number(uci.get('internet-detector', 'ui_config', 'interval_up')); - window.uiCheckIntervalDown = Number(uci.get('internet-detector', 'ui_config', 'interval_down')); - window.currentAppMode = uci.get('internet-detector', 'config', 'mode'); + await uci.load(this.appName).then(data => { + window.uiCheckIntervalUp = Number(uci.get(this.appName, 'config', 'ui_interval_up')); + window.uiCheckIntervalDown = Number(uci.get(this.appName, 'config', 'ui_interval_down')); + window.currentAppMode = uci.get(this.appName, 'config', 'mode'); }).catch(e => {}); }; diff --git a/luci-app-internet-detector/po/ru/internet-detector.po b/luci-app-internet-detector/po/ru/internet-detector.po index 033f736..a10cefb 100644 --- a/luci-app-internet-detector/po/ru/internet-detector.po +++ b/luci-app-internet-detector/po/ru/internet-detector.po @@ -9,24 +9,44 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.4.2\n" "Last-Translator: \n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? " +"1 : 2);\n" "Language: ru\n" +msgid "LED Name" +msgstr "Имя LED" + +msgid "LED control" +msgstr "Управление LED" + +msgid "" +"LED is on when Internet is " +"available." +msgstr "LED включен если Интернет доступен." + msgid "Alive interval" msgstr "Интервал при подключении" +msgid "Alive period" +msgstr "Период после подключения" + +msgid "" +"An email will be sent when the internet connection is restored after being " +"disconnected." +msgstr "Сообщение будет отправлено при восстановлении соединения с Интернет после отключения." + msgid "An error has occurred" msgstr "Произошла ошибка" +msgid "Big: 248 bytes" +msgstr "Большой: 248 байт" + msgid "Check type" msgstr "Тип проверки" msgid "Checking Internet availability." msgstr "Проверка доступности Интернет." -msgid "Collecting data..." -msgstr "Сбор данных..." - msgid "Command failed" msgstr "Команда не выполнена" @@ -39,23 +59,29 @@ msgstr "Попытки подключения" msgid "Connection timeout" msgstr "Таймаут соединения" -msgid "Connections" -msgstr "Подключения" - msgid "Contents have been saved." msgstr "Содержимое сохранено." msgid "Dead interval" msgstr "Интервал при отключении" -msgid "Default port value for TCP connections" -msgstr "Стандартное значение порта для TCP-подключений" +msgid "Dead period" +msgstr "Период после отключения" + +msgid "Default port value for TCP connections." +msgstr "Стандартное значение порта для TCP-подключений." + +msgid "Device will be rebooted when the Internet is disconnected." +msgstr "Устройство будет перезагружено при отключении Интернет." + +msgid "Disable forced reboot" +msgstr "Отключить принудительную перезагрузку" msgid "Disabled" msgstr "Отключен" -msgid "Disabled: detector is completely off" -msgstr "Отключен: детектор полностью выключен" +msgid "Disabled: detector is completely off." +msgstr "Отключен: детектор полностью выключен." msgid "Disconnected" msgstr "Отключен" @@ -69,53 +95,69 @@ msgstr "Изменить" msgid "Edit down-script" msgstr "Изменить down-script" -msgid "Edit run-script" -msgstr "Изменить run-script" - msgid "Edit up-script" msgstr "Изменить up-script" -msgid "Enable down-script" -msgstr "Включить down-script" +msgid "Email notification" +msgstr "Уведомление по email" + +msgid "Email address of the recipient." +msgstr "Email-адрес получателя." + +msgid "Email address of the sender." +msgstr "Email-адрес отправителя." + +msgid "Enable" +msgstr "Включить" msgid "Enable logging" msgstr "Включить запись событий в лог" -msgid "Enable run-script" -msgstr "Включить run-script" - -msgid "Enable up-script" -msgstr "Включить up-script" - msgid "Enabled" msgstr "Включен" -msgid "Execute commands every time the Internet is checked for availability" -msgstr "Выполнение команд при каждой проверке доступности Интернет" - -msgid "Execute commands when the Internet is connected" -msgstr "Выполнение команд при подключении к Интернет" - -msgid "Execute commands when the Internet is disconnected" -msgstr "Выполнение команд при отключении от Интернет" +msgid "Expecting:" +msgstr "Ожидается:" msgid "Failed to get %s init status: %s" msgstr "Не удалось получить статус инициализации %s: %s" -msgid "Host availability check type" -msgstr "Тип проверки доступности хоста" +msgid "Forced reboot delay" +msgstr "Задержка принудительной перезагрузки" + +msgid "Host alias" +msgstr "Псевдоним хоста" + +msgid "Host availability check type." +msgstr "Тип проверки доступности хоста." + +msgid "Host identifier in messages. If not specified, hostname will be used." +msgstr "Идентификатор хоста в сообщениях. Если не указан, будет использовано имя хоста." + +msgid "Hostname/IP address of the SMTP server." +msgstr "Имя хоста/IP-адрес SMTP-сервера." msgid "Hosts" msgstr "Хосты" -msgid "Hosts polling interval when the Internet is down" -msgstr "Интервал опроса хостов если Интернет не доступен" +msgid "Hosts polling interval when the Internet is down." +msgstr "Интервал опроса хостов если Интернет не доступен." -msgid "Hosts polling interval when the Internet is up" -msgstr "Интервал опроса хостов если Интернет доступен" +msgid "Hosts polling interval when the Internet is up." +msgstr "Интервал опроса хостов если Интернет доступен." -msgid "Hosts to check Internet availability. Hosts are polled (in list order) until at least one of them responds" -msgstr "Хосты для проверки доступности Интернет. Хосты опрашиваются (в порядке списка) до тех пор, пока хотя бы один из них не ответит" +msgid "" +"Hosts to check Internet availability. Hosts are polled (in list order) until " +"at least one of them responds." +msgstr "" +"Хосты для проверки доступности Интернет. Хосты опрашиваются (в порядке " +"списка) до тех пор, пока хотя бы один из них не ответит." + +msgid "Huge: 1492 bytes" +msgstr "Огромный: 1492 байта" + +msgid "Interface" +msgstr "Интерфейс" msgid "Internet" msgstr "Интернет" @@ -129,36 +171,169 @@ msgstr "Режим интернет-детектора" msgid "Internet status" msgstr "Статус Интернет" +msgid "Jumbo: 9000 bytes" +msgstr "Гигантский: 9000 байт" + +msgid "LED control" +msgstr "Управление LED" + msgid "Loading" msgstr "Загрузка" +msgid "" +"Longest period of time after connecting to Internet before \"up-script\" " +"runs." +msgstr "" +"Максимальный промежуток времени после подключения к Интернет перед запуском " +"\"up-script\"." + +msgid "" +"Longest period of time after connecting to the Internet before sending a " +"message." +msgstr "Максимальный промежуток времени после отключения Интернет перед отправкой сообщения." + +msgid "" +"Longest period of time after disconnecting from Internet before \"down-script" +"\" runs." +msgstr "" +"Максимальный промежуток времени после отключения Интернет перед запуском " +"\"down-script\"." + +msgid "Longest period of time without Internet access before modem restart." +msgstr "" +"Максимальное время отсутствия доступа в Интренет перед перезапуском модема." + +msgid "Longest period of time without Internet access before network restart." +msgstr "" +"Максимальное время отсутствия доступа в Интренет перед перезапуском сети." + +msgid "" +"Longest period of time without Internet access until the device is rebooted." +msgstr "" +"Максимальное время отсутствия доступа в Интренет перед перезагрузкой " +"устройства." + +msgid "Mailsend is not available..." +msgstr "Mailsend недоступен..." + msgid "Main settings" msgstr "Основные настройки" -msgid "Maximum timeout for waiting for a response from the host" -msgstr "Маскимальный таймаут ожидания ответа от хоста" +msgid "Maximum number of attempts to connect to each host." +msgstr "Максимальное количество попыток подключения к каждому хосту." -msgid "Maximum number of attempts to connect to each host" -msgstr "Максимальное количество попыток подключения к каждому хосту" +msgid "" +"Maximum number of network restart attempts before Internet access is " +"available." +msgstr "" +"Максимальное количество попыток перезапуска сети до появления доступа в " +"Интренет." + +msgid "Maximum timeout for waiting for a response from the host." +msgstr "Максимальный таймаут ожидания ответа от хоста." + +msgid "Modem will be restarted when the Internet is disconnected." +msgstr "Модем будет перезапущен при отключении Интернет." + +msgid "ModemManager is not available..." +msgstr "ModemManager недоступен..." + +msgid "" +"ModemManger interface. If specified, it will be restarted after restarting " +"ModemManager." +msgstr "" +"Интерфейс ModemManager. Если задан, то будет перезапущен после перезапуска " +"ModemManger." + +msgid "" +"Network interface for Internet access. If not specified, the default " +"interface is used." +msgstr "" +"Сетевой интерфейс для доступа в Интернет. Если не указан, используется " +"интерфейс по умолчанию." + +msgid "" +"Network interface to restart. If not specified, then the network service is restarted." +msgstr "" +"Сетевой интерфейс для перезапуска. Если не задан, то будет перезапущена сетевая " +"служба." + +msgid "Network will be restarted when the Internet is disconnected." +msgstr "Сеть будет перезапущена при отключении Интернет." + +msgid "No LEDs available..." +msgstr "Нет доступных LED..." + +msgid "One of the following:" +msgstr "Одно из следующих значений:" + +msgid "Password" +msgstr "Пароль" + +msgid "Password for SMTP authentication." +msgstr "Пароль для SMTP-аутентификации." + +msgid "" +"Performing actions when connecting and disconnecting the Internet (available " +"in the \"Service\" mode)." +msgstr "" +"Выполнение действий при подключении и отключении Интернет (доступно в режиме " +"\"Служба\")." msgid "Ping host" msgstr "Пинг хоста" +msgid "Ping packet size" +msgstr "Размер пакета Ping" + +msgid "Reboot device" +msgstr "Перезагрузка устройства" + +msgid "Recipient" +msgstr "Получатель" + msgid "Restart" msgstr "Перезапуск" +msgid "Restart attempts" +msgstr "Попытки перезапуска" + +msgid "Restart modem" +msgstr "Перезапуск модема" + +msgid "Restart network" +msgstr "Перезапуск сети" + msgid "Restart service" msgstr "Перезапуск службы" +msgid "Restart timeout" +msgstr "Таймаут перезапуска" + msgid "Run service at startup" msgstr "Запуск службы при старте" msgid "Running" msgstr "Выполняется" +msgid "SMTP server" +msgstr "SMTP-сервер" + +msgid "SMTP server port" +msgstr "Порт SMTP-сервера" + +msgid "SSL: SMTP over SSL." +msgstr "SSL: SMTP поверх SSL." + msgid "Save" msgstr "Сохранить" +msgid "Security" +msgstr "Безопасность" + +msgid "Sender" +msgstr "Отправитель" + msgid "Service" msgstr "Служба" @@ -168,17 +343,29 @@ msgstr "Не удалось выполнить действие службы \"% msgid "Service configuration" msgstr "Конфигурация службы" -msgid "Service: detector always runs as a system service" -msgstr "Служба: детектор работает постоянно, как системная служба" +msgid "Service modules" +msgstr "Модули службы" -msgid "Shell commands that are executed every time the Internet is checked for availability" -msgstr "Команды shell выполняемые при каждой проверке доступности Интернет" +msgid "Service: detector always runs as a system service." +msgstr "Служба: детектор работает постоянно, как системная служба." -msgid "Shell commands that run when connected to the Internet" -msgstr "Команды shell выполняемые при подключении к Интернет" +msgid "Set the modem to be allowed to use any band." +msgstr "Разрешить модему использование любой частоты." -msgid "Shell commands to run when disconnected from the Internet" -msgstr "Команды shell выполняемые при отключении от Интернет" +msgid "Shell commands that run when connected to the Internet." +msgstr "Команды shell выполняемые при подключении к Интернет." + +msgid "Shell commands to run when connected or disconnected from the Internet." +msgstr "Команды shell выполняемые при подключении или отключении Интернет." + +msgid "Shell commands to run when disconnected from the Internet." +msgstr "Команды shell выполняемые при отключении от Интернет." + +msgid "Small: 1 byte" +msgstr "Маленький: 1 байт" + +msgid "Standard: 56 bytes" +msgstr "Стандартный: 56 байт" msgid "Stopped" msgstr "Остановлена" @@ -189,6 +376,15 @@ msgstr "TCP-порт" msgid "TCP port connection" msgstr "Подключение к TCP-порту" +msgid "TLS: use STARTTLS if the server supports it." +msgstr "TLS: использовать STARTTLS если сервер поддерживает." + +msgid "Timeout between stopping and starting the interface." +msgstr "Таймаут между остановкой и запуском интерфейса." + +msgid "Type a time string" +msgstr "Введите строку времени" + msgid "UI detector configuration" msgstr "Конфигурация UI детектора" @@ -201,41 +397,57 @@ msgstr "Невозможно сохранить содержимое" msgid "Undefined" msgstr "Неопределён" +msgid "Unlock modem bands" +msgstr "Освободить частоты модема" + +msgid "User" +msgstr "Пользователь" + +msgid "User scripts" +msgstr "Пользовательские скрипты" + +msgid "Username for SMTP authentication." +msgstr "Имя пользователя для SMTP-аутентификации." + +msgid "Waiting for a reboot to complete before performing a forced reboot." +msgstr "" +"Ожидание завершения перезагрузки перед выполнением принудительной " +"перезагрузки." + msgid "Web UI only" msgstr "Только web-интерфейс" -msgid "Web UI only: detector works only when the Web UI is open (UI detector)" -msgstr "Только web-интерфейс: детектор работает только в web-интерфейсе (UI детектор)" +msgid "Web UI only: detector works only when the Web UI is open (UI detector)." +msgstr "" +"Только web-интерфейс: детектор работает только в web-интерфейсе (UI " +"детектор)." -msgid "Write messages to the system log" -msgstr "Записывать сообщения в системный журнал" +msgid "Windows: 32 bytes" +msgstr "Windows: 32 байта" + +msgid "Write messages to the system log." +msgstr "Записывать сообщения в системный журнал." msgid "down-script" -msgstr "" +msgstr "down-script" + +msgid "hour" +msgstr "час" + +msgid "hours" +msgstr "часы" msgid "min" msgstr "мин" -msgid "run-script" -msgstr "" +msgid "minutes" +msgstr "минуты" msgid "sec" msgstr "сек" +msgid "seconds" +msgstr "секунды" + msgid "up-script" -msgstr "" - -msgid "LED control" -msgstr "Управление LED" - -msgid "LED is on when Internet is available." -msgstr "LED включен если Интернет доступен." - -msgid "LED Name" -msgstr "Имя LED" - -msgid "Enable LED control" -msgstr "Включить управление LED" - -msgid "No LEDs available..." -msgstr "Нет доступных LED..." +msgstr "up-script" diff --git a/luci-app-internet-detector/po/templates/internet-detector.pot b/luci-app-internet-detector/po/templates/internet-detector.pot index f818fa2..0601987 100644 --- a/luci-app-internet-detector/po/templates/internet-detector.pot +++ b/luci-app-internet-detector/po/templates/internet-detector.pot @@ -1,21 +1,40 @@ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" +msgid "LED Name" +msgstr "" + +msgid "LED control" +msgstr "" + +msgid "" +"LED is on when Internet is " +"available." +msgstr "" + msgid "Alive interval" msgstr "" +msgid "Alive period" +msgstr "" + +msgid "" +"An email will be sent when the internet connection is restored after being " +"disconnected." +msgstr "" + msgid "An error has occurred" msgstr "" +msgid "Big: 248 bytes" +msgstr "" + msgid "Check type" msgstr "" msgid "Checking Internet availability." msgstr "" -msgid "Collecting data..." -msgstr "" - msgid "Command failed" msgstr "" @@ -28,22 +47,28 @@ msgstr "" msgid "Connection timeout" msgstr "" -msgid "Connections" -msgstr "" - msgid "Contents have been saved." msgstr "" msgid "Dead interval" msgstr "" -msgid "Default port value for TCP connections" +msgid "Dead period" +msgstr "" + +msgid "Default port value for TCP connections." +msgstr "" + +msgid "Device will be rebooted when the Internet is disconnected." +msgstr "" + +msgid "Disable forced reboot" msgstr "" msgid "Disabled" msgstr "" -msgid "Disabled: detector is completely off" +msgid "Disabled: detector is completely off." msgstr "" msgid "Disconnected" @@ -58,52 +83,66 @@ msgstr "" msgid "Edit down-script" msgstr "" -msgid "Edit run-script" -msgstr "" - msgid "Edit up-script" msgstr "" -msgid "Enable down-script" +msgid "Email notification" +msgstr "" + +msgid "Email address of the recipient." +msgstr "" + +msgid "Email address of the sender." +msgstr "" + +msgid "Enable" msgstr "" msgid "Enable logging" msgstr "" -msgid "Enable run-script" -msgstr "" - -msgid "Enable up-script" -msgstr "" - msgid "Enabled" msgstr "" -msgid "Execute commands every time the Internet is checked for availability" -msgstr "" - -msgid "Execute commands when the Internet is connected" -msgstr "" - -msgid "Execute commands when the Internet is disconnected" +msgid "Expecting:" msgstr "" msgid "Failed to get %s init status: %s" msgstr "" -msgid "Host availability check type" +msgid "Forced reboot delay" +msgstr "" + +msgid "Host alias" +msgstr "" + +msgid "Host availability check type." +msgstr "" + +msgid "Host identifier in messages. If not specified, hostname will be used." +msgstr "" + +msgid "Hostname/IP address of the SMTP server." msgstr "" msgid "Hosts" msgstr "" -msgid "Hosts polling interval when the Internet is down" +msgid "Hosts polling interval when the Internet is down." msgstr "" -msgid "Hosts polling interval when the Internet is up" +msgid "Hosts polling interval when the Internet is up." msgstr "" -msgid "Hosts to check Internet availability. Hosts are polled (in list order) until at least one of them responds" +msgid "" +"Hosts to check Internet availability. Hosts are polled (in list order) until " +"at least one of them responds." +msgstr "" + +msgid "Huge: 1492 bytes" +msgstr "" + +msgid "Interface" msgstr "" msgid "Internet" @@ -118,33 +157,151 @@ msgstr "" msgid "Internet status" msgstr "" +msgid "Jumbo: 9000 bytes" +msgstr "" + +msgid "LED control" +msgstr "" + msgid "Loading" msgstr "" +msgid "" +"Longest period of time after connecting to Internet before \"up-script\" " +"runs." +msgstr "" + +msgid "" +"Longest period of time after connecting to the Internet before sending a " +"message." +msgstr "" + +msgid "" +"Longest period of time after disconnecting from Internet before \"down-script" +"\" runs." +msgstr "" + +msgid "Longest period of time without Internet access before modem restart." +msgstr "" + +msgid "Longest period of time without Internet access before network restart." +msgstr "" + +msgid "" +"Longest period of time without Internet access until the device is rebooted." +msgstr "" + +msgid "Mailsend is not available..." +msgstr "" + msgid "Main settings" msgstr "" -msgid "Maximum timeout for waiting for a response from the host" +msgid "Maximum number of attempts to connect to each host." msgstr "" -msgid "Maximum number of attempts to connect to each host" +msgid "" +"Maximum number of network restart attempts before Internet access is " +"available." +msgstr "" + +msgid "Maximum timeout for waiting for a response from the host." +msgstr "" + +msgid "Modem will be restarted when the Internet is disconnected." +msgstr "" + +msgid "ModemManager is not available..." +msgstr "" + +msgid "" +"ModemManger interface. If specified, it will be restarted after restarting " +"ModemManager." +msgstr "" + +msgid "" +"Network interface for Internet access. If not specified, the default " +"interface is used." +msgstr "" + +msgid "" +"Network interface to restart. If not specified, then the network service is restarted." +msgstr "" + +msgid "Network will be restarted when the Internet is disconnected." +msgstr "" + +msgid "No LEDs available..." +msgstr "" + +msgid "One of the following:" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password for SMTP authentication." +msgstr "" + +msgid "" +"Performing actions when connecting and disconnecting the Internet (available " +"in the \"Service\" mode)." msgstr "" msgid "Ping host" msgstr "" +msgid "Ping packet size" +msgstr "" + +msgid "Reboot device" +msgstr "" + +msgid "Recipient" +msgstr "" + msgid "Restart" msgstr "" +msgid "Restart attempts" +msgstr "" + +msgid "Restart modem" +msgstr "" + +msgid "Restart network" +msgstr "" + msgid "Restart service" msgstr "" +msgid "Restart timeout" +msgstr "" + msgid "Run service at startup" msgstr "" msgid "Running" msgstr "" +msgid "SMTP server" +msgstr "" + +msgid "SMTP server port" +msgstr "" + +msgid "SSL: SMTP over SSL." +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Security" +msgstr "" + +msgid "Sender" +msgstr "" + msgid "Service" msgstr "" @@ -154,16 +311,28 @@ msgstr "" msgid "Service configuration" msgstr "" -msgid "Service: detector always runs as a system service" +msgid "Service modules" msgstr "" -msgid "Shell commands that are executed every time the Internet is checked for availability" +msgid "Service: detector always runs as a system service." msgstr "" -msgid "Shell commands that run when connected to the Internet" +msgid "Set the modem to be allowed to use any band." msgstr "" -msgid "Shell commands to run when disconnected from the Internet" +msgid "Shell commands that run when connected to the Internet." +msgstr "" + +msgid "Shell commands to run when connected or disconnected from the Internet." +msgstr "" + +msgid "Shell commands to run when disconnected from the Internet." +msgstr "" + +msgid "Small: 1 byte" +msgstr "" + +msgid "Standard: 56 bytes" msgstr "" msgid "Stopped" @@ -175,6 +344,15 @@ msgstr "" msgid "TCP port connection" msgstr "" +msgid "TLS: use STARTTLS if the server supports it." +msgstr "" + +msgid "Timeout between stopping and starting the interface." +msgstr "" + +msgid "Type a time string" +msgstr "" + msgid "UI detector configuration" msgstr "" @@ -187,41 +365,53 @@ msgstr "" msgid "Undefined" msgstr "" +msgid "Unlock modem bands" +msgstr "" + +msgid "User" +msgstr "" + +msgid "User scripts" +msgstr "" + +msgid "Username for SMTP authentication." +msgstr "" + +msgid "Waiting for a reboot to complete before performing a forced reboot." +msgstr "" + msgid "Web UI only" msgstr "" -msgid "Web UI only: detector works only when the Web UI is open (UI detector)" +msgid "Web UI only: detector works only when the Web UI is open (UI detector)." msgstr "" -msgid "Write messages to the system log" +msgid "Windows: 32 bytes" +msgstr "" + +msgid "Write messages to the system log." msgstr "" msgid "down-script" msgstr "" +msgid "hour" +msgstr "" + +msgid "hours" +msgstr "" + msgid "min" msgstr "" -msgid "run-script" +msgid "minutes" msgstr "" msgid "sec" msgstr "" +msgid "seconds" +msgstr "" + msgid "up-script" msgstr "" - -msgid "LED control" -msgstr "" - -msgid "LED is on when Internet is available." -msgstr "" - -msgid "LED Name" -msgstr "" - -msgid "Enable LED control" -msgstr "" - -msgid "No LEDs available..." -msgstr "" diff --git a/luci-app-internet-detector/root/usr/share/luci/menu.d/luci-app-internet-detector.json b/luci-app-internet-detector/root/usr/share/luci/menu.d/luci-app-internet-detector.json index 9b59524..4bcd09e 100644 --- a/luci-app-internet-detector/root/usr/share/luci/menu.d/luci-app-internet-detector.json +++ b/luci-app-internet-detector/root/usr/share/luci/menu.d/luci-app-internet-detector.json @@ -10,7 +10,8 @@ "acl": [ "luci-app-internet-detector" ], "fs": { "/usr/bin/internet-detector": "executable" - } + }, + "uci": { "internet-detector": true } } } } diff --git a/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json b/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json index 4f53d74..7a49cf0 100644 --- a/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json +++ b/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json @@ -6,8 +6,8 @@ "/sys/class/leds": [ "list" ], "/etc/internet-detector/up-script": [ "read" ], "/etc/internet-detector/down-script": [ "read" ], - "/etc/internet-detector/run-script": [ "read" ], - "/usr/bin/internet-detector*": [ "exec" ] + "/usr/bin/internet-detector*": [ "exec" ], + "/usr/bin/mailsend": [ "exec" ] }, "uci": [ "internet-detector" ], "ubus": { @@ -17,8 +17,7 @@ "write": { "file": { "/etc/internet-detector/up-script": [ "write" ], - "/etc/internet-detector/down-script": [ "write" ], - "/etc/internet-detector/run-script": [ "write" ] + "/etc/internet-detector/down-script": [ "write" ] }, "uci": [ "internet-detector" ] } diff --git a/screenshots/01.jpg b/screenshots/01.jpg index c411d40..9fd03fd 100644 Binary files a/screenshots/01.jpg and b/screenshots/01.jpg differ diff --git a/screenshots/02.jpg b/screenshots/02.jpg index a023073..7f56f84 100644 Binary files a/screenshots/02.jpg and b/screenshots/02.jpg differ diff --git a/screenshots/03.jpg b/screenshots/03.jpg deleted file mode 100644 index a76c483..0000000 Binary files a/screenshots/03.jpg and /dev/null differ diff --git a/screenshots/04.jpg b/screenshots/04.jpg deleted file mode 100644 index 7f56f84..0000000 Binary files a/screenshots/04.jpg and /dev/null differ