RaspBoard计划

不知不觉家里的智能设备越来越多,而Home Assistant早期运行在树莓派里,设备多了响应也变得慢了起来。所以最近把Home Assistant从ARM架构迁移到NAS的Docker中后,Siri再也不会「请稍后,正在处理中」了,用户体验极好。🥳

这样树莓派也闲置了下来,在淘宝收了一个LCD显示屏正好可以放在桌上,不如改造一下做一个第三代的DashBoard:RaspBoard。

前几代DashBoard

命令行

第一个DashBoard是用Python写的一个简单的命令行,运行在iPad上,但是用了一段时间后就废弃掉了。

废弃的原因太多了:

  • 文字太小看不清楚
  • iPad长期开机导致了电池老化
  • 用户界面(UX)非常不友好

Hub Center

第二个DashBoard使用了一台被Root的安卓机,通过Home Assistant提供的HADashboard服务,在手机上显示家里设备的状态和一些天气状态及公交状态。

因为可以通过WIFI使用ADB操作,所以可以完美的根据是否在家控制屏幕的开与关,再通过智能插座控制手机的充电状态。目前一直用到了现在。

但是必须依靠Home Assitant和有限的模块限制了其他信息的显示,所以有了RaspBoard的想法。

实现方式

同样的,RaspBoard通过网页显示所有的信息。Raspbian系统内置了一个Chromium,可以用来显示网页,而全屏打开Chromoum也非常的容易,只需要在终端执行:

1
chromium-browser  --disable-popup-blocking --no-first-run --disable-desktop-notifications  --kiosk "http://127.0.0.1"

这样,只需要写一个网页,然后定时请求不同的设备接口,RaspBoard就完工了。🎉

但是如何制作这个网页?

前端

前端还是使用了Vue,每一个模块/卡片为一个组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
components
├── dashboard
│   ├── RdFansBoard.vue
│   ├── RdHassBoard.vue
│   ├── RdHeader.vue
│   ├── RdInfoBoard.vue
│   ├── RdPcBoard.vue
│   └── index.js
├── index.js
└── integrations
├── RdClick.vue
├── RdFans.vue
├── RdLight.vue
├── RdPc.vue
├── RdPi.vue
├── RdSwitch.vue
├── RdTemperature.vue
├── RdTime.vue
├── RdVpn.vue
├── RdWeather.vue
├── RdWeatherAqi.vue
├── index.js
└── mixins
└── hass.js

定时请求

定时请求使用了vue-cronoNPM包,当组件created的时候创建定时器,定时器的生命周期同Vue,当Vue被destory的时候删除定时器。

状态管理

状态管理使用了Vue的官方Vuex,通过store集中管理所有的组件状态。

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

每一个组件的状态大致为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const state = {
status: false,
first: true,
next: 2,
data: {}
};

const mutations = {
SET_HASS(state, value) {
state.data = value
state.first = false
},
SET_NEXT(state, value) {
state.next = value
}
};

const actions = {
setHassData({commit}, data) {
commit('SET_HASS', data)
},
setHassNext({commit}, value) {
commit('SET_NEXT', value)
}
};

export default {
state,
mutations,
actions
}

每一个组件包含四个值:

  • status: 判断当前数据是否有效
  • first: 是否是第一次请求
  • next: 轮询时间,当next为0时再次请求数据接口
  • data: 接口数据

后端

每个组件根据数据的时效性来设置定时器请求数据接口,但是每隔1秒的HTTP请求显然很消耗资源。所以RashBoard没有直接请求数据接口,而是使用了tornado来作为一层代理,前端和后端通过WebSocket连接用来转发所有的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import tornado
import json
import requests

class EchoWebSocket(tornado.websocket.WebSocketHandler):
def check_origin(self, origin):
return True

def open(self):
print("WebSocket opened")

def on_message(self, data):
···

def on_close(self):
print("WebSocket closed")


class Application(tornado.web.Application):
def __init__(self, handlers):
super(Application, self).__init__(handlers)


def main():
options.parse_command_line()
app = Application([('/', EchoWebSocket)])
app.listen(6901, address='0.0.0.0')
tornado.ioloop.IOLoop.current().start()

if __name__ == '__main__':
main()

当Vue组件被创建后,RaspBoard和本地服务一直保持着长连接,通过WS交换数据:

作为服务

为了更好的控制,把代理作为一项systemctl服务:

1
vim /usr/lib/systemd/system/raspbpard-server.service
1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=raspbpard-server
After=network.target

[Service]
TimeoutStartSec=30
ExecStart=/usr/bin/python3 /home/hades/raspboard-server/ws.py
ExecStop=/bin/kill $MAINPID

[Install]
WantedBy=multi-user.target

生命周期

这样,RaspBoard的生命周期大致为:

  • 创建Vue,开启WebSocket连接,定义定时器,保持onMessage监听
  • 判断每个组件的状态和Next值,当组件定时器被清零后提交请求
  • 服务端按照队列请求数据接口,请求成功后发送message到前端
  • 前端判断返回的message类型,根据类型更新相应组件的状态,渲染组件数据

界面设计

目前RaspBoard有四个Dashboard组成,每一个Dashboard由不同的模块/组件组成:

  • 基本:包含天气、时钟和树莓派的运行状态。
  • 家庭:Home Assistant的设备信息。
  • 主机:主机的运行状态。
  • 粉丝:微博、BiliBili的粉丝数、订阅数。

RashBoard Main

为了能够自动显示,增加了一个Switch按钮,这样,激活按钮后每一个Tab将循环显示,实现方式也简单粗暴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
autoSwitchTabs() {
if (this.isSwitch && this.timer) {
clearInterval(this.timer);
} else {
this.timer = setInterval(() => {
if (this.active < 3) {
this.$store.dispatch('setActive', parseInt(this.active) + 1);
} else {
this.$store.dispatch('setActive', 0)
}
}, 5000);
}
this.$store.dispatch('setSwitch', !this.isSwitch);
},

信息获取

UI绘制完成,就该考虑如何获取这些数据了。天气流量数据以及粉丝数都有接口,直接请求就可以得到这些数据:

1
2
3
4
# weibo fans
https://m.weibo.cn/api/container/getIndex?vmid=xxx
# bilibi fans
https://api.bilibili.com/x/relation/stat?containerid=xxx

Raspberry Pi

通过Python的os模块,可以很方便的获取树莓派的硬件信息:

CPU

1
2
3
4
5
def getCPUtemperature():
return os.popen('/opt/vc/bin/vcgencmd measure_temp').read()[5:9]

def getCPUuse():
return(str(os.popen("top -n1 | awk '/Cpu\(s\):/ {print $2}'").readline().strip()))

RAM && Disk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def getDiskSpace():
return os.popen('df -h /|tail -n +2').readline().split()[1:5]
def getRAMinfo():
return os.popen('free|tail -n +2').readline().split()[1:4]

# RAM information
# Output is in kb, here I convert it in Mb for readability
RAM_stats = getRAMinfo()
RAM_total = round(int(RAM_stats[0]) / 1000, 1)
RAM_used = round(int(RAM_stats[1]) / 1000, 1)
RAM_free = round(int(RAM_stats[2]) / 1000, 1)

# Disk information
DISK_stats = getDiskSpace()
DISK_total = DISK_stats[0]
DISK_used = DISK_stats[1]
DISK_perc = DISK_stats[3]

System

1
2
3
4
5
def getUptime():
return os.popen("cat /proc/uptime| awk -F. '{run_days=$1 / 86400;run_hour=($1 % 86400)/3600;run_minute=($1 % 3600)/60;run_second=$1 % 60;printf(\"%dday%d:%d:%d\",run_days,run_hour,run_minute,run_second)}'").read()

def getIp():
return os.popen("ifconfig wlan0 | grep \"inet 192\" | awk '{ print $2}'").read()

PC

平时使用AIDA64来监听主机的设备信息,所以要获取主机的信息主要从AIDA64入手。

python_aida64库

AIDA64可以将设备信息写入到内存或者注册表中,通过读取内存或注册表就可以得到主机的设备信息,Github上找到了一份Python的代码,可以读取内存中的ADID64的数据。

那如何才能让其他设备访问主机中的数据呢?

为了可以在局域网的其他设备中请求数据,同时在主机使用tornado开启了一个WEB服务用来暴露数据,RaspBoard请求这个接口就可以得到主机的状态信息。

开机自启

为了能保证做为一个应用程序在开机之后自动启动,使用pyinstaller将这个WEB程序打包成了一个.exe文件,直接丢到启动项里,这样就方便了很多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# -*- coding:utf-8 -*-
import tornado.web
import json
import os
import platform
import python_aida64

if platform.system() == "Windows":
import asyncio

asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())


class IndexHandler(tornado.web.RequestHandler):
def set_default_headers(self):
self.set_header("Access-Control-Allow-Origin", "*")
self.set_header("Access-Control-Allow-Headers", "x-requested-with")
self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')

def get(self):
data = python_aida64.getData()
self.write(json.dumps(data))


if __name__ == '__main__':
app = tornado.web.Application([
('/', IndexHandler),
])
app.listen(8080, address='0.0.0.0')
tornado.ioloop.IOLoop.current().start()

设备状态

当主机关机是RaspBoard应该暂时取消主机的设备请求,所以通过Home Assistant增加了判断主机是否在线的Sensor:

1
2
3
4
binary_sensor:
- platform: ping
name: hades_pc
host: 192.168.1.100

这样,一个桌面级的DashBoard就完工了,以至于还内心澎湃的拍了一个15s小视频:

配合上Home Assistant的自动化,就可以做到回到家自动打开RashBoard,离开家关闭RashBoard了!

最后

终于玩上了动森,开始了还房贷的生活,真的是太太太好玩了!任天堂真是_____!

🥳 加载Disqus评论