DIY智能家庭系统
#编程 #python #ubuntu #smarthome

我想分享我在业余时间从事的小项目。这是一个简单的智能家居项目,旨在在我的公寓中记录温度和湿度,同时还可以控制加热系统。为此,我从 Shelly 中利用传感器和恒温阀,这些传感器和恒温阀具有多种优势。这些设备能够通过Wi-Fi进行通信,它们支持MQTT协议,并提供Web REST-API接口。整个系统的核心是在Linux上运行的简单Python应用程序。该应用程序从传感器中收集数据,并将其存储在PostgreSQL数据库中。此外,还有一个在Apache上运行的Web应用程序。通过Web应用程序,我可以控制和配置整个系统。
设计系统时我有两个大问题。
第一个是确定最佳传感器放置,并决定是使用电池还是直接电源为其供电。
和秒:我没有Linux,Python,Postgresql和Apache的经验:D

请注意,这不是用于配置和设置所有必要组件的分步教程。我正在提供设置的基本概述和骨骼结构。

Project diagram

配置Ubuntu

作为第一步,我必须配置家庭服务器。由于其简单性,我选择了Ubuntu桌面作为操作系统。设置操作系统后,我继续逐步安装必要的服务。

首先,我通过执行以下命令安装了PostgreSQL:

$ sudo apt install postgresql postgresql-contrib

要启动PostgreSQL服务,我使用了命令:

$ sudo systemctl start postgresql.service

为了确保服务在系统启动时自动启动,我将其启用了:

$ sudo systemctl enable postgresql.service

此外,我安装了PGADMIN4,以方便使用数据库。安装程序包还包括Apache2 Web服务器。我执行了以下步骤安装pgadmin4:

$ curl https://www.pgadmin.org/static/packages_pgadmin_org.pub | sudo apt-key add
$ sudo sh -c 'echo "deb https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/$(lsb_release -cs) pgadmin4 main" > /etc/apt/sources.list.d/pgadmin4.list && apt update'
$ sudo apt install pgadmin4

创建数据库

现在,我拥有所有必要的组件来创建一个数据库,以从传感器收集数据。为了满足我的要求,我设计了以下表:

  • 房间表:
    该表包含公寓中的房间列表。

  • 设备表:
    该表拥有所有传感器的共同属性,包括名称,WiFi连接状态,IP地址,RSSI(接收信号强度指示器),MAC地址,更新的可用性...

  • RoomDevice表:
    该表通过存储房间ID和设备ID来建立房间和设备之间的关系,从而允许将任何设备分配到任何房间。

前进,我创建了特定于每种设备类型的表:

  • 打开窗口传感器表:
    该表收集诸如温度,lux(光强度),振动,电池状态,动作原因和测量数据等数据...

  • 温度和湿度传感器表:
    该表捕获了诸如温度,湿度,电池状态,动作原因和测量日期之类的数据...

  • 恒温散热器阀表:
    该表收集数据,例如温度,阀门位置,窗口打开状态,电池状态,时间表配置文件和测量日期...

安装蚊子

接下来,我继续配置所有雪莉设备。我更新了它们,在家庭网络上分配了固定的IP地址,并将它们配置为连接到MQTT服务器。
哦,我几乎忘记了,我还需要安装MQTT服务器。为此,我选择了蚊子经纪人。

安装蚊子:

$ sudo apt update -y && sudo apt install mosquitto mosquitto-clients -y

启动经纪人:

$ sudo systemctl start mosquitto

使经纪人能够从系统启动开始:

$ sudo systemctl enable mosquitto

此外,我需要在防火墙中打开所需的端口:

$ sudo ufw allow 8883

Python

现在是时候让我从未冒险过的编程语言Python了。
我需要一些基本元素:与PostgreSQL互动的适配器,用于通信的MQTT客户端,记录机构和JSON解析库。

我导入以下库:

import psycopg2
import paho.mqtt.client as mqtt
import logging
import json

此外,我为配置目的定义了一组常数:

# mqtt constant
CLIENT_NAME = 'Subscrible_test'
USER_NAME = 'username'
PASSWD = 'password'
HOST = 'IP'
PORT = 1883
# postgresql constant
SQL_HOST = 'localhost'
SQL_DBNAME = 'smarthome_db'
SQL_USER = 'username'
SQL_PASSWD = 'password'
SQL_PORT = 5432

现在,我将继续设置与数据库建立连接的记录器和定义过程。一个过程处理数据库连接的创建,而另一个过程检查数据库是否可访问。我每次传感器向我发送数据时都会运行该过程以验证访问。

要创建记录器,我实现了以下代码:

#create a logger
logger = logging.getLogger('mqtt_archiver')
logging.basicConfig(level=logging.DEBUG, format='%(message)s')
handler = logging.FileHandler(/mqtt_archiver.log')
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

# Write PID to log file
pid = os.getpid()
logger.info('pid: ' + str(pid))

上面的代码设置了一个记录器并将其配置。记录器还将进程ID(PID)写入日志文件。

接下来,我定义了两个函数: sql_connect() check_sql_connection(),分别处理数据库连接和连接检查:

def sql_connect():
    global con
    try:
        con = psycopg2.connect(user=SQL_USER, password=SQL_PASSWD, host=SQL_HOST, port=SQL_PORT, database=SQL_DBNAME)
        logger.info('SQL: connect to' + SQL_DBNAME)
    except (Exception, psycopg2.DatabaseError) as error:
        logger.error('SQL: connect error to ' + SQL_DBNAME + '. ' + str(error))
        if con:
            con.rollback()

def check_sql_connection():
    try:
        if con == None:
            sql_connect()
        else:
            cur = con.cursor()     
            cur.execute("SELECT 1")
    except (Exception, psycopg2.DatabaseError) as error:
        print(datetime.datetime.now(), ' SQL: check connection error: ', error)
        logger.error('SQL: check connection error: ' + str(error))
        sql_connect()

# Connect to PostgreSQL
sql_connect()

sql_connect()函数建立了与PostgreSQL数据库的连接。它记录了成功的连接或任何遇到的错误。
check_sql_connection()函数检查数据库连接是否可用。如果不存在连接,它将调用 sql_connect()函数以建立新连接。否则,它执行一个简单的SQL语句来验证连接。
最后,该代码调用 sql_connect()函数以连接到postgresql。

接下来,我为MQTT客户端定义了几个功能:

# Called when the client connects to the server.
def on_connect(client, userdata, flags, rc):
    logger.info( 'MQTT: connected with result code ' + str(rc) + '; client_id: ' + str(client._client_id))

# Called when the client disconnects from the server.
def on_disconnect(client, userdata, rc):
    if rc != 0:
        logger.warning( 'MQTT: unexpected disconnection. ' + str(rc) + '; client_id: ' + str(client._client_id))

# Called when a message has been received on the subscribed topic.
def on_message(client, userdata, message):
    check_sql_connection()
    try:
        topic_array = message.topic.split('/')
        if len(topic_array) > 2 and topic_array[2] == 'info':
            update_tbl_device(message)
            if get_device(message.topic).startswith('shellyht-'):
                insert_tbl_shelly_ht(message)
            elif get_device(message.topic).startswith('shellytrv-'):
                insert_tbl_shelly_vrt(message)
            elif get_device(message.topic).startswith('shellydw2-'):
                insert_tbl_shelly_dw2(message)
        elif get_device(message.topic).startswith('shellyplug-s-'):
            insert_tbl_shelly_plugs(message)
    except (Exception) as error:
        logger.error('on_message: ' + str(error))

on_message()函数中,我首先检查SQL连接,然后确定接收到的消息所属的Shelly设备。基于设备类型,我处理消息。函数 update_tbl_device(message)负责解析消息并填充设备表中的基本信息。同样,函数 insert_tbl_shelly_ht(message) insert_tbl_shelly_vrt(message) insert_tbl_shelly_dw2(message)雪莉设备。不同之处在于 insert_tbl_shelly_ht() insert_tbl_shelly_dw2()触发一个将HTTP获取请求发送到恒温器散热器阀的外部功能。一个发送当前的室温(对于自动阀调节很有用),而另一个则发送状态,指示窗口是否打开(阀门打开时自动关闭)。

外部功能看起来像这样:

# Send open window state
device_id = sys.argv[1]
sensor_state = sys.argv[2]
params = {'state': sensor_state}

if device_id == 'shellydw2-XXXXXX':     #shellydw-room1
    response = requests.get('http://192.168.88.101/window/', params)
if device_id == 'shellydw2-YYYYYY':     #shellydw-room2
    response = requests.get('http://192.168.88.102/window/', params)
# Send actual temperature
device_id = sys.argv[1]
tmp_val = sys.argv[2]
hum_val = sys.argv[3]
params = {'temp': tmp_val}

if device_id == 'shellyht-AAAAAA':      #shellyht-room1
    response = requests.get('http://192.168.88.101/ext_t/', params)
if device_id == 'shellyht-BBBBBB':      #shellyht-room2
    response = requests.get('http://192.168.88.102/ext_t/', params)

现在我需要设置MQTT经纪人:

try:
    client = mqtt.Client(CLIENT_NAME, clean_session=False)   
    client.username_pw_set(USER_NAME, PASSWD)
    client.on_connect = on_connect
    client.on_disconnect = on_disconnect
    client.on_message=on_message
    client.connect(HOST, PORT)
    client.subscribe("shellies/#")
    client.loop_forever()
    con.close()
except (Exception) as error:
    logger.error('MAIN LOOP: ' + str(error))

我创建一个新客户端,并为其提供名称和密码。此外,我为处理连接,断开和接收的消息分配了必要的功能: on_connect() on_disconnect(),and on_message() 。我建立了与MQTT经纪人的连接,并订阅了一组特定的主题。就我而言,主题由“ shellies/#” 表示。最后,我启动执行无限循环以处理传入消息。
最后,确保当服务器启动时自动启动此应用程序。为了实现这一目标,我利用Cron是Ubuntu的任务自动化工具。

网页

接下来,我为我的智能家庭项目创建了一个简单的网页。索引页面提供了所有房间的概述,并在每个房间内的设备及其最重要的值显示。单击房间可打开详细的视图,其中所有值均列出。另外,可以修改某些设备的设置,例如设置温度或更改散热器阀的套筒轮廓。

客厅的视图:
Livingroom

客厅细节的视图:
Livingroom Detail

这是创建自己的DIY智能家庭系统的方法。只需几个传感器即可读取数据并处理并存储它。可以根据存储的数据来控制其他活性元素,例如恒温散热器阀。
将来,我计划通过聚光灯和LED条扩展系统,可以通过场景控制器设备或网页控制。