以Debian系統資訊為例實作Home Assistant MQTT sensor

Home Assistant MQTT sensor

我將Home Assistant安裝VMware建構的虛擬機器上,而VMware則是安裝在一台運行Debian 12作業系統的伺服器上,為了方便監看Debain的系統資訊,我把它們整合進Home Assistant的sensor中。

過去是透過Home Assistant的automation與shell_command,經由ssh連線去獲取需要的系統資訊存成txt或json檔,以每三分鐘抓一次lm-sensors的內容為例,automation與shell_command分別為

automation:
  - alias: Information:Get_Debian_Temperature
    trigger:
      - platform: time_pattern
        minutes: "/3"
    action:
      - service: shell_command.get_debian_temperature
    mode: queued

shell_command:
  get_debian_temperature: "ssh -i /config/.ssh/id_rsa -o StrictHostKeyChecking=no -q user@192.168.xx.yy sensors > /config/data/DebianTemp.txt"

(缺點1:要弄ssh key)

再經由command_line sensor去處理這些檔案擷取所需要的資訊,以Core0的溫度為例,command_line sensor為

command_line:
    - sensor:
        name: DEBIAN_Core0_Temperature
        command: "cat /config/data/DebianTemp.txt | grep 'Core 0' | cut -c17-20"
        unit_of_measurement: "°C"

(缺點2:cut -c17-20實在不是一個很好的做法)

除非透過automation監聽DebianTemp.txt的最後修改時間來觸發command_line sensor的更新,automation如下

automation:
  - alias: Information:Update_Debian_Temp
    trigger:
      - platform: state
        entity_id: sensor.debiantemp_txt_last_updated
    action:
      - service: homeassistant.update_entity
        data: {}
        target:
          entity_id:
            - sensor.debian_core0_temperature
    mode: queued

不然就只能被動地靠command_line sensor的scan_interval來更新sensor的值,但這樣子其實相當的被動,且「抓到資訊」與「更新資訊」的時間會有落差。除此之外還要維護一連串的實體,平常沒事就沒事,如果過了幾個月要修改或是新增,可能已經忘了當初是怎麼寫的,改起來就會落東落西的。

(缺點3:時間差和維護問題)

後來看到MQTT sensor,MQTT是Message Queuing Telemetry Transport的縮寫,是一種基於「發佈」和「訂閱」的通訊協議。它專為物聯網(IoT)應用而設計,具有簡單、開放、輕量級和易於實現的特點。

  • 溝通模式:MQTT需要一個MQTT伺服器,連到這個伺服器的客戶端可以區分為「發佈者」和「訂閱者」。發佈者將消息發送到特定的主題(topic),而訂閱者則通過訂閱這些主題來接收訊息(message)。
  • 輕量級和低頻寬消耗:MQTT協議非常輕量,具有低頻寬消耗的特點。這使得它非常適合在資源有限的設備上運行,如嵌入式系統和物聯網設備。
  • 可靠的消息傳遞:MQTT支援三種消息傳送級別,從最多一次傳遞到僅一次傳遞。這使得它能夠適應不同的應用環境,並確保消息的可靠傳遞。
  • 支援SSL/TLS加密:MQTT可以通過SSL/TLS協議進行加密通訊,保護資料的安全性,這使得它更適用於需要安全通訊的環境。

回到原來的題目,Home Assistant要怎麼透過MQTT sensor來擷取lm-sensors裡的資訊當sensor的值?

先要架設一個MQTT伺服器,最簡單方便的就是Home Assistant附加元件中的Mosquitto broker。

Mosquitto broker

再來我是透過python將lm-sensors的資訊擷取出來並透過MQTT發佈到MQTT伺服器上,paho-mqtt是python的MQTT套件,先把它裝起來

$ pip install paho-mqtt

lm-sensors的內容如下,我要抓出Core 0到Core 5的值

coretemp-isa-0000
Adapter: ISA adapter
Package id 0:  +28.0°C  (high = +80.0°C, crit = +100.0°C)
Core 0:        +28.0°C  (high = +80.0°C, crit = +100.0°C)
Core 1:        +28.0°C  (high = +80.0°C, crit = +100.0°C)
Core 2:        +25.0°C  (high = +80.0°C, crit = +100.0°C)
Core 3:        +26.0°C  (high = +80.0°C, crit = +100.0°C)
Core 4:        +27.0°C  (high = +80.0°C, crit = +100.0°C)
Core 5:        +29.0°C  (high = +80.0°C, crit = +100.0°C)

acpitz-acpi-0
Adapter: ACPI interface
temp1:        +27.8°C  (crit = +105.0°C)

iwlwifi_1-virtual-0
Adapter: Virtual device
temp1:        +28.0°C

nvme-pci-0100
Adapter: PCI adapter
Composite:    +50.9°C  (low  =  -0.1°C, high = +79.8°C)
                       (crit = +81.8°C)
Sensor 1:     +50.9°C  (low  = -273.1°C, high = +65261.8°C)

撰寫一個名為sensors.py的python script,透過subprocess套件下指令,再找出所有符合Core*: +*.*°C的字串並記錄在core_temps列表裡,最後轉成json再透過MQTT以debian/core_temperature這個「主題」發佈到伺服器上。

import json
import re
import subprocess
import paho.mqtt.client as mqtt
import logging

logging.basicConfig(filename='error.log', level=logging.ERROR)

broker_address = "mqtt_server_ip"
broker_port = 1883
username = "mqtt_user"
password = "mqtt_password"

def connect_mqtt():
    client = mqtt.Client()
    client.username_pw_set(username, password)
    client.connect(broker_address, broker_port)
    return client

def publish_message(client, topic, data):
    client.publish(topic, json.dumps(data), retain=True)

def sensor():
    try:
        core_temps = {}
        sensors_output = subprocess.check_output(["sensors"]).decode("utf-8")
        matches = re.findall(r'Core (\d+):\s+\+(\d+\.\d+)°C', sensors_output)

        for match in matches:
            core_temps[f"Core{match[0]}"] = float(match[1])

        client = connect_mqtt()
        publish_message(client, "debian/core_temperature", core_temps)
        client.disconnect()

    except Exception as e:
        logging.error(f"Error in sensor: {e}")

if __name__ == "__main__":
    sensor()

core_temps的結果如下

{"Core0": 28.0, "Core1": 29.0, "Core2": 25.0, "Core3": 27.0, "Core4": 27.0, "Core5": 28.0}

這時可以透過Home Assistant附加元件裡的MQTT Explorer觀看是否有成功發佈。

MQTT Explorer

如果成功的話可以找到一個叫做debian的「主題」,下拉可以看到子主題為core_temperature的內容,這樣就是有成功發佈了。

MQTT Explorer

再來建立MQTT sensor,以Core0為例,state_topic指定監聽debian/core_temperature就可以得到上面這一串json,再透過value_template取出key為Core0對應的值

mqtt:
  sensor:
    - name: DEBIAN_Core0_Temperature
      state_topic: "debian/core_temperature"
      value_template: "{{ value_json.Core0 }}"
      unit_of_measurement: "°C"
      force_update: true

最後在Debian上建立一個cronjob定時去執行sensors.py,如果要每三分鐘執行一次,寫法如下

*/3 * * * * /usr/bin/python3 /home/sensors.py

這樣就可以得到一個連續且準時更新的sensor。

MQTT sensor要做attribute也滿容易的,比如將docker stats裡,frigate的資訊整理成json如下

{"BlockIO": "336MB / 19.5GB", "CPUPerc": "378.77%", "Container": "439617391d65", "ID": "439617391d65", "MemPerc": "45.96%", "MemUsage": "2.757GiB / 6GiB", "Name": "frigate", "NetIO": "377GB / 12.1GB", "PIDs": "1318"}

我只想取出CPUPerc當sensor,其他key做為這個sensor的attribute就好,sensor和attribute的差別在於sensor會將狀態記錄到資料庫中,而attribute則只會顯示當下的值。

透過json_attributes_topic監聽docker_stats/frigate這個「主題」,再利用json_attributes_template: “{{ value_json | tojson }}”將所有key存為attribute,寫法如下

mqtt:
  sensor:
    - name: Frigate_DockerCPU
        state_topic: "docker_stats/frigate"
        value_template: "{{ value_json.CPUPerc | replace('%', '') }}"
        unit_of_measurement: "%"
        json_attributes_topic: "docker_stats/frigate"
        json_attributes_template: "{{ value_json | tojson }}"
        force_update: true

最後要特別注意的是,當一個「主題」及其訊息被發佈到MQTT伺服器上之後,MQTT伺服器並不會保留下記錄,因為從MQTT機制面上來看,訊息發佈後就會有對應該「主題」的訂閱者做接收。當Home Assistant重新啟動之後,所有實體包含MQTT sensor都會被更新,但更新MQTT sensor時MQTT伺服器上卻沒有相關記錄,所以MQTT sensor會變成unavailable。要解決這個問題,可以在發佈MQTT時加上retain的旗標,這樣在MQTT伺服器上就保留這個「主題」的最新一條訊息。

client.publish(topic, message, retain=True)

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

返回頂端