TCL AC driver progress + LowMidHigh etc channels HA discovery

* Buzzer

* Update obk_config.h

* Display

* uart test stub

* unfinished TCL_DoDiscovery

* ha

* Update drv_tclAC.c

* ha mode send

* unfinished not working

* Update drv_tclAC.c

* Update hass.h

* better discovery

* Update obk_config.h
This commit is contained in:
openshwprojects
2025-05-30 16:10:36 +02:00
committed by GitHub
parent a2c9d19147
commit d6f7678dc6
12 changed files with 777 additions and 201 deletions

View File

@ -33,7 +33,7 @@ const char *g_template_lowMidHigh = "{% if value == '0' %}\n"
/// @param index Entity index (Ignored for RGB)
/// @param uniq_id Array to populate (should be of size HASS_UNIQUE_ID_SIZE)
/// @param asensdatasetix dataset index for ENERGY_METER_SENSOR, otherwise 0
void hass_populate_unique_id(ENTITY_TYPE type, int index, char* uniq_id, int asensdatasetix) {
void hass_populate_unique_id(ENTITY_TYPE type, int index, char* uniq_id, int asensdatasetix, const char *title) {
//https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements
//mentions that mac can be used for unique_id and deviceName contains that.
const char* longDeviceName = CFG_GetDeviceName();
@ -48,7 +48,13 @@ void hass_populate_unique_id(ENTITY_TYPE type, int index, char* uniq_id, int ase
case LIGHT_RGBCW:
sprintf(uniq_id, "%s_%s", longDeviceName, "light");
break;
case HASS_FAN:
sprintf(uniq_id, "%s_fan", longDeviceName);
break;
case HASS_HVAC:
sprintf(uniq_id, "%s_thermostat", longDeviceName);
break;
case RELAY:
sprintf(uniq_id, "%s_%s_%d", longDeviceName, "relay", index);
break;
@ -129,6 +135,10 @@ void hass_populate_unique_id(ENTITY_TYPE type, int index, char* uniq_id, int ase
sprintf(uniq_id, "%s_%s_%d", longDeviceName, "sensor", index);
break;
}
if (title) {
strcat(uniq_id, "_");
strcat(uniq_id, title);
}
// There can be no spaces in this name!
// See: https://www.elektroda.com/rtvforum/topic4000620.html
STR_ReplaceWhiteSpacesWithUnderscore(uniq_id);
@ -142,7 +152,7 @@ void hass_populate_unique_id(ENTITY_TYPE type, int index, char* uniq_id, int ase
/// @param asensdatasetix dataset index for ENERGY_METER_SENSOR, otherwise 0
void hass_print_unique_id(http_request_t* request, const char* fmt, ENTITY_TYPE type, int index, int asensdatasetix) {
char uniq_id[HASS_UNIQUE_ID_SIZE];
hass_populate_unique_id(type, index, uniq_id, asensdatasetix);
hass_populate_unique_id(type, index, uniq_id, asensdatasetix, NULL);
hprintf255(request, fmt, uniq_id);
}
@ -165,6 +175,12 @@ void hass_populate_device_config_channel(ENTITY_TYPE type, char* uniq_id, HassDe
case BINARY_SENSOR:
sprintf(info->channel, "binary_sensor/%s/config", uniq_id);
break;
case HASS_HVAC:
sprintf(info->channel, "climate/%s/config", uniq_id);
break;
case HASS_FAN:
sprintf(info->channel, "fan/%s/config", uniq_id);
break;
case SMOKE_SENSOR:
case CO2_SENSOR:
case TVOC_SENSOR:
@ -208,6 +224,186 @@ cJSON* hass_build_device_node(cJSON* ids) {
return dev;
}
// TODO, broken
//HassDeviceInfo* hass_createFanWithModes(const char *label, const char *stateTopic, const char *command, const char **options, int numOptions) {
// HassDeviceInfo* info = hass_init_device_info(HASS_FAN, 0, NULL, NULL, 0);
// if (info == NULL) {
// addLogAdv(LOG_ERROR, LOG_FEATURE_HASS, "Failed to initialize HassDeviceInfo for fan");
// return NULL;
// }
//
// cJSON_ReplaceItemInObject(info->root, "name", cJSON_CreateString(label));
//
// char uniq_id[HASS_UNIQUE_ID_SIZE];
// snprintf(uniq_id, HASS_UNIQUE_ID_SIZE, "%s_%s", info->unique_id, label);
// STR_ReplaceWhiteSpacesWithUnderscore(uniq_id);
// cJSON_ReplaceItemInObject(info->root, "uniq_id", cJSON_CreateString(uniq_id));
//
// sprintf(info->channel, "fan/%s/config", uniq_id);
// STR_ReplaceWhiteSpacesWithUnderscore(info->channel);
//
// cJSON_AddStringToObject(info->root, "pr_mode_stat_t", stateTopic);
// sprintf(g_hassBuffer, "cmnd/%s/%s", CFG_GetMQTTClientId(), command);
// cJSON_AddStringToObject(info->root, "pr_mode_cmd_t", g_hassBuffer);
// cJSON_AddStringToObject(info->root, "dev_cla", "fan");
// cJSON_AddItemToObject(info->root, "osc", cJSON_CreateBool(false));
// cJSON_AddItemToObject(info->root, "percentage", cJSON_CreateBool(false));
// cJSON_AddItemToObject(info->root, "pr_modes", cJSON_CreateStringArray(options, numOptions));
//
// return info;
//}
HassDeviceInfo* hass_createSelectEntity(const char* state_topic, const char* command_topic, int numoptions,
const char* options[], const char* title) {
// Initialize device info for a single select entity
HassDeviceInfo* info = hass_init_device_info(HASS_SELECT, 0, NULL, NULL, 0, title);
// Set entity properties
cJSON_AddStringToObject(info->root, "name", title);
cJSON_AddStringToObject(info->root, "unique_id", title); // Using title as unique_id for simplicity; adjust if needed
cJSON_AddStringToObject(info->root, "state_topic", state_topic);
cJSON_AddStringToObject(info->root, "command_topic", command_topic);
// Create options array from provided options
cJSON* select_options = cJSON_CreateArray();
for (int i = 0; i < numoptions; i++) {
cJSON_AddItemToArray(select_options, cJSON_CreateString(options[i]));
}
cJSON_AddItemToObject(info->root, "options", select_options);
// Set availability
cJSON_AddStringToObject(info->root, "availability_topic", "~/status");
cJSON_AddStringToObject(info->root, "payload_available", "online");
cJSON_AddStringToObject(info->root, "payload_not_available", "offline");
// Set configuration channel for select entity
sprintf(info->channel, "select/%s/config", info->unique_id);
// Update device info
cJSON* dev = info->device;
cJSON_ReplaceItemInObject(dev, "manufacturer", cJSON_CreateString("Custom"));
cJSON_ReplaceItemInObject(dev, "model", cJSON_CreateString("C-Swing-Control"));
return info;
}
// Helper function to generate a dictionary string for value_template mapping integers to strings
static void generate_value_template(int numoptions, const char* options[], char* buffer, size_t bufsize) {
size_t len = 0;
len += snprintf(buffer + len, bufsize - len, "{{ {");
for (int i = 0; i < numoptions && len < bufsize; i++) {
len += snprintf(buffer + len, bufsize - len, "'%d': '%s'%s", i, options[i], i < numoptions - 1 ? ", " : "");
}
snprintf(buffer + len, bufsize - len, "} [value] }}");
}
// Helper function to generate a dictionary string for command_template mapping strings to integers
static void generate_command_template(int numoptions, const char* options[], char* buffer, size_t bufsize) {
size_t len = 0;
len += snprintf(buffer + len, bufsize - len, "{{ {");
for (int i = 0; i < numoptions && len < bufsize; i++) {
len += snprintf(buffer + len, bufsize - len, "'%s': '%d'%s", options[i], i, i < numoptions - 1 ? ", " : "");
}
snprintf(buffer + len, bufsize - len, "} [value] }}");
}
HassDeviceInfo* hass_createSelectEntityIndexed(const char* state_topic, const char* command_topic, int numoptions,
const char* options[], const char* title) {
HassDeviceInfo* info = hass_init_device_info(HASS_SELECT, 0, NULL, NULL, 0, title);
cJSON_AddStringToObject(info->root, "name", title);
cJSON_AddStringToObject(info->root, "unique_id", title);
cJSON_AddStringToObject(info->root, "state_topic", state_topic);
cJSON_AddStringToObject(info->root, "command_topic", command_topic);
cJSON* select_options = cJSON_CreateArray();
for (int i = 0; i < numoptions; i++) {
cJSON_AddItemToArray(select_options, cJSON_CreateString(options[i]));
}
cJSON_AddItemToObject(info->root, "options", select_options);
char value_template[512];
generate_value_template(numoptions, options, value_template, sizeof(value_template));
cJSON_AddStringToObject(info->root, "value_template", value_template);
char command_template[512];
generate_command_template(numoptions, options, command_template, sizeof(command_template));
cJSON_AddStringToObject(info->root, "command_template", command_template);
cJSON_AddStringToObject(info->root, "availability_topic", "~/status");
cJSON_AddStringToObject(info->root, "payload_available", "online");
cJSON_AddStringToObject(info->root, "payload_not_available", "offline");
sprintf(info->channel, "select/%s/config", info->unique_id);
cJSON* dev = info->device;
cJSON_ReplaceItemInObject(dev, "manufacturer", cJSON_CreateString("Custom"));
cJSON_ReplaceItemInObject(dev, "model", cJSON_CreateString("C-Swing-Control"));
return info;
}
HassDeviceInfo* hass_createHVAC(float min, float max, float step, const char **fanOptions, int numFanOptions) {
HassDeviceInfo* info = hass_init_device_info(HASS_HVAC, 0, NULL, NULL, 0, 0);
// Set the name for the HVAC device
cJSON_AddStringToObject(info->root, "name", "Smart Thermostat");
// Set temperature unit
cJSON_AddStringToObject(info->root, "temperature_unit", "C");
// Set temperature topics
cJSON_AddStringToObject(info->root, "current_temperature_topic", "~/CurrentTemperature/get");
sprintf(g_hassBuffer, "cmnd/%s/TargetTemperature", CFG_GetMQTTClientId());
cJSON_AddStringToObject(info->root, "temperature_command_topic", g_hassBuffer);
cJSON_AddStringToObject(info->root, "temperature_state_topic", "~/TargetTemperature/get");
// Set temperature range and step
cJSON_AddNumberToObject(info->root, "min_temp", min);
cJSON_AddNumberToObject(info->root, "max_temp", max);
cJSON_AddNumberToObject(info->root, "temp_step", step);
// Set mode topics
cJSON_AddStringToObject(info->root, "mode_state_topic", "~/ACMode/get");
sprintf(g_hassBuffer, "cmnd/%s/ACMode", CFG_GetMQTTClientId());
cJSON_AddStringToObject(info->root, "mode_command_topic", g_hassBuffer);
// Add supported modes
cJSON* modes = cJSON_CreateArray();
cJSON_AddItemToArray(modes, cJSON_CreateString("off"));
cJSON_AddItemToArray(modes, cJSON_CreateString("heat"));
cJSON_AddItemToArray(modes, cJSON_CreateString("cool"));
cJSON_AddItemToObject(info->root, "modes", modes);
if (fanOptions && numFanOptions) {
// Add fan mode topics
cJSON_AddStringToObject(info->root, "fan_mode_state_topic", "~/FanMode/get");
sprintf(g_hassBuffer, "cmnd/%s/FanMode", CFG_GetMQTTClientId());
cJSON_AddStringToObject(info->root, "fan_mode_command_topic", g_hassBuffer);
// Add supported fan modes
cJSON* fan_modes = cJSON_CreateArray();
for (int i = 0; i < numFanOptions; i++) {
const char *mode = fanOptions[i];
cJSON_AddItemToArray(fan_modes, cJSON_CreateString(mode));
}
cJSON_AddItemToArray(fan_modes, cJSON_CreateString("high"));
cJSON_AddItemToObject(info->root, "fan_modes", fan_modes);
}
// Set availability topic
cJSON_AddStringToObject(info->root, "availability_topic", "~/status");
cJSON_AddStringToObject(info->root, "payload_available", "online");
cJSON_AddStringToObject(info->root, "payload_not_available", "offline");
// Update device configuration channel for HVAC
sprintf(info->channel, "climate/%s/config", info->unique_id);
// Update device info
cJSON* dev = info->device;
cJSON_ReplaceItemInObject(dev, "manufacturer", cJSON_CreateString("Custom"));
cJSON_ReplaceItemInObject(dev, "model", cJSON_CreateString("C-Thermo"));
return info;
}
/// @brief Initializes HomeAssistant device discovery storage with common values.
/// @param type
/// @param index This is used to generate generate unique_id and name.
@ -217,11 +413,11 @@ cJSON* hass_build_device_node(cJSON* ids) {
/// @param payload_off The payload that represents disabled state. This is not added for POWER_SENSOR.
/// @param asensdatasetix dataset index for ENERGY_METER_SENSOR, otherwise 0
/// @return
HassDeviceInfo* hass_init_device_info(ENTITY_TYPE type, int index, const char* payload_on, const char* payload_off, int asensdatasetix) {
HassDeviceInfo* hass_init_device_info(ENTITY_TYPE type, int index, const char* payload_on, const char* payload_off, int asensdatasetix, const char *title) {
HassDeviceInfo* info = os_malloc(sizeof(HassDeviceInfo));
addLogAdv(LOG_DEBUG, LOG_FEATURE_HASS, "hass_init_device_info=%p", info);
hass_populate_unique_id(type, index, info->unique_id, asensdatasetix);
hass_populate_unique_id(type, index, info->unique_id, asensdatasetix, title);
hass_populate_device_config_channel(type, info->unique_id, info);
info->ids = cJSON_CreateArray();
@ -335,6 +531,10 @@ HassDeviceInfo* hass_init_device_info(ENTITY_TYPE type, int index, const char* p
break;
}
}
if (title) {
strcat(g_hassBuffer, "_");
strcat(g_hassBuffer, title);
}
cJSON_AddStringToObject(info->root, "name", g_hassBuffer);
cJSON_AddStringToObject(info->root, "~", CFG_GetMQTTClientId()); //base topic
// remove availability information for sensor to keep last value visible on Home Assistant
@ -362,16 +562,41 @@ HassDeviceInfo* hass_init_device_info(ENTITY_TYPE type, int index, const char* p
return info;
}
HassDeviceInfo* hass_createToggle(const char *label, const char *stateTopic, const char *command) {
HassDeviceInfo* info = hass_init_device_info(RELAY, 0, "1", "0", 0, label);
if (info == NULL) {
addLogAdv(LOG_ERROR, LOG_FEATURE_HASS, "Failed to initialize HassDeviceInfo for toggle");
return NULL;
}
cJSON_ReplaceItemInObject(info->root, "name", cJSON_CreateString(label));
char uniq_id[HASS_UNIQUE_ID_SIZE];
snprintf(uniq_id, HASS_UNIQUE_ID_SIZE, "%s_%s", info->unique_id, label);
STR_ReplaceWhiteSpacesWithUnderscore(uniq_id);
cJSON_ReplaceItemInObject(info->root, "uniq_id", cJSON_CreateString(uniq_id));
// update the discovery channel with the new unique_id
sprintf(info->channel, "switch/%s/config", uniq_id);
STR_ReplaceWhiteSpacesWithUnderscore(info->channel);
cJSON_AddStringToObject(info->root, "stat_t", stateTopic);
sprintf(g_hassBuffer, "cmnd/%s/%s", CFG_GetMQTTClientId(), command);
cJSON_AddStringToObject(info->root, "cmd_t", g_hassBuffer);
return info;
}
/// @brief Initializes HomeAssistant relay device discovery storage.
/// @param index
/// @return
HassDeviceInfo* hass_init_relay_device_info(int index, ENTITY_TYPE type, bool bToggleInv) {
HassDeviceInfo* info;
if (bToggleInv) {
info = hass_init_device_info(type, index, "0", "1", 0);
info = hass_init_device_info(type, index, "0", "1", 0, NULL);
}
else {
info = hass_init_device_info(type, index, "1", "0", 0);
info = hass_init_device_info(type, index, "1", "0", 0, NULL);
}
sprintf(g_hassBuffer, "~/%i/get", index);
@ -393,7 +618,7 @@ HassDeviceInfo* hass_init_light_device_info(ENTITY_TYPE type) {
//We can just use 1 to generate unique_id and name for single PWM.
//The payload_on/payload_off have to match the state_topic/command_topic values.
info = hass_init_device_info(type, 1, "1", "0", 0);
info = hass_init_device_info(type, 1, "1", "0", 0, NULL);
switch (type) {
case LIGHT_RGBCW:
@ -464,7 +689,7 @@ HassDeviceInfo* hass_init_binary_sensor_device_info(int index, bool bInverse) {
payload_off = "1";
payload_on = "0";
}
HassDeviceInfo* info = hass_init_device_info(BINARY_SENSOR, index, payload_on, payload_off, 0);
HassDeviceInfo* info = hass_init_device_info(BINARY_SENSOR, index, payload_on, payload_off, 0, NULL);
sprintf(g_hassBuffer, "~/%i/get", index);
cJSON_AddStringToObject(info->root, "stat_t", g_hassBuffer); //state_topic
@ -490,7 +715,7 @@ HassDeviceInfo* hass_init_energy_sensor_device_info(int index, int asensdataseti
//in twin mode, for ix0 is last OBK_CONSUMPTION_YESTERDAY, for ix1 ,OBK_CONSUMPTION_TODAY
if ((index > OBK_CONSUMPTION_STORED_LAST[asensdatasetix]) && (index <= OBK_CONSUMPTION__DAILY_LAST)) return info;
#endif
info = hass_init_device_info(ENERGY_METER_SENSOR, index, NULL, NULL, asensdatasetix);
info = hass_init_device_info(ENERGY_METER_SENSOR, index, NULL, NULL, asensdatasetix, NULL);
cJSON_AddStringToObject(info->root, "dev_cla", DRV_GetEnergySensorNamesEx(asensdatasetix,index)->hass_dev_class); //device_class=voltage,current,power, energy, timestamp
//20241024 XJIKKA unit_of_meas is set bellow (was set twice)
@ -555,7 +780,7 @@ HassDeviceInfo* hass_init_light_singleColor_onChannels(int toggle, int dimmer, i
const char* clientId;
clientId = CFG_GetMQTTClientId();
dev_info = hass_init_device_info(LIGHT_PWM, toggle, "1", "0", 0);
dev_info = hass_init_device_info(LIGHT_PWM, toggle, "1", "0", 0, NULL);
sprintf(g_hassBuffer, "~/%i/get", toggle);
cJSON_AddStringToObject(dev_info->root, "stat_t", g_hassBuffer); //state_topic
@ -577,7 +802,7 @@ HassDeviceInfo* hass_init_light_singleColor_onChannels(int toggle, int dimmer, i
/// @return
HassDeviceInfo* hass_init_sensor_device_info(ENTITY_TYPE type, int channel, int decPlaces, int decOffset, int divider) {
//Assuming that there is only one DHT setup per device which keeps uniqueid/names simpler
HassDeviceInfo* info = hass_init_device_info(type, channel, NULL, NULL, 0); //using channel as index to generate uniqueId
HassDeviceInfo* info = hass_init_device_info(type, channel, NULL, NULL, 0, NULL); //using channel as index to generate uniqueId
//https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes
switch (type) {

View File

@ -95,7 +95,9 @@ typedef enum {
WATER_QUALITY_TDS,
/// @brief Battery level sensor in perc, under channel topic
BATTERY_CHANNEL_SENSOR,
HASS_HVAC,
HASS_FAN,
HASS_SELECT
} ENTITY_TYPE;
//unique_id is defined in hass_populate_unique_id and is based on CFG_GetDeviceName() whose size is CGF_DEVICE_NAME_SIZE.
@ -122,12 +124,21 @@ typedef struct HassDeviceInfo_s {
void hass_print_unique_id(http_request_t* request, const char* fmt, ENTITY_TYPE type, int index, int asensdatasetix);
HassDeviceInfo* hass_init_relay_device_info(int index, ENTITY_TYPE type, bool bInverse);
HassDeviceInfo* hass_init_device_info(ENTITY_TYPE type, int index, const char* payload_on, const char* payload_off, int asensdatasetix);
HassDeviceInfo* hass_init_device_info(ENTITY_TYPE type, int index, const char* payload_on, const char* payload_off, int asensdatasetix, const char *title);
HassDeviceInfo* hass_init_light_device_info(ENTITY_TYPE type);
HassDeviceInfo* hass_init_energy_sensor_device_info(int index, int asensdatasetix);
HassDeviceInfo* hass_init_light_singleColor_onChannels(int toggle, int dimmer, int brightness_scale);
HassDeviceInfo* hass_init_binary_sensor_device_info(int index, bool bInverse);
HassDeviceInfo* hass_init_sensor_device_info(ENTITY_TYPE type, int channel, int decPlaces, int decOffset, int divider);
HassDeviceInfo* hass_createHVAC(float min, float max, float step, const char **fanOptions, int numFanOptions);
HassDeviceInfo* hass_createFanWithModes(const char *label, const char *stateTopic,
const char *command, const char **options, int numOptions);
HassDeviceInfo* hass_createSelectEntity(const char* state_topic, const char* command_topic, int numoptions,
const char* options[], const char* title);
HassDeviceInfo* hass_createSelectEntityIndexed(const char* state_topic, const char* command_topic, int numoptions,
const char* options[], const char* title);
HassDeviceInfo* hass_createToggle(const char *label, const char *stateTopic, const char *commandTopic);
const char* hass_build_discovery_json(HassDeviceInfo* info);
void hass_free_device_info(HassDeviceInfo* info);
char *hass_generate_multiplyAndRound_template(int decimalPlacesForRounding, int decimalPointOffset, int divider);

View File

@ -78,6 +78,7 @@ const char* g_typesLowMidHighHighest[] = { "Low","Mid","High","Highest" };
const char* g_typesOffOnRemember[] = { "Off", "On", "Remember" };
const char* g_typeLowMidHigh[] = { "Low","Mid","High" };
const char* g_typesLowestLowMidHighHighest[] = { "Lowest", "Low", "Mid", "High", "Highest" };;
const char* g_typeOpenStopClose[] = { "Open","Stop","Close" };
#define ADD_OPTION(t,a) if(type == t) { *numTypes = sizeof(a)/sizeof(a[0]); return a; }
@ -89,6 +90,7 @@ const char **Channel_GetOptionsForChannelType(int type, int *numTypes) {
ADD_OPTION(ChType_LowMidHighHighest, g_typesLowMidHighHighest);
ADD_OPTION(ChType_OffOnRemember, g_typesOffOnRemember);
ADD_OPTION(ChType_LowMidHigh, g_typeLowMidHigh);
ADD_OPTION(ChType_OpenStopClose, g_typeOpenStopClose);
*numTypes = 0;
return 0;
@ -495,6 +497,9 @@ int http_fn_index(http_request_t* request) {
if (channelType == ChType_OffOnRemember) {
what = "memory";
}
else if (channelType == ChType_OpenStopClose) {
what = "mode";
}
else {
what = "speed";
}
@ -1800,6 +1805,7 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
hooks.free_fn = os_free;
cJSON_InitHooks(&hooks);
DRV_OnHassDiscovery(topic);
#if ENABLE_ADVANCED_CHANNELTYPES_DISCOVERY
// try to pair toggles with dimmers. This is needed only for TuyaMCU,
@ -2024,7 +2030,7 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
{
dev_info = hass_init_sensor_device_info(READONLYLOWMIDHIGH_SENSOR, i, -1, -1, 1);
}
break;
break;
case ChType_BatteryLevelPercent:
{
dev_info = hass_init_sensor_device_info(BATTERY_CHANNEL_SENSOR, i, -1, -1, 1);
@ -2172,6 +2178,30 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
dev_info = hass_init_sensor_device_info(WATER_QUALITY_TDS, i, -1, 2, 1);
}
break;
default:
{
int numOptions;
const char **options = Channel_GetOptionsForChannelType(type, &numOptions);
if (options && numOptions) {
// backlog setChannelType 2 LowMidHigh; scheduleHADiscovery 1
// backlog setChannelType 3 OpenStopClose; scheduleHADiscovery 1
char stateTopic[32];
char cmdTopic[32];
char title[64];
// TODO: lengths
strcpy(title, CHANNEL_GetLabel(i));
sprintf(stateTopic, "~/%i/get", i);
sprintf(cmdTopic, "~/%i/set", i);
dev_info = hass_createSelectEntityIndexed(
stateTopic,
cmdTopic,
numOptions,
options,
title
);
}
}
break;
}
if (dev_info) {
MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN);