【AsusWrt】华硕梅林固件路由器信息监控之【命令行py脚本实现法】
Sensor.Command_line
Python脚本
经验分享
萌新探索系列
憋说话,先发图。
更新
- 4月27日:
计划新增用于不可描述之事的命令:
_CMD_SSFS = 'nvram get ss_foreign_state'
_CMD_SSCS = 'nvram get ss_china_state'
前言
最近,萌新楼主拜读了囧帅大大@Jone的神贴:梅林路由器CPU和无线芯片温度接入Home Assistant之后,有所启发。结合前期自己利用HA对路由器等网络设备进行信息监控的一些探索和经验,本楼主写了这段在HA上实现的AsusWrt固件路由器信息监控的Py脚本,并配以相应的配置文件,希望对您有所帮助。
想法
编写这个脚本的初衷纯粹是为了把玩,即用其它方法实现囧帅大大帖子中对路由器信息的监控,再就是为以后编写关于AsusWrt的自定义组件积累经验。
有兴趣的同学可以先看看囧帅大大的方法,思路很棒,利用路由器的JFFS存储并自动执行脚本获取路由器数据,然后路由器将这些信息以JSON序列主动向HA的API进行发送。囧帅大大的方法很妙,最关键是不占HA资源,HA不需要向路由器请求,只需要接收数据。
然后我这个方法,其实是土办法,在HA加载一个命令行传感器,命令行传感器执行的是一个py脚本,在脚本中通过SSH登录到路由器并通过运行命令行获得数据,数据经过正则表达式等的修正,成为有效载荷,作为HA命令行传感器的state传回到HA平台。
这种方法虽然土,但一样有效,因为只用了一个命令行传感器执行脚本,避免了SSH疯狂并发工作导致路由器Ban掉HA系统ip的窘境,经过在群晖docker下的HA上实测,工作还算稳定,虽然脚本很单薄,没有太多的异常处置,但是目前还没有报过任何错误。然后就是在本方法中路由器只需要打开SSH,JFFS之类不用打开(最关键可能就是这点,满足了我私人的需求,因为我就是纠结地银,我不想单独为监控信息这种类型的功能打开JFFS,这方法对HA来说很妙,但对路由器来说意义却不大...而且感觉这种方法的远景建设性程度比较受限,总之我个人不足以说服自己)。
那么这个方法是好方法么,当然不是啊!楼主我自己也说了,本文这种方法只能用于把玩把玩,最好的办法还是编写自定义组件!(当然,没有组件之前随便用用并没有啥问题。)
本文方法的局限性有以下几点:
- 这种方法中提取芯片温度的手段略显粗糙,路由器固件自身是通过ioctl函数获取温度值的,下步看看怎么改进。
- 本文中没有提取上传下载量,这属于应该被提取的信息,下步改进。
- 通过这种方法传回来的state值,最大只能255字节,HA你妹的设定,我本来是准备直接传回一整段JSON序列的,但是采集了那么一点点数据,一看JSON数据大小就要500多字节,只能砍掉字典的键、保留值并做成列表形式(砍完正好250字节左右,我地个神),字符串传回来,最后重建列表提取信息。这个问题单单在脚本中无法解决,脚本毕竟是脚本,毕竟是受限于HA的,当然破解方法也不是没有,比如在脚本中把JSON序列保存为HA本地文件,再通过另外的HA传感器把文件读出来......
- 使用这种方法虽然避免了SSH连接请求短时间疯狂并发的情况,但是每一次运行命令行完毕必然导致SSH关闭,下次请求再打开,我现在设定是每30秒运行一次命令行脚本,在路由器上看到的日志就是ssh不停地打开和关闭......这个问题还是要通过编写自定义组件来解决吧。
- 最后就是这种方法其实和系统自带的
device_tracker.asuswrt
组件有很多方法是相通的重复的,未来编写自定义组件应该考虑合并掉DeviceTrack.AsusWrt的功能,有时觉得这个track组件真心不好用,看看能否改进和增加新功能,比如利用wl rssi
命令,通过连接无线网络设备的MAC地址,反查设备的信号强度,以此作为tracker等功能判定的依据。不过前几天看了下device_tracker
这个组件,核心文件__init__.py
就比sensor
组件的要复杂得多,处处涉及异步编程,光配置平台就有4种方法:async_get_scanner
、get_scanner
、async_setup_scanner
、setup_scanner
,让萌新我看得云里雾里......哎,我是一月前连python是啥都不知道,Linux一个命令也不懂的人啊,正要命。
文件
asuswrt_mirukuteii.py
建议在HA系统根目录下找个地方放这个文件,比如我新建了一个script文件夹,专门放脚本文件。
#############################################
# AsusWrt_MRKT #
#############################################
# 本脚本为收集AsusWrt固件的路由器信息而编写 #
# 实现方法:在HA中调用命令行传感器运行本脚本 #
# 本脚本通过SSH连接AsusWrtRouter, #
# 取得相应信息并返回至命令行传感器。 #
# 作者:Mirukuteii@hasssbian 2018-4-25 #
#############################################
import re
REQUIREMENTS = ['pexpect==4.0.1']
#SSH连接参数,请按路由器实际填写
_RT_IP = 'XX.XX.XX.XX'
_RT_PT = 'XXXX'
_RT_USR = 'XXXXXXX'
#SSH密码和证书两种方式2选1,空着的那个直接填''即可
_RT_SSHPWD = 'XXXXXXXX'
_RT_SSHKEYPATH = '/XX/.ssh/id_rsa'
#命令行组
_CMD_NAME = 'nvram get computer_name'
_CMD_WANIP = 'nvram get wan_ipaddr'
_CMD_LANIP = 'nvram get lan_ipaddr'
_CMD_MAC = 'nvram get lan_hwaddr'
_CMD_UPTIME = 'uptime'
_CMD_CPUTEMP = "cat /proc/dmu/temperature |sed -e 's/[^0-9]//g'"
_CMD_24GTEMP = "wl -i eth1 phy_tempsense |awk '{print $1 / 2 + 20}'"
_CMD_5GTEMP = "wl -i eth2 phy_tempsense |awk '{print $1 / 2 + 20}'"
_CMD_24GTXPWR = 'wl -i eth1 txpwr_target_max'
_CMD_5GTXPWR = 'wl -i eth2 txpwr_target_max'
_CMD_MEM = 'top -n 1 -b |grep ^Mem'
#_CMD_RSSI = wl -i eth1 rssi MAC
#正则公式组
_REGEX_NAME = re.compile(
r'(?P<router_name>(.+))\s+')
_REGEX_WANIP = re.compile(
r'(?P<wan_ip>(.+))\s+')
_REGEX_LANIP = re.compile(
r'(?P<lan_ip>(.+))\s+')
_REGEX_MAC = re.compile(
r'(?P<mac>(.+))\s+')
_REGEX_UPTIME = re.compile(
r'\s' +
r'(?P<router_nowtime>(.+))\sup\s' +
r'(?P<router_uptime>(.+)),\s+load.+:\s' +
r'(?P<cpu_load>(.+))\s+')
_REGEX_CPUTEMP = re.compile(
r'(?P<cpu_temp>(\d+))\s+')
_REGEX_24GTEMP = re.compile(
r'(?P<wl24G_temp>(\d+))')
_REGEX_5GTEMP = re.compile(
r'(?P<wl5G_temp>(\d+))')
_REGEX_24GTXPWR = re.compile(
r'.+:\s+' +
r'(?P<wl24G_txpwr>(\d.+))\s')
_REGEX_5GTXPWR = re.compile(
r'.+:\s+' +
r'(?P<wl5G_txpwr>(\d.+))\s')
_REGEX_MEM = re.compile(
r'Mem:\s' +
r'(?P<mem_used>(\d+K))\sused,\s' +
r'(?P<mem_free>(\d+K))\sfree,\s' +
r'(?P<mem_shrd>(\d+K))\sshrd,\s' +
r'(?P<mem_buff>(\d+K))\sbuff,\s' +
r'(?P<mem_cached>(\d+K))\s.+')
#定义类:_Connection
class _Connection:
def __init__(self):
self._connected = False
@property
def connected(self):
"""Return connection state."""
return self._connected
def connect(self):
"""Mark current connection state as connected."""
self._connected = True
def disconnect(self):
"""Mark current connection state as disconnected."""
self._connected = False
#定义类:SshConnection,继承_Connection类
class SshConnection(_Connection):
"""Maintains an SSH connection to an ASUS-WRT router."""
def __init__(self, host, port, username, password, ssh_key):
"""Initialize the SSH connection properties."""
super().__init__()
self._ssh = None
self._host = host
self._port = port
self._username = username
self._password = password
self._ssh_key = ssh_key
def run_command(self, command):
"""Run commands through an SSH connection.
Connect to the SSH server if not currently connected, otherwise
use the existing connection.
"""
from pexpect import pxssh, exceptions
try:
if not self.connected:
self.connect()
self._ssh.sendline(command)
self._ssh.prompt()
lines = self._ssh.before.split(b'\n')[1:-1]
return [line.decode('utf-8') for line in lines]
except exceptions.EOF as err:
#_LOGGER.error("Connection refused. %s", self._ssh.before)
self.disconnect()
return None
except pxssh.ExceptionPxssh as err:
#_LOGGER.error("Unexpected SSH error: %s", err)
self.disconnect()
return None
except AssertionError as err:
#_LOGGER.error("Connection to router unavailable: %s", err)
self.disconnect()
return None
def connect(self):
"""Connect to the ASUS-WRT SSH server."""
from pexpect import pxssh
self._ssh = pxssh.pxssh()
if self._ssh_key:
self._ssh.login(self._host, self._username, quiet=False,
ssh_key=self._ssh_key, port=self._port)
else:
self._ssh.login(self._host, self._username, quiet=False,
password=self._password, port=self._port)
super().connect()
def disconnect(self): \
# pylint: disable=broad-except
"""Disconnect the current SSH connection."""
try:
self._ssh.logout()
except Exception:
pass
finally:
self._ssh = None
super().disconnect()
#定义函数_parse_lines(),从命令行返回值中按正则公式提取信息,返回为一个列表
def _parse_lines(lines, regex):
"""Parse the lines using the given regular expression.
If a line can't be parsed it is logged and skipped in the output.
"""
results = []
for line in lines:
match = regex.search(line)
if not match:
#_LOGGER.debug("Could not parse row: %s", line)
continue
results.append(match.groupdict())
return results
#定义函数get_data_by(),执行命令行语句cmd_line;
#并将返回值交给函数_parse_lines();
#按照正则公式regex_rule提取信息,作为本函数返回值.
def get_data_by(connection, cmd_line, regex_rule):
lines = connection.run_command(cmd_line)
if not lines:
return {}
result = _parse_lines(lines, regex_rule)
data = {}
for element in result:
data.update(element)
return data
#定义函数get_asuswrt_data,完成打开SSH连接并提取并打印数据的整个过程
def get_asuswrt_data(connection):
asuswrt_data = {}
asuswrt_data.update(get_data_by(connection, _CMD_WANIP, _REGEX_WANIP))
asuswrt_data.update(get_data_by(connection, _CMD_LANIP, _REGEX_LANIP))
asuswrt_data.update(get_data_by(connection, _CMD_MAC, _REGEX_MAC))
asuswrt_data.update(get_data_by(connection, _CMD_UPTIME, _REGEX_UPTIME))
asuswrt_data.update(get_data_by(connection, _CMD_CPUTEMP, _REGEX_CPUTEMP))
asuswrt_data.update(get_data_by(connection, _CMD_24GTEMP, _REGEX_24GTEMP))
asuswrt_data.update(get_data_by(connection, _CMD_5GTEMP, _REGEX_5GTEMP))
asuswrt_data.update(get_data_by(connection, _CMD_24GTXPWR, _REGEX_24GTXPWR))
asuswrt_data.update(get_data_by(connection, _CMD_5GTXPWR, _REGEX_5GTXPWR))
asuswrt_data.update(get_data_by(connection, _CMD_MEM, _REGEX_MEM))
dict = get_data_by(connection, _CMD_NAME, _REGEX_NAME)
json_data = {'state': dict['router_name'], 'attributes': asuswrt_data}
#本来这里可以str(json_data)返回了,怎奈HA只接受255B以内的数据
list_data = [dict['router_name']]
for key in asuswrt_data:
list_data.append(asuswrt_data[key])
str_data = ','.join(list_data)
print(str_data)
if __name__ == '__main__':
asuswrt_connection = SshConnection(_RT_IP, _RT_PT, _RT_USR, _RT_SSHPWD, _RT_SSHKEYPATH)
get_asuswrt_data(asuswrt_connection)
asuswrt_mirukuteii.yaml
建议放在packages文件夹中工作,也可修改添加到configuration.yaml中。
#AsusWrt路由器信息监控
sensor:
- platform: command_line
name: router
#下面的路径按py文件的存放位置进行修改哦
command: "python3 /config/script/asuswrt_mirukuteii.py"
scan_interval: 30
- platform: template
sensors:
router_name:
icon_template: mdi:router-wireless
friendly_name: "路由器名称"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[0] }}
router_wanip:
icon_template: mdi:ethernet
friendly_name: "外网IP地址"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[1] }}
router_lanip:
icon_template: mdi:ethernet
friendly_name: "内网IP地址"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[2] }}
router_mac:
icon_template: mdi:ethernet
friendly_name: "路由器MAC地址"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[3] }}
router_nowtime:
icon_template: mdi:clock
friendly_name: "路由器当前时间"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[4] }}
router_uptime:
icon_template: mdi:av-timer
friendly_name: "路由器运行时间"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[5] }} {{ list[6] }}
router_load_1min:
icon_template: mdi:select-inverse
friendly_name: "CPU平均负载:1分钟"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[7] }}
router_load_5min:
icon_template: mdi:select-inverse
friendly_name: "CPU平均负载:5分钟"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[8] }}
router_load_15min:
icon_template: mdi:select-inverse
friendly_name: "CPU平均负载:15分钟"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[9] }}
router_cputemp:
icon_template: mdi:thermometer
friendly_name: "CPU温度"
unit_of_measurement: '℃'
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[10] }}
router_24gtemp:
icon_template: mdi:thermometer
friendly_name: "2.4G温度"
unit_of_measurement: '℃'
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[11] }}
router_5gtemp:
icon_template: mdi:thermometer
friendly_name: "5G温度"
unit_of_measurement: '℃'
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[12] }}
router_24gtxpwr:
icon_template: mdi:wifi
friendly_name: "2.4G天线发射功率"
unit_of_measurement: 'dBm'
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[13] }}
router_5gtxpwr:
icon_template: mdi:wifi
friendly_name: "5G天线发射功率"
unit_of_measurement: 'dBm'
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{{ list[14] }}
router_mem_used:
icon_template: mdi:memory
friendly_name: "已用内存"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{%- set mem = (list[15] |replace('K','') |int) -%}
{%- if mem >= 1024 -%}
{%- set mem = mem / 1024 -%}
{{ mem | round(1) }} MiB
{%- else -%}
{{ mem }} KiB
{%- endif -%}
router_mem_free:
icon_template: mdi:memory
friendly_name: "可用内存"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{%- set mem = (list[16] |replace('K','') |int) -%}
{%- if mem >= 1024 -%}
{%- set mem = mem / 1024 -%}
{{ mem | round(1) }} MiB
{%- else -%}
{{ mem }} KiB
{%- endif -%}
router_mem_shrd:
icon_template: mdi:memory
friendly_name: "共享内存"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{%- set mem = (list[17] |replace('K','') |int) -%}
{%- if mem >= 1024 -%}
{%- set mem = mem / 1024 -%}
{{ mem | round(1) }} MiB
{%- else -%}
{{ mem }} KiB
{%- endif -%}
router_mem_buff:
icon_template: mdi:memory
friendly_name: "磁盘缓存"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{%- set mem = (list[18] |replace('K','') |int) -%}
{%- if mem >= 1024 -%}
{%- set mem = mem / 1024 -%}
{{ mem | round(1) }} MiB
{%- else -%}
{{ mem }} KiB
{%- endif -%}
router_mem_cached:
icon_template: mdi:memory
friendly_name: "文件缓存"
value_template: >-
{%- set list = states.sensor.router.state.split(',') -%}
{%- set mem = (list[19] |replace('K','') |int) -%}
{%- if mem >=1024 -%}
{%- set mem = mem / 1024 -%}
{{ mem | round(1) }} MiB
{%- else -%}
{{ mem }} KiB
{%- endif -%}
group:
routermon:
name: 'ROUTER监控'
view: no
entities:
- sensor.router_name
- sensor.router_wanip
- sensor.router_lanip
- sensor.router_mac
- sensor.router_nowtime
- sensor.router_uptime
- sensor.router_load_1min
- sensor.router_load_5min
- sensor.router_load_15min
- sensor.router_cputemp
- sensor.router_24gtemp
- sensor.router_5gtemp
- sensor.router_24gtxpwr
- sensor.router_5gtxpwr
- sensor.router_mem_used
- sensor.router_mem_free
- sensor.router_mem_shrd
- sensor.router_mem_buff
- sensor.router_mem_cached
Groups.yaml
在这个文件中某个你想要展示路由器监控情况项目的group的entities项中添加如下代码以实现图片中的状态卡片。
- group.routermon