自动监控宿舍的水费和电费

将近两个月前搬到了独立卫浴的新宿舍,除了电费之外也要开始交水费了。

  • 水费是只对热水收钱,每立方米热水 32 元人民币。考虑到热水实际上非常热,水龙头往出放的实际上都是很少的热水与大量的冷水混合的温水,所以真正用的热水是要便宜个几倍的。饶是如此,每天也要花掉 3 块 2 的水费。这数字正巧是 32 元的十分之一,所以宿舍每天消耗一百升热水,想想还是挺恐怖的。作为参考,北京的自来水费是三个阶梯,每立方米分别是 5、7、9 元人民币。不知道热水里有没有摊入一定比例的本应支付的冷水水费,如果不考虑的话,那么看来烧热水是挺花钱的。
  • 电费是每度电 0.5103 元,这个数有零有整的,大体上就是一块钱能充两度电差一点点。这个数字就正好是北京电费里“执行居民价格的非居民用户”这一项计费模式的费用,好像和学生公寓挺搭的;学校也不在这上征收税赋,好像还有点良心。宿舍每天大概会消耗两三四度电,相比水费而言,电费的数额显得不那么稳定,不过也大差不差。宿舍的冰箱的节能标识写明了每天耗费半度电,所以整个宿舍大概可以抽象成六台电冰箱。

水电费一旦用完就会停热水停电,即使立即充值,到生效也有一个延迟,更何况每天 23:00 ~ 24:30 还不能充电费,这要是大夏天的晚上空调突然没了,硬挨一个半小时可以说是人间地狱。某天我突然心血来潮查了查电费发现还剩两度半,那时的恐慌属于是记忆犹新。

结论是必须得有一个机制,在水电费快用完的时候提醒充值。

获取水电费

新宿舍的水电表读数都是联网的,目前观察下来大概是每天中午的十二点左右会抄表一次。所以只要每天上网查一下就好了。

每天查一遍读数然后记下来,这太智障了,我们需要自动化。

我不是很清楚宿舍剩余水电费这种信息是通过什么接口拿到的,说到底根本就不应该期待你清上个年代风格的“家园网主页”能提供什么能用且好用的接口。模拟浏览器操作绝对是最省心省力的方式。为此,我们需要 Selenium:

1
pip3 install selenium

Windows 下面似乎没办法直接这样搞,所以搬到了 WSL 上面。只装 Selenium 是不够的,我们还得装一个 Chrome 来当做浏览器引擎:

1
2
3
4
5
6
7
8
9
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.dist

wget https://dl.google.com/linux/linux_signing_key.pub
sudo apt-key add linux_signing_key.pub

sudo apt update
sudo apt install google-chrome-stable

google-chrome --version # 104.0.5112.101

然后去安装对应版本的 chromedriver。我没有找到完全对应的版本,找了个最接近的,同样能用:

1
2
3
4
wget -c https://registry.npmmirror.com/-/binary/chromedriver/104.0.5112.79/chromedriver_linux64.zip
unzip chromedriver_linux64.zip
chmod +x chromedriver
sudo mv chromedriver /usr/bin/

好了,现在可以写段 Python 来验证 Selenium 能否正常工作了!

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/python3

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By

chrome_options = Options()
chrome_options.add_argument('--disable-gpu') # 防止兼容性 bug
chrome_options.add_argument('--headless') # WSL 不需要可视化界面

driver = webdriver.Chrome(options=chrome_options)

不报任何错,那就应该没有问题了。

现在可以对着 Selenium 模拟键盘输入和鼠标点击,从而实现自动登录了。通过浏览器的检查元素,很容易就能找到用户名、密码、登录按键,所对应的那些 DOM 元素,用 Selenium 的接口获取、模拟键盘键入和鼠标点击就可以完成登录。之后再导航到水电费查询页面,如法炮制。以电费为例:

1
2
3
4
5
6
# 导航到对应页面
driver.get('http://myhome.tsinghua.edu.cn/Netweb_List/Netweb_Home_electricity_Detail.aspx')
driver.implicitly_wait(0.5)
electricity_kwh = float(driver.find_element(By.ID, "Netweb_Home_electricity_DetailCtrl1_lblele").text)
electricity_time = driver.find_element(By.ID, "Netweb_Home_electricity_DetailCtrl1_lbltime").text
electricity_time = datetime.datetime.strptime(electricity_time, '%Y/%m/%d %H:%M:%S').strftime('%Y-%m-%dT%H:%M:%S.000+08:00')

这里我不仅获取水表、电表读数,还获取了一个抄表时间,用来对记录进行去重:如果这次读到的数据中,抄表时间和上次读到的相同,那么这两次读到的就是相同的数据,这次的结果也就无需记录了。因为多抄一次表、少抄一次表完全是无所谓的,所以我直接在本地文件中记录水电费的抄表时间;每次先读取文件中记录的时间,和这次获得的时间比对后,再把这次的时间重新写入文件中。这里还对时间进行了一次解析和重新格式化,这是为了方便之后上传数据。

上传到 Notion

我希望不仅能获取水电费数据,还要能方便地查询水电费数据。一个我觉得比较好的选择是 Notion,主要是我平时也很习惯用。

Notion 中有个东西叫 Database(数据库),它相当于一个精简版的 Excel 工作簿,用来记水电费是足够了。为了用上 Database,先要做几个工作:

  • 搞个页面,创建好 Database,视觉上做的美观一点。我设计的方案是两个 Database,虽然一个里面也能放下两张表,但那样相对不太直观。创建好之后记下 Database 的 ID。
  • 在 Database 里面创建好列,设置好排序依据、格式等等。这里面排序依据显然就是日期时间,升序排序,水费那边把格式设置成 CNY。
  • 这里创建一个 Integration,记下 secret,然后去创建好的 Database 那里共享给这个 Integration。

现在就可以构建 HTTP 请求去上传数据了:

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
32
33
34
35
url = 'https://api.notion.com/v1/pages'
headers = {
'Authorization': 'Bearer secret_0123456789abcdefghijklmnopqrstuvwxyzABCDEFG',
'Notion-Version': '2022-06-28',
}
data = {
'parent': {
'type': 'database_id',
'database_id': database_id,
},
'properties': {
'Date & Time': {
'type': 'date',
'date': {
'start': electricty_time,
'end': None,
'time_zone': None
}
},
'Expense Type': {
'type': 'title',
'title': [{
'text': {
'content': 'Electricity (kWh)',
}
}]
},
'Readings': {
'type': 'number',
'number': electricity_kwh,
},
}
}

req = requests.post(url, headers=headers, json=data)

如果返回值是 200 就没问题了,Notion 里也能马上看到更新的结果。如果返回值是 404,多半说明共享权限没设置好。

自动化

组装一下,现在我就有了一套脚本,可以查询水电费读数,然后在和上次相比不重复的情况下把数据上传到 Notion 供我查阅。但是每天手动跑这个脚本是十分愚蠢的行为,我们希望它自动化。

自动化的手段有很多,最好当然是找台服务器然后写个 crontab 就结束了。不过因为 myhome 有访问 IP 限制,这个服务器必须在校内。说实话,感觉能有校内服务器的人不会太在意水费不水费的(属于实验室资产的服务器要除外),所以要依赖自己的 Windows 台式机,用任务计划来启动定时器。

任务计划并不难搞,设成每天执行一次就好了。如果那个时候没有开机,下次开机的时候 Windows 会帮你补上这一次,所以这一点上不用担心。这里面临三个主要问题:

  • 校园网需要登录,计划任务执行的时候不一定有网。尽管可以勾选“在网络连接可用的时候才启动”,但这时候任务计划的行为就不是推迟到网络好的时候执行,而是干脆就不执行这一次了。我们希望最好还是每天抄表一次,所以需要想办法绕过这个障碍。幸运的是,Selenium 如果发现找不到对应的 HTML element 会抛出 NoSuchElementException,而校园网登录前重定向到的登录页面上自然是没有我们想要的页面元素。我们可以在登录逻辑上套一个大 try 块,如果失败就重试,直到重试了一定次数为止。
  • 启动 WSL 脚本的时候需要用到 bash.exe,这玩意有个黑黑的窗口很不美观。我的解决方案是写个 vbs 脚本封装起来。
  • 任务计划执行时报错 0x800710E0,我被这个困扰了两天。最后多管齐下:勾选了“以最高权限执行”、设置了安全策略、更新了组策略,不知道哪个真正解决了问题。

最终效果图:

缴费提醒

最后还有一个很重要的事情:当宿舍快要没电费或者没水费的情况下,我应该要收到一个提醒。这个提醒最好可以是发到我手机上,因为我们现在都习惯于用手机而不是电脑来接收通知。何况电脑这边我已经把执行窗口隐藏掉了,所以如果硬要搞的话就得去搞通知中心那一套了。WSL 调用通知中心,想想就头疼。

手机通知首先想到了微信公众号推送消息以及短信两种方式,简单查了一下发现门槛都不低。遂另寻门路,结果是一个叫 PushDeer 的应用。目测还不错,达到了能用且比较好用的范畴,这里就不花大篇幅介绍了。

如此一来,当电费、水费低于某个额度的时候,我的手机就会收到推送通知,我也就能及时充钱了。

一些题外话

  • 这可能是 water 这个 tag 第一次用作名词。
  • 经过几天的监控,发现水费是扣费是量子化的,每次只会扣 3.2 元的整数倍。