『瀚思彼岸』» 智能家居技术论坛

 找回密码
 立即注册
查看: 21138|回复: 20

[经验分享] 【AsusWrt】华硕梅林固件路由器信息监控之【命令行py脚本】

[复制链接]

26

主题

553

帖子

2726

积分

金牌会员

Rank: 6Rank: 6

积分
2726
金钱
2148
HASS币
100

教程狂人

发表于 2018-4-24 10:28:43 | 显示全部楼层 |阅读模式
本帖最后由 Mirukuteii 于 2018-4-27 02:05 编辑


【AsusWrt】华硕梅林固件路由器信息监控之【命令行py脚本实现法】

【AsusWrt】华硕梅林固件路由器信息监控之【命令行py脚本实现法】

Sensor.Command_line Python脚本 经验分享 萌新探索系列

憋说话,先发图。
360截图16450708758970.png

更新

  • 4月27日:
    微信截图_20180427020412.png
    计划新增用于不可描述之事的命令:
    _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_scannerget_scannerasync_setup_scannersetup_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




评分

参与人数 2金钱 +25 收起 理由
yoyosuka + 5 这个厉害了,路由器温度高自动打开风扇降温.
+ 20 加油!早日把插件写出来!

查看全部评分

回复

使用道具 举报

65

主题

853

帖子

3038

积分

论坛元老

Rank: 8Rank: 8

积分
3038
金钱
2180
HASS币
40
发表于 2018-4-24 10:48:24 | 显示全部楼层
玩玩还是可以的,但讲真还是J大的方法比较好,
主动推送不会被路由器ban掉ha而影响ha与其他设备间的稳定哈。
回复

使用道具 举报

123

主题

4626

帖子

1万

积分

管理员

囧死

Rank: 9Rank: 9Rank: 9

积分
16009
金钱
11298
HASS币
45
发表于 2018-4-24 14:37:57 | 显示全部楼层
加油!早日把插件写出来!一次登录,终身获取。
回复

使用道具 举报

59

主题

731

帖子

4221

积分

论坛元老

Rank: 8Rank: 8

积分
4221
金钱
3485
HASS币
20
发表于 2018-4-24 14:48:13 | 显示全部楼层
nb!仰慕一下
回复

使用道具 举报

220

主题

1284

帖子

7847

积分

超级版主

Rank: 8Rank: 8

积分
7847
金钱
6533
HASS币
86

教程狂人论坛风云人物突出贡献

发表于 2018-4-24 17:32:11 | 显示全部楼层
Jones 发表于 2018-4-24 14:37
加油!早日把插件写出来!一次登录,终身获取。

太好了,不用我写了,我都准备好demo 的代码了
回复

使用道具 举报

123

主题

4626

帖子

1万

积分

管理员

囧死

Rank: 9Rank: 9Rank: 9

积分
16009
金钱
11298
HASS币
45
发表于 2018-4-24 20:11:04 | 显示全部楼层
lidicn 发表于 2018-4-24 17:32
太好了,不用我写了,我都准备好demo 的代码了

你最近消失了好久啊!
回复

使用道具 举报

9

主题

787

帖子

3829

积分

论坛元老

Rank: 8Rank: 8

积分
3829
金钱
3042
HASS币
87
发表于 2018-4-24 23:22:52 | 显示全部楼层
这个太赞,期待早日发出来。
回复

使用道具 举报

26

主题

553

帖子

2726

积分

金牌会员

Rank: 6Rank: 6

积分
2726
金钱
2148
HASS币
100

教程狂人

 楼主| 发表于 2018-4-25 02:20:01 | 显示全部楼层
plutosherry 发表于 2018-4-24 10:48
玩玩还是可以的,但讲真还是J大的方法比较好,
主动推送不会被路由器ban掉ha而影响ha与其他设备间的稳定哈 ...

嗯...从HA的角度看确实如此,不过插件好好写不至于被ban掉,本文的脚本30秒运行1次,都不会被ban了。
回复

使用道具 举报

26

主题

553

帖子

2726

积分

金牌会员

Rank: 6Rank: 6

积分
2726
金钱
2148
HASS币
100

教程狂人

 楼主| 发表于 2018-4-25 02:22:16 | 显示全部楼层
Jones 发表于 2018-4-24 14:37
加油!早日把插件写出来!一次登录,终身获取。

Jones大,这么一份作业交给我这种萌新好嘛
回复

使用道具 举报

26

主题

553

帖子

2726

积分

金牌会员

Rank: 6Rank: 6

积分
2726
金钱
2148
HASS币
100

教程狂人

 楼主| 发表于 2018-4-25 02:23:53 | 显示全部楼层
lidicn 发表于 2018-4-24 17:32
太好了,不用我写了,我都准备好demo 的代码了

纳尼???,受宠若惊,不胜惶恐,话说L大最近忙啥?
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|Hassbian

GMT+8, 2024-4-19 17:05 , Processed in 0.068449 second(s), 36 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表