Home Assistant MQTT发现服务的实现
MQTT收发基于mosquito
以下内容摘自官方文档,可查看文档原文
MQTT Discovery
The discovery of MQTT devices will enable one to use MQTT devices with only minimal configuration effort on the side of Home Assistant. The configuration is done on the device itself and the topic used by the device. Similar to the HTTP binary sensor and the HTTP sensor. To prevent multiple identical entries if a device reconnects, a unique identifier is necessary. Two parts are required on the device side: The configuration topic which contains the necessary device type and unique identifier, and the remaining device configuration without the device type.
MQTT discovery is enabled by default, but can be disabled. The prefix for the discovery topic (default homeassistant) can be changed. See the MQTT Options sections
Discovery messages
Discovery topic
The discovery topic needs to follow a specific format:
<discovery_prefix>/<component>/[<node_id>/]<object_id>/config
- component : One of the supported MQTT components, eg. binary_sensor.
- <node_id> (Optional): ID of the node providing the topic, this is not used by Home Assistant but may be used to structure the MQTT topic. The ID of the node must only consist of characters from the character class [a-zA-Z0-9_-] (alphanumerics, underscore and hyphen).
- <object_id>: The ID of the device. This is only to allow for separate topics for each device and is not used for the entity_id. The ID of the device must only consist of characters from the character class [a-zA-Z0-9_-] (alphanumerics, underscore and hyphen).
The <node_id> level can be used by clients to only subscribe to their own (command) topics by using one wildcard topic like <discovery_prefix>/+/<node_id>/+/set.
Best practice for entities with a unique_id is to set <object_id> to unique_id and omit the <node_id>.
Discovery payload
The payload must be a serialized JSON dictionary and will be checked like an entry in your configuration.yaml file if a new device is added, with the exception that unknown configuration keys are allowed but ignored. This means that missing variables will be filled with the component’s default values. All configuration variables which are required must be present in the payload. The reason for allowing unknown documentation keys is allow some backwards compatibility, software generating MQTT discovery messages can then be used with older Home Assistant versions which will simply ignore new features.
Subsequent messages on a topic where a valid payload has been received will be handled as a configuration update, and a configuration update with an empty payload will cause a previously discovered device to be deleted.
A base topic ~ may be defined in the payload to conserve memory when the same topic base is used multiple times. In the value of configuration variables ending with _topic, ~ will be replaced with the base topic, if the ~ occurs at the beginning or end of the value.
Configuration variable names in the discovery payload may be abbreviated to conserve memory when sending a discovery message from memory constrained devices.
How to?
-
向特定topic发送按格式要求的payload以创建设备(device)或者实体(entry)
-
向注册的状态topic上报信息
实现过程源码
/**
* @file main.c
* @author IotaHydrae ([email protected])
* @brief
* @version 0.1
* @date 2023-02-07
*
* MIT License
*
* Copyright 2022 IotaHydrae([email protected])
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
*
*/
#include <mosquitto.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <cjson/cJSON.h>
#define DISCOVERY_PERFIX "homeassistant"
#define HA_ID "000102adssdds"
#define HA_NAME "Loona"
#define HA_SW_VER "05881cd52242187d49a38e3fd7d675a41d5f564f"
#define HA_MODEL "Revision a"
#define HA_MANUFACTURER "KEYi TECH"
#define NODE_CPU_TEMPERATURE "/sys/class/hwmon/hwmon0/temp1_input"
#define NODE_BATTERY_PERCENT "/sys/bus/i2c/devices/0-0062/cw2015_cur_percent"
int send_sensor_temperature_discovery(struct mosquitto *mosq)
{
int rc;
char *string = NULL;
cJSON *device = cJSON_CreateObject();
cJSON *payload = cJSON_CreateObject();
cJSON_AddStringToObject(payload, "~", "Loona/sensor/temperature_cpu");
cJSON_AddStringToObject(payload, "name", "Temperature CPU");
cJSON_AddStringToObject(payload, "state_topic", "~/state");
cJSON_AddStringToObject(payload, "unique_id", "0a7476cc-d6c1-40ba-8ae1-606518c3497f");
cJSON_AddStringToObject(payload, "unit_of_measurement", "°C");
// if (cJSON_AddStringToObject(payload, "device", device) == NULL)
// {
// goto end;
// }
cJSON_AddItemToObject(payload, "device", device);
cJSON_AddStringToObject(device, "identifiers", HA_ID);
cJSON_AddStringToObject(device, "name", HA_NAME);
cJSON_AddStringToObject(device, "sw_version", HA_SW_VER);
cJSON_AddStringToObject(device, "model", HA_MODEL);
cJSON_AddStringToObject(device, "manufacturer", HA_MANUFACTURER);
// cJSON_AddItemToObject(payload, "device", cJSON_CreateObject());
cJSON_AddStringToObject(payload, "force_update", "False");
string = cJSON_PrintUnformatted(payload);
if (string == NULL) {
fprintf(stderr, "Failed to print monitor.\n");
}
printf("%s\n", string);
rc = mosquitto_publish(mosq, NULL,
DISCOVERY_PERFIX "/sensor/Loona/temperature_cpu/config",
strlen(string), string, 2, false);
if (rc != MOSQ_ERR_SUCCESS) {
fprintf(stderr, "Error publishing: %s\n", mosquitto_strerror(rc));
}
return 0;
}
int get_temperature(void)
{
sleep(1); /* Prevent a storm of messages - this pretend sensor works at 1Hz */
int fd_temp_cpu = open(NODE_CPU_TEMPERATURE, O_RDONLY);
char temp[10];
read(fd_temp_cpu, temp, sizeof(temp) / sizeof(temp[0]));
// printf("%s\n", temperatue);
close(fd_temp_cpu);
return atoi(temp);
}
void *publish_sensor_data(void *data)
{
char payload[20];
int temp;
int rc;
struct mosquitto *mosq = (struct mosquitto *)data;
while (true) {
/* Get our pretend data */
temp = get_temperature();
printf("temperature : %d.%d\n", temp / 1000, temp % 1000);
/* Print it to a string for easy human reading - payload format is highly
* application dependent. */
snprintf(payload, sizeof(payload), "%d.%d", temp / 1000, temp % 1000);
/* Publish the message
* mosq - our client instance
* *mid = NULL - we don't want to know what the message id for this message is
* topic = "example/temperature" - the topic on which this message will be published
* payloadlen = strlen(payload) - the length of our payload in bytes
* payload - the actual payload
* qos = 2 - publish with QoS 2 for this example
* retain = false - do not use the retained message feature for this message
*/
// rc = mosquitto_publish(mosq, NULL, "example/temperature", strlen(payload), payload, 2, false);
// rc = mosquitto_publish(mosq, NULL, "home/kitchen/light", strlen(payload), payload, 2, false);
rc = mosquitto_publish(mosq, NULL, "Loona/sensor/temperature_cpu/state",
strlen(payload), payload, 2, false);
if (rc != MOSQ_ERR_SUCCESS) {
fprintf(stderr, "Error publishing: %s\n", mosquitto_strerror(rc));
}
}
}
int send_sensor_battery_percent_discovery(struct mosquitto *mosq)
{
int rc;
char *string = NULL;
cJSON *device = cJSON_CreateObject();
cJSON *payload = cJSON_CreateObject();
/*
{
'~': 'XeniaClock/sensor/inside/temperature_roof',
'name': 'Temperature Roof',
'state_topic': '~/state',
'unique_id': '0a7476cc-d6c1-40ba-8ae1-606518c3497f',
'unit_of_measurement': '°C',
'device': {'identifiers': '000102aabbcc', 'name': 'XeniaClock', 'sw_version': '05881cd52242187d49a38e3fd7d675a41d5f564f', 'model': 'Revision A', 'manufacturer': 'embeddedboys'},
'force_update': False
}
*/
/* build payload */
cJSON_AddStringToObject(payload, "~", "Loona/sensor/battery_percent");
cJSON_AddStringToObject(payload, "name", "Battery Percent");
cJSON_AddStringToObject(payload, "state_topic", "~/state");
cJSON_AddStringToObject(payload, "unique_id", "0a7476cc-d6c1-40ba-8ae1-606518c3497d");
cJSON_AddStringToObject(payload, "unit_of_measurement", "%");
// if (cJSON_AddStringToObject(payload, "device", device) == NULL)
// {
// goto end;
// }
cJSON_AddItemToObject(payload, "device", device);
cJSON_AddStringToObject(device, "identifiers", HA_ID);
cJSON_AddStringToObject(device, "name", HA_NAME);
cJSON_AddStringToObject(device, "sw_version", HA_SW_VER);
cJSON_AddStringToObject(device, "model", HA_MODEL);
cJSON_AddStringToObject(device, "manufacturer", HA_MANUFACTURER);
// cJSON_AddItemToObject(payload, "device", cJSON_CreateObject());
cJSON_AddStringToObject(payload, "force_update", "False");
string = cJSON_PrintUnformatted(payload);
if (string == NULL) {
fprintf(stderr, "Failed to print monitor.\n");
}
printf("%s\n", string);
rc = mosquitto_publish(mosq, NULL,
DISCOVERY_PERFIX "/sensor/Loona/battery_percent/config",
strlen(string), string, 2, false);
if (rc != MOSQ_ERR_SUCCESS) {
fprintf(stderr, "Error publishing: %s\n", mosquitto_strerror(rc));
}
return 0;
}
int get_battery_percent()
{
sleep(1); /* Prevent a storm of messages - this pretend sensor works at 1Hz */
int fd_bat_percent = open(NODE_BATTERY_PERCENT, O_RDONLY);
char percent[5];
read(fd_bat_percent, percent, sizeof(percent) / sizeof(percent[0]));
// printf("%s\n", temperatue);
close(fd_bat_percent);
return atoi(percent);
}
void *publish_battery_info(void *data)
{
char payload[20];
int percent;
int rc;
struct mosquitto *mosq = (struct mosquitto *)data;
while (true) {
/* Get our pretend data */
percent = get_battery_percent();
printf("Battery : %d\n", percent);
/* Print it to a string for easy human reading - payload format is highly
* application dependent. */
snprintf(payload, sizeof(payload), "%d", percent);
/* Publish the message
* mosq - our client instance
* *mid = NULL - we don't want to know what the message id for this message is
* topic = "example/temperature" - the topic on which this message will be published
* payloadlen = strlen(payload) - the length of our payload in bytes
* payload - the actual payload
* qos = 2 - publish with QoS 2 for this example
* retain = false - do not use the retained message feature for this message
*/
// rc = mosquitto_publish(mosq, NULL, "example/temperature", strlen(payload), payload, 2, false);
// rc = mosquitto_publish(mosq, NULL, "home/kitchen/light", strlen(payload), payload, 2, false);
rc = mosquitto_publish(mosq, NULL, "Loona/sensor/battery_percent/state",
strlen(payload), payload, 2, false);
if (rc != MOSQ_ERR_SUCCESS) {
fprintf(stderr, "Error publishing: %s\n", mosquitto_strerror(rc));
}
}
}
int send_discovery(struct mosquitto *mosq)
{
printf("Sending Temperature sensor Discovery message ...");
send_sensor_temperature_discovery(mosq);
printf("Sending Battery Percent sensor Discovery message ...");
send_sensor_battery_percent_discovery(mosq);
return 0;
}
void on_connect(struct mosquitto *mosq, void *obj, int reason_code)
{
int rc;
printf("%s, %d, %s\n", __func__, __LINE__, mosquitto_connack_string(reason_code));
if (reason_code != 0) {
/* If the connection fails for any reason, we don't want to keep on
* retrying in this example, so disconnect. Without this, the client
* will attempt to reconnect. */
mosquitto_disconnect(mosq);
}
send_discovery(mosq);
}
int main(int argc, char **argv)
{
int rc;
/* Required before calling other mosquitto functions */
mosquitto_lib_init();
struct mosquitto *mosq = mosquitto_new(NULL, true, NULL);
if (!mosq) {
fprintf(stderr, "oom\n");
return 1;
}
mosquitto_connect_callback_set(mosq, on_connect);
mosquitto_username_pw_set(mosq, "keyi", "keyifamily");
rc = mosquitto_connect(mosq, "192.168.0.190", 1883, 60);
if (rc != MOSQ_ERR_SUCCESS) {
mosquitto_destroy(mosq);
fprintf(stderr, "Error: %s\n", mosquitto_strerror(rc));
return 1;
}
/* Run the network loop in a background thread, this call returns quickly. */
rc = mosquitto_loop_start(mosq);
if (rc != MOSQ_ERR_SUCCESS) {
mosquitto_destroy(mosq);
fprintf(stderr, "Error: %s\n", mosquitto_strerror(rc));
return 1;
}
/* here to create threads */
pthread_t tid_temperature_cpu, tid_battery_percent;
pthread_create(&tid_temperature_cpu, NULL, publish_sensor_data, mosq);
pthread_create(&tid_battery_percent, NULL, publish_battery_info, mosq);
while (true) {
// publish_sensor_data(mosq);
// publish_battery_info(mosq);
sleep(1);
}
mosquitto_loop_forever(mosq, -1, 1);
mosquitto_lib_cleanup();
return 0;
}
参考资料
mqtt发现服务官方文档 https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery
基于python的mqtt发现服务 https://github.com/leech001/hass-mqtt-discovery