Skip to content

Commit 630b8be

Browse files
authored
Merge pull request #470 from dontsovcmc/mqtt_ha_spec
MQTT: честный статус, автообнаружение HA по спецификации
2 parents 52e8d4d + 9306063 commit 630b8be

33 files changed

Lines changed: 1087 additions & 359 deletions

‎ESP8266/plan/tests_plan.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,10 @@ jobs:
359359
- `test_ha` — допустимые пары `device_class` + `unit_of_measurement`. HA молча
360360
выбрасывает сущность с недопустимой парой: сенсор не появляется, ошибка
361361
видна только в логе самого HA (#356)
362+
- `test_discovery` — когда переотправлять автообнаружение HA и какие сущности
363+
нужны входу. Отпечаток обязан меняться от каждого поля, из которого собраны
364+
конфиги: иначе смена пропадёт до нажатия кнопки. Ненужные сущности входа
365+
прошивка удаляет, поэтому ошибка в списке стирает из HA живую сущность
362366
- `test_idle` — режим "выходить на связь только при расходе" (#361):
363367
обнаружение расхода, решение включать ли Wi-Fi, отметка "устройство живо"
364368
раз в трое суток и интервал, который уезжает на сервер вместо периода

‎ESP8266/src/core/discovery.cpp‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#include "discovery.h"
2+
#include "types.h"
3+
4+
namespace
5+
{
6+
// FNV-1a, 32 бита
7+
const uint32_t FNV_OFFSET = 2166136261u;
8+
const uint32_t FNV_PRIME = 16777619u;
9+
10+
uint32_t mix_byte(uint32_t hash, uint8_t byte)
11+
{
12+
return (hash ^ byte) * FNV_PRIME;
13+
}
14+
15+
// С завершающим нулём: иначе "ab"+"c" и "a"+"bc" дали бы одно и то же
16+
uint32_t mix_string(uint32_t hash, const char *s)
17+
{
18+
if (s != nullptr)
19+
{
20+
for (; *s; ++s)
21+
hash = mix_byte(hash, (uint8_t)*s);
22+
}
23+
return mix_byte(hash, 0);
24+
}
25+
}
26+
27+
uint32_t discovery_signature(const DiscoveryState &state)
28+
{
29+
uint32_t hash = FNV_OFFSET;
30+
31+
hash = mix_string(hash, state.firmware);
32+
hash = mix_byte(hash, state.attiny_version);
33+
hash = mix_byte(hash, state.model);
34+
hash = mix_byte(hash, state.counter_type0);
35+
hash = mix_byte(hash, state.counter_type1);
36+
hash = mix_byte(hash, state.counter_name0);
37+
hash = mix_byte(hash, state.counter_name1);
38+
hash = mix_string(hash, state.topic);
39+
hash = mix_string(hash, state.discovery_topic);
40+
41+
return hash == 0 ? 1 : hash;
42+
}
43+
44+
bool channel_entity_wanted(uint8_t counter_type, ChannelEntity entity)
45+
{
46+
switch (entity)
47+
{
48+
case ChannelEntity::INPUT_TYPE:
49+
// Иначе включить выключенный вход из HA было бы нечем
50+
return true;
51+
case ChannelEntity::RETIRED:
52+
return false;
53+
case ChannelEntity::WET:
54+
return counter_type == CounterType::LEAKAGE || counter_type == CounterType::LEAKAGE_NC;
55+
default:
56+
return counts_impulses(counter_type);
57+
}
58+
}

‎ESP8266/src/core/discovery.h‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#ifndef _WATERIUS_CORE_DISCOVERY_h
2+
#define _WATERIUS_CORE_DISCOVERY_h
3+
4+
/*
5+
Автообнаружение Home Assistant: когда его публиковать и из чего оно состоит.
6+
7+
Устройство спит и не слышит `homeassistant/status`, поэтому переслать конфиги
8+
по просьбе HA не может. Вместо этого оно помнит отпечаток опубликованного и
9+
публикует заново, как только отпечаток разошёлся с текущим состоянием.
10+
11+
Часть чистого ядра src/core: без Arduino.h.
12+
*/
13+
14+
#include <stdint.h>
15+
16+
// То, от чего зависит содержимое конфигов
17+
struct DiscoveryState
18+
{
19+
const char *firmware; // FIRMWARE_VERSION: набор сущностей и sw_version
20+
uint8_t attiny_version; // тоже в sw_version
21+
uint8_t model;
22+
uint8_t counter_type0;
23+
uint8_t counter_type1;
24+
uint8_t counter_name0;
25+
uint8_t counter_name1;
26+
const char *topic; // stat_t и cmd_t сущностей
27+
const char *discovery_topic;
28+
};
29+
30+
// Ноль не возвращает никогда: им в настройках помечено «ничего не опубликовано»
31+
uint32_t discovery_signature(const DiscoveryState &state);
32+
33+
// Группы сущностей входа
34+
enum class ChannelEntity : uint8_t
35+
{
36+
INPUT_TYPE, // ctype
37+
WET, // alarm_wet
38+
READINGS, // chN: sensor и number
39+
SERIAL_NUMBER,
40+
FACTOR,
41+
RESOURCE, // cname
42+
ALARM_CONFIG, // av, ar, ah, as
43+
ALARM_STATE, // alarm_flow, alarm_leak, alarm_stop
44+
RETIRED, // af, al: были в сборках dev 2.0.47, у брокеров могли остаться
45+
};
46+
47+
/*
48+
Нужна ли сущность входу такого типа. Ненужную прошивка не пропускает, а
49+
удаляет у брокера: иначе после смены типа она осталась бы в HA навсегда.
50+
*/
51+
bool channel_entity_wanted(uint8_t counter_type, ChannelEntity entity);
52+
53+
#endif

‎ESP8266/src/core/types.h‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,11 +579,18 @@ struct Settings
579579
*/
580580
uint8_t alarm_confirm = CONFIRM_ANY;
581581

582+
/*
583+
Отпечаток опубликованного автообнаружения (core/discovery.h). Ноль -
584+
ничего не опубликовано: так поле приезжает у прошитых устройств, и первый
585+
же сеанс после обновления переотправит конфиги.
586+
*/
587+
uint32_t discovery_signature = 0;
588+
582589
/*
583590
Зарезервируем кучу места, чтобы не писать конвертер конфигураций.
584591
Будет актуально для On-the-Air обновлений
585592
*/
586-
uint8_t reserved9[31] = {0};
593+
uint8_t reserved9[27] = {0};
587594

588595
}; // 960 байт
589596

‎ESP8266/src/ha/publish.cpp‎

Lines changed: 24 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,156 +1,47 @@
11
#include "publish.h"
22
#include "Logging.h"
3-
#include "setup.h"
43
#include <PubSubClient.h>
5-
#include <ArduinoJson.h>
6-
7-
extern Settings sett;
84

95
/**
10-
* @brief Публикация топика в MQTT в различных режимах
6+
* @brief Публикация топика в MQTT
117
*
12-
* @param mqtt_client клиент MQTT
13-
* @param topic строка с топиком
14-
* @param payload содержимое топика
15-
* @param mode режим публикации, режим по умолчанию PUBLISH_MODE_BIG
16-
*/
17-
void publish(PubSubClient &mqtt_client, const String &topic, const String &payload, const int mode)
18-
{
19-
switch (mode)
20-
{
21-
case PUBLISH_MODE_SIMPLE:
22-
publish_simple(mqtt_client, topic, payload);
23-
break;
24-
case PUBLISH_MODE_CHUNKED:
25-
publish_chunked(mqtt_client, topic, payload);
26-
break;
27-
case PUBLISH_MODE_BIG:
28-
default:
29-
publish_big(mqtt_client, topic, payload);
30-
}
31-
}
32-
33-
/**
34-
* @brief Публикация топика в MQTT по частям,
35-
* используется в случае если очень много информации
8+
* Без промежуточного буфера: сообщение может быть больше буфера клиента.
9+
* QoS 0, поэтому «опубликовано» значит «записано в сокет целиком» -
10+
* подтверждения от брокера нет. endPublish у PubSubClient 2.8 всегда
11+
* возвращает 1, судить о результате по нему нельзя.
3612
*
3713
* @param mqtt_client клиент MQTT
3814
* @param topic строка с топиком
3915
* @param payload содержимое топика
16+
* @param retain флаг retain
17+
* @return true сообщение ушло в сокет целиком
4018
*/
41-
void publish_chunked(PubSubClient &mqtt_client,
42-
const String &topic,
43-
const String &payload,
44-
const unsigned int chunk_size)
19+
bool publish(PubSubClient &mqtt_client, const String &topic, const String &payload, bool retain)
4520
{
4621
LOG_INFO(F("Free memory: ") << ESP.getFreeHeap());
4722
LOG_INFO(F("MQTT: Publish Topic: ") << topic);
4823
LOG_INFO(F("MQTT: Payload Size: ") << payload.length());
4924
LOG_DEBUG(F("MQTT: Payload: ") << payload);
25+
LOG_INFO(F("MQTT: Retain: ") << retain);
5026

51-
unsigned int len = payload.length();
52-
const uint8_t *buf = (const uint8_t *)payload.c_str();
53-
54-
if (mqtt_client.beginPublish(topic.c_str(), len, (bool)sett.mqtt_retain))
27+
const unsigned int len = payload.length();
28+
if (!mqtt_client.beginPublish(topic.c_str(), len, retain))
5529
{
56-
while (len > 0)
57-
{
58-
if (len >= chunk_size)
59-
{
60-
mqtt_client.write(buf, chunk_size);
61-
buf += chunk_size;
62-
len -= chunk_size;
63-
LOG_INFO(F("MQTT: Sended chunk size: ") << chunk_size);
64-
}
65-
else
66-
{
67-
mqtt_client.write(buf, len);
68-
LOG_INFO(F("MQTT: Sended chunk size: ") << len);
69-
break;
70-
}
71-
}
72-
if (mqtt_client.endPublish())
73-
{
74-
LOG_INFO(F("MQTT: Published succesfully"));
75-
}
76-
else
77-
{
78-
LOG_ERROR(F("MQTT: Publish failed"));
79-
}
30+
LOG_ERROR(F("MQTT: Publish failed: no connection"));
31+
return false;
8032
}
81-
else
82-
{
83-
LOG_ERROR(F("MQTT: Client not connected."));
84-
}
85-
}
86-
87-
/**
88-
* @brief Публикация топика в MQTT (основной метод)
89-
* не использует промежуточных буферов,
90-
* сообщение может иметь размер больше 250 байт
91-
*
92-
* @param mqtt_client клиент MQTT
93-
* @param topic строка с топиком
94-
* @param payload содержимое топика
95-
*/
96-
void publish_big(PubSubClient &mqtt_client,
97-
const String &topic,
98-
const String &payload)
99-
{
100-
LOG_INFO(F("Free memory: ") << ESP.getFreeHeap());
101-
LOG_INFO(F("MQTT: Publish Topic: ") << topic);
102-
LOG_INFO(F("MQTT: Payload Size: ") << payload.length());
103-
LOG_DEBUG(F("MQTT: Payload: ") << payload);
10433

105-
unsigned int len = payload.length();
106-
LOG_INFO(F("MQTT: Retain: ") << sett.mqtt_retain);
107-
if (mqtt_client.beginPublish(topic.c_str(), len, (bool)sett.mqtt_retain))
108-
{
109-
if (mqtt_client.print(payload.c_str()) == len)
110-
{
111-
LOG_INFO(F("MQTT: Published succesfully"));
112-
}
113-
else
114-
{
115-
LOG_ERROR(F("MQTT: Publish failed"));
116-
}
34+
const bool sent = mqtt_client.print(payload.c_str()) == len;
35+
mqtt_client.endPublish();
11736

118-
mqtt_client.endPublish();
119-
}
120-
else
37+
if (!sent)
12138
{
122-
LOG_ERROR(F("MQTT: Client not connected."));
39+
LOG_ERROR(F("MQTT: Publish failed"));
40+
return false;
12341
}
124-
}
125-
/**
126-
* @brief Публикация топика в MQTT если сообщение меньше 250 символов
127-
*
128-
* @param mqtt_client клиент MQTT
129-
* @param topic строка с топиком
130-
* @param payload содержимое топика
131-
*/
132-
void publish_simple(PubSubClient &mqtt_client, const String &topic, const String &payload)
133-
{
134-
LOG_INFO(F("Free memory: ") << ESP.getFreeHeap());
135-
LOG_INFO(F("MQTT: Publish Topic: ") << topic);
136-
LOG_INFO(F("MQTT: Payload Size: ") << payload.length());
137-
LOG_DEBUG(F("MQTT: Payload: ") << payload);
13842

139-
if (mqtt_client.connected())
140-
{
141-
if (mqtt_client.publish(topic.c_str(), payload.c_str(), (bool)sett.mqtt_retain))
142-
{
143-
LOG_INFO(F("MQTT: Published succesfully"));
144-
}
145-
else
146-
{
147-
LOG_ERROR(F("MQTT: Publish failed"));
148-
}
149-
}
150-
else
151-
{
152-
LOG_ERROR(F("MQTT: Client not connected."));
153-
}
43+
LOG_INFO(F("MQTT: Published succesfully"));
44+
return true;
15445
}
15546

15647
/**
@@ -162,13 +53,16 @@ void publish_simple(PubSubClient &mqtt_client, const String &topic, const String
16253
*
16354
* @param mqtt_client клиент MQTT
16455
* @param topic строка с топиком
56+
* @return true пустое сообщение ушло
16557
*/
166-
void clear_retained(PubSubClient &mqtt_client, const String &topic)
58+
bool clear_retained(PubSubClient &mqtt_client, const String &topic)
16759
{
16860
LOG_INFO(F("MQTT: Remove retain message: ") << topic);
16961

17062
if (!mqtt_client.publish(topic.c_str(), "", true))
17163
{
17264
LOG_ERROR(F("MQTT: Publish failed"));
65+
return false;
17366
}
67+
return true;
17468
}

‎ESP8266/src/ha/publish.h‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,8 @@
1111
#define HA_PUBLISH_H_
1212

1313
#include <PubSubClient.h>
14-
#include <ArduinoJson.h>
1514

16-
#define MQTT_CHUNK_SIZE 128
17-
#define PUBLISH_MODE_BIG 0
18-
#define PUBLISH_MODE_CHUNKED 1
19-
#define PUBLISH_MODE_SIMPLE 2
20-
#define DEFAULT_PUBLISH_MODE PUBLISH_MODE_BIG
21-
22-
extern void publish(PubSubClient &mqtt_client, const String &topic, const String &payload, const int mode = DEFAULT_PUBLISH_MODE);
23-
extern void publish_big(PubSubClient &mqtt_client, const String &topic, const String &payload);
24-
extern void publish_simple(PubSubClient &mqtt_client, const String &topic, const String &payload);
25-
extern void publish_chunked(PubSubClient &mqtt_client, const String &topic, const String &payload, const unsigned int chunk_size=MQTT_CHUNK_SIZE);
26-
extern void clear_retained(PubSubClient &mqtt_client, const String &topic);
15+
extern bool publish(PubSubClient &mqtt_client, const String &topic, const String &payload, bool retain);
16+
extern bool clear_retained(PubSubClient &mqtt_client, const String &topic);
2717

2818
#endif

0 commit comments

Comments
 (0)