diff --git a/src/driver/drv_local.h b/src/driver/drv_local.h index 1e7bff7e7..4bbcf4666 100644 --- a/src/driver/drv_local.h +++ b/src/driver/drv_local.h @@ -193,6 +193,7 @@ void CSE7761_RunEverySecond(void); void TCL_Init(void); void TCL_UART_RunEverySecond(void); void TCL_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState); +void TCL_DoDiscovery(const char *topic); #define SM2135_DELAY 4 diff --git a/src/driver/drv_main.c b/src/driver/drv_main.c index 8bca42b7d..a547a3137 100644 --- a/src/driver/drv_main.c +++ b/src/driver/drv_main.c @@ -23,6 +23,7 @@ typedef struct driver_s { void(*runQuickTick)(); void(*stopFunc)(); void(*onChannelChanged)(int ch, int val); + void(*onHassDiscovery)(const char *topic); bool bLoaded; } driver_t; @@ -36,53 +37,53 @@ static driver_t g_drivers[] = { //drvdetail:"title":"TODO", //drvdetail:"descr":"TuyaMCU is a protocol used for communication between WiFI module and external MCU. This protocol is using usually RX1/TX1 port of BK chips. See [TuyaMCU dimmer example](https://www.elektroda.com/rtvforum/topic3929151.html), see [TH06 LCD humidity/temperature sensor example](https://www.elektroda.com/rtvforum/topic3942730.html), see [fan controller example](https://www.elektroda.com/rtvforum/topic3908093.html), see [simple switch example](https://www.elektroda.com/rtvforum/topic3906443.html)", //drvdetail:"requires":""} - { "TuyaMCU", TuyaMCU_Init, TuyaMCU_RunEverySecond, NULL, TuyaMCU_RunFrame, NULL, NULL, false }, + { "TuyaMCU", TuyaMCU_Init, TuyaMCU_RunEverySecond, NULL, TuyaMCU_RunFrame, NULL, NULL, NULL, false }, //drvdetail:{"name":"tmSensor", //drvdetail:"title":"TODO", //drvdetail:"descr":"The tmSensor must be used only when TuyaMCU is already started. tmSensor is a TuyaMcu Sensor, it's used for Low Power TuyaMCU communication on devices like TuyaMCU door sensor, or TuyaMCU humidity sensor. After device reboots, tmSensor uses TuyaMCU to request data update from the sensor and reports it on MQTT. Then MCU turns off WiFi module again and goes back to sleep. See an [example door sensor here](https://www.elektroda.com/rtvforum/topic3914412.html).", //drvdetail:"requires":""} - { "tmSensor", TuyaMCU_Sensor_Init, TuyaMCU_Sensor_RunEverySecond, NULL, NULL, NULL, NULL, false }, + { "tmSensor", TuyaMCU_Sensor_Init, TuyaMCU_Sensor_RunEverySecond, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_FREEZE //drvdetail:{"name":"FREEZE", //drvdetail:"title":"TODO", //drvdetail:"descr":"Freeze is a test driver for watchdog. Enabling this will freeze device main loop.", //drvdetail:"requires":""} - { "Freeze", Freeze_Init, Freeze_OnEverySecond, NULL, Freeze_RunFrame, NULL, NULL, false }, + { "Freeze", Freeze_Init, Freeze_OnEverySecond, NULL, Freeze_RunFrame, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_PIR //drvdetail:{"name":"PIR", //drvdetail:"title":"TODO", //drvdetail:"descr":"PIR", //drvdetail:"requires":""} - { "PIR", PIR_Init, PIR_OnEverySecond, PIR_AppendInformationToHTTPIndexPage, NULL, NULL, PIR_OnChannelChanged, false }, + { "PIR", PIR_Init, PIR_OnEverySecond, PIR_AppendInformationToHTTPIndexPage, NULL, NULL, PIR_OnChannelChanged, NULL, false }, #endif #if ENABLE_DRIVER_PIXELANIM //drvdetail:{"name":"PixelAnim", //drvdetail:"title":"TODO", //drvdetail:"descr":"PixelAnim provides a simple set of WS2812B animations", //drvdetail:"requires":""} - { "PixelAnim", PixelAnim_Init, NULL, NULL, PixelAnim_SetAnimQuickTick, NULL, NULL, false }, + { "PixelAnim", PixelAnim_Init, NULL, NULL, PixelAnim_SetAnimQuickTick, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_DRAWERS //drvdetail:{"name":"Drawers", //drvdetail:"title":"TODO", //drvdetail:"descr":"WS2812B driver wrapper with REST API for [smart drawers project](https://www.elektroda.com/rtvforum/topic4054134.html)", //drvdetail:"requires":""} - { "Drawers", Drawers_Init, NULL, NULL, Drawers_QuickTick, NULL, NULL, false }, + { "Drawers", Drawers_Init, NULL, NULL, Drawers_QuickTick, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_HGS02 //drvdetail:{"name":"HGS02", //drvdetail:"title":"TODO", //drvdetail:"descr":"[HGS02](https://www.elektroda.com/rtvforum/viewtopic.php?p=21177061#21177061)", //drvdetail:"requires":""} - { "HGS02", HGS02_Init, HGS02_RunEverySecond, NULL, NULL, NULL, NULL, false }, + { "HGS02", HGS02_Init, HGS02_RunEverySecond, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_TCL - { "TCL", TCL_Init, TCL_UART_RunEverySecond, TCL_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "TCL", TCL_Init, TCL_UART_RunEverySecond, TCL_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, TCL_DoDiscovery, false }, #endif #if ENABLE_DRIVER_OPENWEATHERMAP @@ -90,171 +91,178 @@ static driver_t g_drivers[] = { //drvdetail:"title":"TODO", //drvdetail:"descr":"OpenWeatherMap integration allows you to fetch current weather for your lat/long. You can later extract temperatura, humidity and pressure data and display it on main page.", //drvdetail:"requires":""} - { "OpenWeatherMap", DRV_OpenWeatherMap_Init, NULL, OWM_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "OpenWeatherMap", DRV_OpenWeatherMap_Init, NULL, OWM_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_WIDGET //drvdetail:{"name":"Widget", //drvdetail:"title":"TODO", //drvdetail:"descr":"Widget driver allows you to create custom HTML snippets that are displayed on main OBK page. Snippets are loaded from LittleFS file system and can use OBK REST API.", //drvdetail:"requires":""} - { "Widget", DRV_Widget_Init, NULL, DRV_Widget_AddToHtmlPage, NULL, NULL, NULL, false }, + { "Widget", DRV_Widget_Init, NULL, DRV_Widget_AddToHtmlPage, NULL, NULL, NULL, NULL, false }, #endif #if WINDOWS //drvdetail:{"name":"TestCharts", //drvdetail:"title":"TODO", //drvdetail:"descr":"Development only driver - a sample of chart generation with chart.js.", //drvdetail:"requires":""} - { "TestCharts", NULL, NULL, DRV_Test_Charts_AddToHtmlPage, NULL, NULL, NULL, false }, + { "TestCharts", NULL, NULL, DRV_Test_Charts_AddToHtmlPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_CHARTS //drvdetail:{"name":"Charts", //drvdetail:"title":"TODO", //drvdetail:"descr":"Charts driver allows you to create a customizable chart directly on your device. See [tutorial](https://www.elektroda.com/rtvforum/topic4075289.html).", //drvdetail:"requires":""} - { "Charts", DRV_Charts_Init, NULL, DRV_Charts_AddToHtmlPage, NULL, NULL, NULL, false }, + { "Charts", DRV_Charts_Init, NULL, DRV_Charts_AddToHtmlPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_NTP //drvdetail:{"name":"NTP", //drvdetail:"title":"TODO", //drvdetail:"descr":"NTP driver is required to get current time and date from web. Without it, there is no correct datetime. Put 'startDriver NTP' in short startup line or autoexec.bat to run it on start.", //drvdetail:"requires":""} - { "NTP", NTP_Init, NTP_OnEverySecond, NTP_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "NTP", NTP_Init, NTP_OnEverySecond, NTP_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_HTTPBUTTONS //drvdetail:{"name":"HTTPButtons", //drvdetail:"title":"TODO", //drvdetail:"descr":"This driver allows you to create custom, scriptable buttons on main WWW page. You can create those buttons in autoexec.bat and assign commands to them", //drvdetail:"requires":""} - { "HTTPButtons", DRV_InitHTTPButtons, NULL, NULL, NULL, NULL, NULL, false }, + { "HTTPButtons", DRV_InitHTTPButtons, NULL, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_TESTPOWER //drvdetail:{"name":"TESTPOWER", //drvdetail:"title":"TODO", //drvdetail:"descr":"This is a fake POWER measuring socket driver, only for testing", //drvdetail:"requires":""} - { "TESTPOWER", Test_Power_Init, Test_Power_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "TESTPOWER", Test_Power_Init, Test_Power_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_TESTLED //drvdetail:{"name":"TESTLED", //drvdetail:"title":"TODO", //drvdetail:"descr":"This is a fake I2C LED driver, only for testing", //drvdetail:"requires":""} - { "TESTLED", Test_LED_Driver_Init, Test_LED_Driver_RunEverySecond, NULL, NULL, NULL, Test_LED_Driver_OnChannelChanged, false }, + { "TESTLED", Test_LED_Driver_Init, Test_LED_Driver_RunEverySecond, NULL, NULL, NULL, Test_LED_Driver_OnChannelChanged, NULL, false }, +#endif +#if ENABLE_DRIVER_TESTUART + //drvdetail:{"name":"TESTUART", + //drvdetail:"title":"TODO", + //drvdetail:"descr":"g", + //drvdetail:"requires":""} + { "TESTUART", Test_UART_Init, Test_UART_RunEverySecond, Test_UART_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_TEST_COMMANDS //drvdetail:{"name":"Test", //drvdetail:"title":"TODO", //drvdetail:"descr":"Self test of the device", //drvdetail:"requires":""} - { "Test", Test_Init, NULL, Test_AppendInformationToHTTPIndexPage, Test_RunQuickTick, NULL, NULL, false }, + { "Test", Test_Init, NULL, Test_AppendInformationToHTTPIndexPage, Test_RunQuickTick, NULL, NULL, NULL, false }, #endif #if ENABLE_I2C //drvdetail:{"name":"I2C", //drvdetail:"title":"TODO", //drvdetail:"descr":"Generic I2C, not used for LED drivers, but may be useful for displays or port expanders. Supports both hardware and software I2C.", //drvdetail:"requires":""} - { "I2C", DRV_I2C_Init, DRV_I2C_EverySecond, NULL, NULL, DRV_I2C_Shutdown, NULL, false }, + { "I2C", DRV_I2C_Init, DRV_I2C_EverySecond, NULL, NULL, DRV_I2C_Shutdown, NULL, NULL, false }, #endif #if ENABLE_DRIVER_RN8209 //drvdetail:{"name":"RN8209", //drvdetail:"title":"TODO", //drvdetail:"descr":"WIP driver for power-metering chip RN8209 found in one of Zmai-90 versions.", //drvdetail:"requires":""} - { "RN8209", RN8209_Init, RN8029_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "RN8209", RN8209_Init, RN8029_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_BL0942 //drvdetail:{"name":"BL0942", //drvdetail:"title":"TODO", //drvdetail:"descr":"BL0942 is a power-metering chip which uses UART protocol for communication. It's usually connected to TX1/RX1 port of BK. You need to calibrate power metering once, just like in Tasmota. See [LSPA9 teardown example](https://www.elektroda.com/rtvforum/topic3887748.html). By default, it uses 4800 baud, but you can also enable it with baud 9600 by using 'startDriver BL0942 9600', see [related topic](https://www.elektroda.com/rtvforum/viewtopic.php?p=20957896#20957896)", //drvdetail:"requires":""} - { "BL0942", BL0942_UART_Init, BL0942_UART_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "BL0942", BL0942_UART_Init, BL0942_UART_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_PWM_GROUP //drvdetail:{"name":"PWMG", //drvdetail:"title":"TODO", //drvdetail:"descr":"PWM Groups (synchronized PWMs) driver for OpenBeken.", //drvdetail:"requires":""} - { "PWMG", PWMG_Init, NULL, NULL, NULL, NULL, NULL, false }, + { "PWMG", PWMG_Init, NULL, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_BL0942SPI //drvdetail:{"name":"BL0942SPI", //drvdetail:"title":"TODO", //drvdetail:"descr":"BL0942 driver version for SPI protocol. It's usually connected to SPI1 port of BK. You need to calibrate power metering once, just like in Tasmota. See [PZIOT-E01 teardown example](https://www.elektroda.com/rtvforum/topic3945667.html). ", //drvdetail:"requires":""} - { "BL0942SPI", BL0942_SPI_Init, BL0942_SPI_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "BL0942SPI", BL0942_SPI_Init, BL0942_SPI_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_CHARGINGLIMIT //drvdetail:{"name":"ChargingLimit", //drvdetail:"title":"TODO", //drvdetail:"descr":"Mechanism to perform an action based on a max. delta value and max time. Used to control Electric Vehicle chargers. See [discussion](https://github.com/openshwprojects/OpenBK7231T_App/issues/892).", //drvdetail:"requires":""} - { "ChargingLimit", ChargingLimit_Init, ChargingLimit_OnEverySecond, ChargingLimit_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "ChargingLimit", ChargingLimit_Init, ChargingLimit_OnEverySecond, ChargingLimit_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_BL0937 //drvdetail:{"name":"BL0937", //drvdetail:"title":"TODO", //drvdetail:"descr":"BL0937 is a power-metering chip which uses custom protocol to report data. It requires setting 3 pins in pin config: CF, CF1 and SEL", //drvdetail:"requires":""} - { "BL0937", BL0937_Init, BL0937_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "BL0937", BL0937_Init, BL0937_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_CSE7761 - { "CSE7761", CSE7761_Init, CSE7761_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "CSE7761", CSE7761_Init, CSE7761_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_CSE7766 //drvdetail:{"name":"CSE7766", //drvdetail:"title":"TODO", //drvdetail:"descr":"CSE7766 is a power-metering chip which uses UART protocol for communication. It's usually connected to TX1/RX1 port of BK", //drvdetail:"requires":""} - { "CSE7766", CSE7766_Init, CSE7766_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "CSE7766", CSE7766_Init, CSE7766_RunEverySecond, BL09XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_MAX6675 //drvdetail:{"name":"MAX6675", //drvdetail:"title":"TODO", //drvdetail:"descr":"Thermocouple driver for measuring high temperatures, see [presentation](https://www.elektroda.com/rtvforum/topic4055231.html)", //drvdetail:"requires":""} - { "MAX6675", MAX6675_Init, MAX6675_RunEverySecond, NULL, NULL, NULL, NULL, false }, + { "MAX6675", MAX6675_Init, MAX6675_RunEverySecond, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_PT6523 //drvdetail:{"name":"PT6523", //drvdetail:"title":"TODO", //drvdetail:"descr":"Car radio LCD driver, see [teardown and presentation](https://www.elektroda.com/rtvforum/topic3983111.html)", //drvdetail:"requires":""} - { "PT6523", PT6523_Init, PT6523_RunFrame, NULL, NULL, NULL, NULL, false }, + { "PT6523", PT6523_Init, PT6523_RunFrame, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_TEXTSCROLLER //drvdetail:{"name":"TextScroller", //drvdetail:"title":"TODO", //drvdetail:"descr":"Wrapper utility that can do text scrolling animation on implemented displays (WIP)", //drvdetail:"requires":""} - { "TextScroller", TS_Init, NULL, NULL, TS_RunQuickTick, NULL, NULL, false }, + { "TextScroller", TS_Init, NULL, NULL, TS_RunQuickTick, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_SM16703P //drvdetail:{"name":"SM16703P", //drvdetail:"title":"TODO", //drvdetail:"descr":"SM16703P is an individually addressable LEDs controller like WS2812B. Currently SM16703P LEDs are supported through hardware SPI, LEDs data should be connected to P16 (MOSI), [here you can read](https://www.elektroda.com/rtvforum/topic4005865.html) how to break it out on CB2S.", //drvdetail:"requires":""} - { "SM16703P", SM16703P_Init, NULL, NULL, NULL, NULL, NULL, false }, + { "SM16703P", SM16703P_Init, NULL, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_SM15155E //drvdetail:{"name":"SM15155E", //drvdetail:"title":"TODO", //drvdetail:"descr":"SM15155E is a WS2812B-like single wire LED controller. It's also always using P16 (SPI out) on Beken. See [reverse-engineering topic](https://www.elektroda.com/rtvforum/topic4060227.html)", //drvdetail:"requires":""} - { "SM15155E", SM15155E_Init, NULL, NULL, NULL, NULL, NULL, false }, + { "SM15155E", SM15155E_Init, NULL, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_IRREMOTEESP - { "IR", DRV_IR_Init, NULL, NULL, DRV_IR_RunFrame, NULL, NULL, false }, + { "IR", DRV_IR_Init, NULL, NULL, DRV_IR_RunFrame, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_IR //drvdetail:{"name":"IR", //drvdetail:"title":"TODO", //drvdetail:"descr":"IRLibrary wrapper, so you can receive remote signals and send them. See [forum discussion here](https://www.elektroda.com/rtvforum/topic3920360.html), also see [LED strip and IR YT video](https://www.youtube.com/watch?v=KU0tDwtjfjw)", //drvdetail:"requires":""} - { "IR", DRV_IR_Init, NULL, NULL, DRV_IR_RunFrame, NULL, NULL, false }, + { "IR", DRV_IR_Init, NULL, NULL, DRV_IR_RunFrame, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_IR2 - { "IR2", DRV_IR2_Init, NULL, NULL, NULL, NULL, NULL, false }, + { "IR2", DRV_IR2_Init, NULL, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_DDP @@ -262,83 +270,83 @@ static driver_t g_drivers[] = { //drvdetail:"title":"TODO", //drvdetail:"descr":"DDP is a LED control protocol that is using UDP. You can use xLights or any other app to control OBK LEDs that way. See [DDP topic](https://www.elektroda.com/rtvforum/topic4040325.html)", //drvdetail:"requires":""} - { "DDP", DRV_DDP_Init, NULL, DRV_DDP_AppendInformationToHTTPIndexPage, DRV_DDP_RunFrame, DRV_DDP_Shutdown, NULL, false }, + { "DDP", DRV_DDP_Init, NULL, DRV_DDP_AppendInformationToHTTPIndexPage, DRV_DDP_RunFrame, DRV_DDP_Shutdown, NULL, NULL, false }, #endif #if ENABLE_DRIVER_SSDP //drvdetail:{"name":"SSDP", //drvdetail:"title":"TODO", //drvdetail:"descr":"SSDP is a discovery protocol, so BK devices can show up in, for example, Windows network section", //drvdetail:"requires":""} - { "SSDP", DRV_SSDP_Init, DRV_SSDP_RunEverySecond, NULL, DRV_SSDP_RunQuickTick, DRV_SSDP_Shutdown, NULL, false }, + { "SSDP", DRV_SSDP_Init, DRV_SSDP_RunEverySecond, NULL, DRV_SSDP_RunQuickTick, DRV_SSDP_Shutdown, NULL, NULL, false }, #endif #if ENABLE_TASMOTADEVICEGROUPS //drvdetail:{"name":"DGR", //drvdetail:"title":"TODO", //drvdetail:"descr":"Tasmota Device groups driver. See [forum example](https://www.elektroda.com/rtvforum/topic3925472.html) and [video tutorial](https://www.youtube.com/watch?v=e1xcq3OUR5M&ab_channel=Elektrodacom)", //drvdetail:"requires":""} - { "DGR", DRV_DGR_Init, DRV_DGR_RunEverySecond, DRV_DGR_AppendInformationToHTTPIndexPage, DRV_DGR_RunQuickTick, DRV_DGR_Shutdown, DRV_DGR_OnChannelChanged, false }, + { "DGR", DRV_DGR_Init, DRV_DGR_RunEverySecond, DRV_DGR_AppendInformationToHTTPIndexPage, DRV_DGR_RunQuickTick, DRV_DGR_Shutdown, DRV_DGR_OnChannelChanged, NULL, false }, #endif #if ENABLE_DRIVER_WEMO //drvdetail:{"name":"Wemo", //drvdetail:"title":"TODO", //drvdetail:"descr":"Wemo emulation for Alexa. You must also start SSDP so it can run, because it depends on SSDP discovery.", //drvdetail:"requires":""} - { "Wemo", WEMO_Init, NULL, WEMO_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "Wemo", WEMO_Init, NULL, WEMO_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_HUE //drvdetail:{"name":"Hue", //drvdetail:"title":"TODO", //drvdetail:"descr":"Hue emulation for Alexa. You must also start SSDP so it can run, because it depends on SSDP discovery.", //drvdetail:"requires":""} - { "Hue", HUE_Init, NULL, HUE_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "Hue", HUE_Init, NULL, HUE_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if defined(PLATFORM_BEKEN) || defined(WINDOWS) //drvdetail:{"name":"PWMToggler", //drvdetail:"title":"TODO", //drvdetail:"descr":"PWMToggler is a custom abstraction layer that can run on top of raw PWM channels. It provides ability to turn off/on the PWM while keeping it's value, which is not possible by direct channel operations. It can be used for some custom devices with extra lights/lasers. See example [here](https://www.elektroda.com/rtvforum/topic3939064.html).", //drvdetail:"requires":""} - { "PWMToggler", DRV_InitPWMToggler, NULL, DRV_Toggler_AppendInformationToHTTPIndexPage, DRV_Toggler_QuickTick, NULL, NULL, false }, + { "PWMToggler", DRV_InitPWMToggler, NULL, DRV_Toggler_AppendInformationToHTTPIndexPage, DRV_Toggler_QuickTick, NULL, NULL, NULL, false }, //drvdetail:{"name":"DoorSensor", //drvdetail:"title":"TODO", //drvdetail:"descr":"DoorSensor is using deep sleep to preserve battery. This is used for devices without TuyaMCU, where BK deep sleep and wakeup on GPIO is used. This drives requires you to set a DoorSensor pin. Change on door sensor pin wakes up the device. If there are no changes for some time, device goes to sleep. See example [here](https://www.elektroda.com/rtvforum/topic3960149.html). If your door sensor does not wake up in certain pos, please use DSEdge command (try all 3 options, default is 2). ", //drvdetail:"requires":""} - { "DoorSensor", DoorDeepSleep_Init, DoorDeepSleep_OnEverySecond, DoorDeepSleep_AppendInformationToHTTPIndexPage, NULL, NULL, DoorDeepSleep_OnChannelChanged, false }, + { "DoorSensor", DoorDeepSleep_Init, DoorDeepSleep_OnEverySecond, DoorDeepSleep_AppendInformationToHTTPIndexPage, NULL, NULL, DoorDeepSleep_OnChannelChanged, NULL, false }, #endif #if ENABLE_DRIVER_ADCBUTTON //drvdetail:{"name":"ADCButton", //drvdetail:"title":"TODO", //drvdetail:"descr":"This allows you to connect multiple buttons on single ADC pin. Each button must have a different resistor value, this works by probing the voltage on ADC from a resistor divider. You need to select AB_Map first. See forum post for [details](https://www.elektroda.com/rtvforum/viewtopic.php?p=20541973#20541973).", //drvdetail:"requires":""} - { "ADCButton", DRV_ADCButton_Init, NULL, NULL, DRV_ADCButton_RunFrame, NULL, NULL, false }, + { "ADCButton", DRV_ADCButton_Init, NULL, NULL, DRV_ADCButton_RunFrame, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_MAX72XX //drvdetail:{"name":"MAX72XX_Clock", //drvdetail:"title":"TODO", //drvdetail:"descr":"Simple hardcoded driver for MAX72XX clock. Requires manual start of MAX72XX driver with MAX72XX setup and NTP start.", //drvdetail:"requires":""} - { "MAX72XX_Clock", DRV_MAX72XX_Clock_Init, DRV_MAX72XX_Clock_OnEverySecond, NULL, DRV_MAX72XX_Clock_RunFrame, NULL, NULL, false }, + { "MAX72XX_Clock", DRV_MAX72XX_Clock_Init, DRV_MAX72XX_Clock_OnEverySecond, NULL, DRV_MAX72XX_Clock_RunFrame, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_LED //drvdetail:{"name":"SM2135", //drvdetail:"title":"TODO", //drvdetail:"descr":"SM2135 custom-'I2C' LED driver for RGBCW lights. This will start automatically if you set both SM2135 pin roles. This may need you to remap the RGBCW indexes with SM2135_Map command", //drvdetail:"requires":""} - { "SM2135", SM2135_Init, NULL, NULL, NULL, NULL, NULL, false }, + { "SM2135", SM2135_Init, NULL, NULL, NULL, NULL, NULL, NULL, false }, //drvdetail:{"name":"BP5758D", //drvdetail:"title":"TODO", //drvdetail:"descr":"BP5758D custom-'I2C' LED driver for RGBCW lights. This will start automatically if you set both BP5758D pin roles. This may need you to remap the RGBCW indexes with BP5758D_Map command. This driver is used in some of BL602/Sonoff bulbs, see [video flashing tutorial here](https://www.youtube.com/watch?v=L6d42IMGhHw)", //drvdetail:"requires":""} - { "BP5758D", BP5758D_Init, NULL, NULL, NULL, NULL, NULL, false }, + { "BP5758D", BP5758D_Init, NULL, NULL, NULL, NULL, NULL, NULL, false }, //drvdetail:{"name":"BP1658CJ", //drvdetail:"title":"TODO", //drvdetail:"descr":"BP1658CJ custom-'I2C' LED driver for RGBCW lights. This will start automatically if you set both BP1658CJ pin roles. This may need you to remap the RGBCW indexes with BP1658CJ_Map command", //drvdetail:"requires":""} - { "BP1658CJ", BP1658CJ_Init, NULL, NULL, NULL, NULL, NULL, false }, + { "BP1658CJ", BP1658CJ_Init, NULL, NULL, NULL, NULL, NULL, NULL, false }, //drvdetail:{"name":"SM2235", //drvdetail:"title":"TODO", //drvdetail:"descr":"SM2335 andd SM2235 custom-'I2C' LED driver for RGBCW lights. This will start automatically if you set both SM2235 pin roles. This may need you to remap the RGBCW indexes with SM2235_Map command. This driver also works for SM2185N.", //drvdetail:"requires":""} - { "SM2235", SM2235_Init, NULL, NULL, NULL, NULL, NULL, false }, + { "SM2235", SM2235_Init, NULL, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_BMP280 @@ -346,70 +354,70 @@ static driver_t g_drivers[] = { //drvdetail:"title":"TODO", //drvdetail:"descr":"BMP280 is a Temperature and Pressure sensor with I2C interface.", //drvdetail:"requires":""} - { "BMP280", BMP280_Init, BMP280_OnEverySecond, BMP280_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "BMP280", BMP280_Init, BMP280_OnEverySecond, BMP280_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_MAX72XX //drvdetail:{"name":"MAX72XX", //drvdetail:"title":"TODO", //drvdetail:"descr":"MAX72XX LED matrix display driver with font and simple script interface. See [protocol explanation](https://www.elektroda.pl/rtvforum/viewtopic.php?p=18040628#18040628)", //drvdetail:"requires":""} - { "MAX72XX", DRV_MAX72XX_Init, NULL, NULL, NULL, NULL, NULL, false }, + { "MAX72XX", DRV_MAX72XX_Init, NULL, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_BMPI2C //drvdetail:{"name":"BMPI2C", //drvdetail:"title":"TODO", //drvdetail:"descr":"Driver for BMP085, BMP180, BMP280, BME280, BME68X sensors with I2C interface.", //drvdetail:"requires":""} - { "BMPI2C", BMPI2C_Init, BMPI2C_OnEverySecond, BMPI2C_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "BMPI2C", BMPI2C_Init, BMPI2C_OnEverySecond, BMPI2C_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_CHT83XX //drvdetail:{"name":"CHT83XX", //drvdetail:"title":"TODO", //drvdetail:"descr":"CHT8305, CHT8310 and CHT8315 are a Temperature and Humidity sensors with I2C interface.", //drvdetail:"requires":""} - { "CHT83XX", CHT83XX_Init, CHT83XX_OnEverySecond, CHT83XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "CHT83XX", CHT83XX_Init, CHT83XX_OnEverySecond, CHT83XX_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_MCP9808 //drvdetail:{"name":"MCP9808", //drvdetail:"title":"TODO", //drvdetail:"descr":"MCP9808 is a Temperature sensor with I2C interface and an external wakeup pin, see [docs](https://www.elektroda.pl/rtvforum/topic3988466.html).", //drvdetail:"requires":""} - { "MCP9808", MCP9808_Init, MCP9808_OnEverySecond, MCP9808_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, false }, + { "MCP9808", MCP9808_Init, MCP9808_OnEverySecond, MCP9808_AppendInformationToHTTPIndexPage, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_KP18058 //drvdetail:{"name":"KP18058", //drvdetail:"title":"TODO", //drvdetail:"descr":"KP18058 I2C LED driver. Supports also KP18068. Working, see reverse-engineering [topic](https://www.elektroda.pl/rtvforum/topic3991620.html)", //drvdetail:"requires":""} - { "KP18058", KP18058_Init, NULL, NULL, NULL, NULL, NULL, false }, + { "KP18058", KP18058_Init, NULL, NULL, NULL, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_ADCSMOOTHER //drvdetail:{"name":"ADCSmoother", //drvdetail:"title":"TODO", //drvdetail:"descr":"ADCSmoother is used for 3-way stairs switches synchronized via extra wire.", //drvdetail:"requires":""} - { "ADCSmoother", DRV_ADCSmoother_Init, NULL, NULL, DRV_ADCSmoother_RunFrame, NULL, NULL, false }, + { "ADCSmoother", DRV_ADCSmoother_Init, NULL, NULL, DRV_ADCSmoother_RunFrame, NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_SHT3X //drvdetail:{"name":"SHT3X", //drvdetail:"title":"TODO", //drvdetail:"descr":"Humidity/temperature sensor. See [SHT Sensor tutorial topic here](https://www.elektroda.com/rtvforum/topic3958369.html), also see [this sensor teardown](https://www.elektroda.com/rtvforum/topic3945688.html)", //drvdetail:"requires":""} - { "SHT3X", SHT3X_Init, SHT3X_OnEverySecond, SHT3X_AppendInformationToHTTPIndexPage, NULL, SHT3X_StopDriver, NULL, false }, + { "SHT3X", SHT3X_Init, SHT3X_OnEverySecond, SHT3X_AppendInformationToHTTPIndexPage, NULL, SHT3X_StopDriver, NULL, NULL, false }, #endif #if ENABLE_DRIVER_SGP //drvdetail:{"name":"SGP", //drvdetail:"title":"TODO", //drvdetail:"descr":"SGP Air Quality sensor with I2C interface. See [this DIY sensor](https://www.elektroda.com/rtvforum/topic3967174.html) for setup information.", //drvdetail:"requires":""} - { "SGP", SGP_Init, SGP_OnEverySecond, SGP_AppendInformationToHTTPIndexPage, NULL, SGP_StopDriver, NULL, false }, + { "SGP", SGP_Init, SGP_OnEverySecond, SGP_AppendInformationToHTTPIndexPage, NULL, SGP_StopDriver, NULL, NULL, false }, #endif #if ENABLE_DRIVER_SHIFTREGISTER //drvdetail:{"name":"ShiftRegister", //drvdetail:"title":"TODO", //drvdetail:"descr":"Simple Shift Register driver that allows you to map channels to shift register output. See [related topic](https://www.elektroda.com/rtvforum/viewtopic.php?p=20533505#20533505)", //drvdetail:"requires":""} - { "ShiftRegister", Shift_Init, Shift_OnEverySecond, NULL, NULL, NULL, Shift_OnChannelChanged, false }, + { "ShiftRegister", Shift_Init, Shift_OnEverySecond, NULL, NULL, NULL, Shift_OnChannelChanged, NULL, false }, #endif #if ENABLE_DRIVER_AHT2X //drvdetail:{"name":"AHT2X", @@ -430,7 +438,7 @@ static driver_t g_drivers[] = { //drvdetail:"title":"TODO", //drvdetail:"descr":"Driver for 16-segment LED display with I2C. See [protocol explanation](https://www.elektroda.pl/rtvforum/topic3984616.html)", //drvdetail:"requires":""} - { "HT16K33", HT16K33_Init, NULL, NULL, NULL,NULL, NULL, false }, + { "HT16K33", HT16K33_Init, NULL, NULL, NULL,NULL, NULL, NULL, false }, #endif // Shared driver for TM1637, GN6932, TM1638 - TM_GN_Display_SharedInit #if ENABLE_DRIVER_TMGN @@ -438,43 +446,43 @@ static driver_t g_drivers[] = { //drvdetail:"title":"TODO", //drvdetail:"descr":"Driver for 7-segment LED display with DIO/CLK interface. See [TM1637 information](https://www.elektroda.com/rtvforum/viewtopic.php?p=20468593#20468593)", //drvdetail:"requires":""} - { "TM1637", TM1637_Init, NULL, NULL, TMGN_RunQuickTick,NULL, NULL, false }, + { "TM1637", TM1637_Init, NULL, NULL, TMGN_RunQuickTick,NULL, NULL, NULL, false }, //drvdetail:{"name":"GN6932", //drvdetail:"title":"TODO", //drvdetail:"descr":"Driver for 7-segment LED display with DIO/CLK/STB interface. See [this topic](https://www.elektroda.com/rtvforum/topic3971252.html) for details.", //drvdetail:"requires":""} - { "GN6932", GN6932_Init, NULL, NULL, TMGN_RunQuickTick, NULL, NULL, false }, + { "GN6932", GN6932_Init, NULL, NULL, TMGN_RunQuickTick, NULL, NULL, NULL, false }, //drvdetail:{"name":"TM1638", //drvdetail:"title":"TODO", //drvdetail:"descr":"Driver for 7-segment LED display with DIO/CLK/STB interface. TM1638 is very similiar to GN6932 and TM1637. See [this topic](https://www.elektroda.com/rtvforum/viewtopic.php?p=20553628#20553628) for details.", //drvdetail:"requires":""} - { "TM1638", TM1638_Init, NULL, NULL, TMGN_RunQuickTick,NULL, NULL, false }, + { "TM1638", TM1638_Init, NULL, NULL, TMGN_RunQuickTick,NULL, NULL, NULL, false }, //drvdetail:{"name":"HD2015", //drvdetail:"title":"TODO", //drvdetail:"descr":"Driver for 7-segment LED display with I2C-like interface. Seems to be compatible with TM1650. HD2015 is very similiar to GN6932 and TM1637. See [this topic](https://www.elektroda.com/rtvforum/topic4052946.html) for details.", //drvdetail:"requires":""} - { "HD2015", HD2015_Init, NULL, NULL, TMGN_RunQuickTick,NULL, NULL, false }, + { "HD2015", HD2015_Init, NULL, NULL, TMGN_RunQuickTick,NULL, NULL, NULL, false }, #endif #if ENABLE_DRIVER_BATTERY //drvdetail:{"name":"Battery", //drvdetail:"title":"TODO", //drvdetail:"descr":"Custom mechanism to measure battery level with ADC and an optional relay. See [example here](https://www.elektroda.com/rtvforum/topic3959103.html).", //drvdetail:"requires":""} - { "Battery", Batt_Init, Batt_OnEverySecond, Batt_AppendInformationToHTTPIndexPage, NULL, Batt_StopDriver, NULL, false }, + { "Battery", Batt_Init, Batt_OnEverySecond, Batt_AppendInformationToHTTPIndexPage, NULL, Batt_StopDriver, NULL, NULL, false }, #endif #if ENABLE_DRIVER_BRIDGE //drvdetail:{"name":"Bridge", //drvdetail:"title":"TODO", //drvdetail:"descr":"A bridge relay driver, added for [TONGOU TO-Q-SY1-JWT Din Rail Switch](https://www.elektroda.com/rtvforum/topic3934580.html). See linked topic for info.", //drvdetail:"requires":""} - { "Bridge", Bridge_driver_Init, NULL, NULL, Bridge_driver_QuickFrame, Bridge_driver_DeInit, Bridge_driver_OnChannelChanged, false } + { "Bridge", Bridge_driver_Init, NULL, NULL, Bridge_driver_QuickFrame, Bridge_driver_DeInit, Bridge_driver_OnChannelChanged, NULL, false } #endif #if ENABLE_DRIVER_UART_TCP //drvdetail:{"name":"UART to TCP bridge", //drvdetail:"title":"TODO", //drvdetail:"descr":"UART to TCP, mainly for WiFi Zigbee coordinators.", //drvdetail:"requires":""} - { "UartTCP", UART_TCP_Init, NULL, NULL, NULL, UART_TCP_Deinit, NULL, false } + { "UartTCP", UART_TCP_Init, NULL, NULL, NULL, UART_TCP_Deinit, NULL, NULL, false } #endif }; @@ -672,6 +680,23 @@ void DRV_Generic_Init() { //cmddetail:"examples":""} CMD_RegisterCommand("stopDriver", DRV_Stop, NULL); } + +void DRV_OnHassDiscovery(const char *topic) { + int i; + + if (DRV_Mutex_Take(100) == false) { + return; + } + for (i = 0; i < g_numDrivers; i++) { + if (g_drivers[i].bLoaded) { + if (g_drivers[i].onHassDiscovery) { + g_drivers[i].onHassDiscovery(topic); + } + } + } + DRV_Mutex_Free(); + +} void DRV_AppendInformationToHTTPIndexPage(http_request_t* request, int bPreState) { int i, j; int c_active = 0; diff --git a/src/driver/drv_public.h b/src/driver/drv_public.h index ff620fd42..dc6057afe 100644 --- a/src/driver/drv_public.h +++ b/src/driver/drv_public.h @@ -49,6 +49,7 @@ typedef struct energySensorNames_s { extern int g_dhtsCount; void DRV_Generic_Init(); +void DRV_OnHassDiscovery(const char *topic); void DRV_AppendInformationToHTTPIndexPage(http_request_t* request, int bPreState); void DRV_OnEverySecond(); void DHT_OnEverySecond(); diff --git a/src/driver/drv_tclAC.c b/src/driver/drv_tclAC.c index 223623436..d958961fd 100644 --- a/src/driver/drv_tclAC.c +++ b/src/driver/drv_tclAC.c @@ -7,6 +7,7 @@ #include "../new_cfg.h" #include "../new_pins.h" #include "../cmnds/cmd_public.h" +#include "../mqtt/new_mqtt.h" #include "../httpserver/new_http.h" #include "drv_uart.h" @@ -17,62 +18,12 @@ #include "drv_tclAC.h" -//static int TCL_UART_TryToGetNextPacket() { -// int cs; -// int i; -// int c_garbage_consumed = 0; -// byte checksum; -// -// cs = UART_GetDataSize(); -// -// if(cs < TCL_UART_PACKET_LEN) { -// return 0; -// } -// // skip garbage data (should not happen) -// while(cs > 0) { -// //if (UART_GetByte(0) != TCL_UART_PACKET_HEAD) -// if(1) -// { -// UART_ConsumeBytes(1); -// c_garbage_consumed++; -// cs--; -// } else { -// break; -// } -// } -// if(c_garbage_consumed > 0){ -// ADDLOG_WARN(LOG_FEATURE_ENERGYMETER, -// "Consumed %i unwanted non-header byte in TCL buffer\n", -// c_garbage_consumed); -// } -// if(cs < TCL_UART_PACKET_LEN) { -// return 0; -// } -// //if (UART_GetByte(0) != 0x55) -// // return 0; -// -// -// //if (checksum != UART_GetByte(TCL_UART_PACKET_LEN - 1)) { -// // ADDLOG_WARN(LOG_FEATURE_ENERGYMETER, -// // "Skipping packet with bad checksum %02X wanted %02X\n", -// // UART_GetByte(TCL_UART_PACKET_LEN - 1), checksum); -// // UART_ConsumeBytes(TCL_UART_PACKET_LEN); -// // return 1; -// //} -// -// -// -// UART_ConsumeBytes(TCL_UART_PACKET_LEN); -// return TCL_UART_PACKET_LEN; -//} - - - - uint8_t set_cmd_base[35] = { 0xBB, 0x00, 0x01, 0x03, 0x1D, 0x00, 0x00, 0x64, 0x03, 0xF3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; bool ready_to_send_set_cmd_flag = false; set_cmd_t m_set_cmd = { 0 }; get_cmd_resp_t m_get_cmd_resp = { 0 }; +int g_buzzer = 1; +int g_disp = 1; typedef enum { CLIMATE_MODE_OFF, @@ -92,8 +43,8 @@ void build_set_cmd(get_cmd_resp_t * get_cmd_resp) { m_set_cmd.data.power = get_cmd_resp->data.power; m_set_cmd.data.off_timer_en = 0; m_set_cmd.data.on_timer_en = 0; - m_set_cmd.data.beep = 1; - m_set_cmd.data.disp = 1; + m_set_cmd.data.beep = g_buzzer; + m_set_cmd.data.disp = g_disp; m_set_cmd.data.eco = 0; switch (get_cmd_resp->data.mode) { @@ -182,6 +133,107 @@ typedef enum { } fanMode_e; +static const struct { + const char *name; + fanMode_e mode; +} fanModeMap[] = { + {"off", FAN_OFF}, + {"1", FAN_1}, + {"2", FAN_2}, + {"3", FAN_3}, + {"4", FAN_4}, + {"5", FAN_5}, + {"mute", FAN_MUTE}, + {"turbo", FAN_TURBO}, + {"auto", FAN_AUTOMATIC}, +}; + +fanMode_e parseFanMode(const char *s) { + for (int i = 0; i < sizeof(fanModeMap) / sizeof(fanModeMap[0]); ++i) { + if (!stricmp(s, fanModeMap[i].name)) { + return fanModeMap[i].mode; + } + } + return (fanMode_e)atoi(s); +} + +const char *fanModeToStr(fanMode_e mode) { + for (int i = 0; i < sizeof(fanModeMap) / sizeof(fanModeMap[0]); ++i) { + if (fanModeMap[i].mode == mode) { + return fanModeMap[i].name; + } + } + return NULL; +} +const char *fanOptions[] = { "auto", "low", "medium", "high" }; +typedef enum { + VS_NONE, + VS_MoveFull, + VS_MoveUpper, + VS_MoveLower, + VS_FixTop, + VS_FixUpper, + VS_FixMid, + VS_FixLower, + VS_FixBottom +} VerticalSwingMode; +typedef enum { + HS_NONE, + HS_MOVE_FULL, + HS_MOVE_LEFT, + HS_MOVE_MID, + HS_MOVE_RIGHT, + HS_FIX_LEFT, + HS_FIX_MID_LEFT, + HS_FIX_MID, + HS_FIX_MID_RIGHT, + HS_FIX_RIGHT +} HorizontalSwing; +const char* vertical_swing_options[] = { + "none", + "move_full", + "move_upper", + "move_lower", + "fix_top", + "fix_upper", + "fix_mid", + "fix_lower", + "fix_bottom" +}; + +const char* horizontal_swing_options[] = { + "none", + "move_full", + "move_left", + "move_mid", + "move_right", + "fix_left", + "fix_mid_left", + "fix_mid", + "fix_mid_right", + "fix_right" +}; +const char *getSwingVLabel(VerticalSwingMode m) { + return vertical_swing_options[m]; +} +const char *getSwingHLabel(HorizontalSwing m) { + return horizontal_swing_options[m]; +} +VerticalSwingMode parse_vertical_swing(const char *s) { + for (int i = 0; i < sizeof(vertical_swing_options) / sizeof(vertical_swing_options[0]); ++i) { + if (stricmp(s, vertical_swing_options[i]) == 0) + return (VerticalSwingMode)i; + } + return atoi(s); +} + +HorizontalSwing parse_horizontal_swing(const char *s) { + for (int i = 0; i < sizeof(horizontal_swing_options) / sizeof(horizontal_swing_options[0]); ++i) { + if (stricmp(s, horizontal_swing_options[i]) == 0) + return (HorizontalSwing)i; + } + return atoi(s); +} void OBK_SetTargetTemperature(float temp) { // User requested target temperature change @@ -219,6 +271,26 @@ void OBK_SetFanMode(fanMode_e fan_mode) { ready_to_send_set_cmd_flag = true; } +void OBK_SetBuzzer(int buzzer) { + + get_cmd_resp_t get_cmd_resp = { 0 }; + memcpy(get_cmd_resp.raw, m_get_cmd_resp.raw, sizeof(get_cmd_resp.raw)); + + g_buzzer = buzzer; + + build_set_cmd(&get_cmd_resp); + ready_to_send_set_cmd_flag = true; +} +void OBK_SetDisplay(int display) { + + get_cmd_resp_t get_cmd_resp = { 0 }; + memcpy(get_cmd_resp.raw, m_get_cmd_resp.raw, sizeof(get_cmd_resp.raw)); + + g_disp = display; + + build_set_cmd(&get_cmd_resp); + ready_to_send_set_cmd_flag = true; +} void OBK_SetClimate(climateMode_e climate_mode) { get_cmd_resp_t get_cmd_resp = { 0 }; @@ -263,6 +335,38 @@ void write_array(byte *b, int s) { } } + +static const struct { + const char *name; + climateMode_e mode; +} climateModeMap[] = { + {"off", CLIMATE_MODE_OFF}, + {"cool", CLIMATE_MODE_COOL}, + {"dry", CLIMATE_MODE_DRY}, + {"fan", CLIMATE_MODE_FAN_ONLY}, + {"heat", CLIMATE_MODE_HEAT}, + {"heatcool", CLIMATE_MODE_HEAT_COOL}, + {"auto", CLIMATE_MODE_AUTO} +}; + +climateMode_e parseClimate(const char *s) { + for (int i = 0; i < sizeof(climateModeMap) / sizeof(climateModeMap[0]); ++i) { + if (!stricmp(s, climateModeMap[i].name)) { + return climateModeMap[i].mode; + } + } + return (climateMode_e)atoi(s); +} + +const char *climateModeToStr(climateMode_e mode) { + for (int i = 0; i < sizeof(climateModeMap) / sizeof(climateModeMap[0]); ++i) { + if (climateModeMap[i].mode == mode) { + return climateModeMap[i].name; + } + } + return NULL; +} + int read_data_line(int readch, uint8_t *buffer, int len) { static int pos = 0; @@ -303,29 +407,6 @@ bool is_valid_xor(uint8_t *buffer, int len) } } -typedef enum { - VS_NONE, - VS_MoveFull, - VS_MoveUpper, - VS_MoveLower, - VS_FixTop, - VS_FixUpper, - VS_FixMid, - VS_FixLower, - VS_FixBottom -} VerticalSwingMode; -typedef enum { - HS_NONE, - HS_MOVE_FULL, - HS_MOVE_LEFT, - HS_MOVE_MID, - HS_MOVE_RIGHT, - HS_FIX_LEFT, - HS_FIX_MID_LEFT, - HS_FIX_MID, - HS_FIX_MID_RIGHT, - HS_FIX_RIGHT -} HorizontalSwing; void control_vertical_swing(VerticalSwingMode swing_mode) { get_cmd_resp_t get_cmd_resp = { 0 }; memcpy(get_cmd_resp.raw, m_get_cmd_resp.raw, sizeof(get_cmd_resp.raw)); @@ -398,9 +479,37 @@ void set_target_temperature(float newTemp) { is_changed = true; target_temperature = newTemp; } - +fanMode_e g_fanMode; +climateMode_e g_mode = CLIMATE_MODE_OFF; +void set_mode(climateMode_e mode) { + if (g_mode == mode) + return; + is_changed = true; + g_mode = mode; +} +VerticalSwingMode g_swingV; +HorizontalSwing g_swingH; +void set_swingV(VerticalSwingMode mode) { + if (g_swingV == mode) + return; + is_changed = true; + g_swingV = mode; +} +void set_swingH(HorizontalSwing mode) { + if (g_swingH == mode) + return; + is_changed = true; + g_swingH = mode; +} +void set_custom_fan_mode(fanMode_e mode) { + if (g_fanMode == mode) + return; + is_changed = true; + g_fanMode = mode; +} void set_current_temperature(float newTemp) { - if (current_temperature == newTemp) return; + if (current_temperature == newTemp) + return; is_changed = true; current_temperature = newTemp; } @@ -425,14 +534,14 @@ void TCL_UART_TryToGetNextPacket() { ADDLOG_WARN(LOG_FEATURE_ENERGYMETER, "Ok we got reply with mode %i, fan %i, turbo %i, mute %i", (int)m_get_cmd_resp.data.power, (int)m_get_cmd_resp.data.fan, (int)m_get_cmd_resp.data.turbo, (int)m_get_cmd_resp.data.mute); - /*if (m_get_cmd_resp.data.power == 0x00) set_mode(CLIMATE_MODE_OFF); + + if (m_get_cmd_resp.data.power == 0x00) set_mode(CLIMATE_MODE_OFF); else if (m_get_cmd_resp.data.mode == 0x01) set_mode(CLIMATE_MODE_COOL); else if (m_get_cmd_resp.data.mode == 0x03) set_mode(CLIMATE_MODE_DRY); else if (m_get_cmd_resp.data.mode == 0x02) set_mode(CLIMATE_MODE_FAN_ONLY); else if (m_get_cmd_resp.data.mode == 0x04) set_mode(CLIMATE_MODE_HEAT); else if (m_get_cmd_resp.data.mode == 0x05) set_mode(CLIMATE_MODE_AUTO); - if (m_get_cmd_resp.data.turbo) set_custom_fan_mode((FAN_TURBO)); else if (m_get_cmd_resp.data.mute) set_custom_fan_mode((FAN_MUTE)); else if (m_get_cmd_resp.data.fan == 0x00) set_custom_fan_mode((FAN_AUTOMATIC)); @@ -443,31 +552,35 @@ void TCL_UART_TryToGetNextPacket() { else if (m_get_cmd_resp.data.fan == 0x03) set_custom_fan_mode((FAN_5)); - if (m_get_cmd_resp.data.hswing && m_get_cmd_resp.data.vswing) set_swing_mode(CLIMATE_SWING_BOTH); + /* if (m_get_cmd_resp.data.hswing && m_get_cmd_resp.data.vswing) set_swing_mode(CLIMATE_SWING_BOTH); else if (!m_get_cmd_resp.data.hswing && !m_get_cmd_resp.data.vswing) set_swing_mode(CLIMATE_SWING_OFF); else if (m_get_cmd_resp.data.vswing) set_swing_mode(CLIMATE_SWING_VERTICAL); - else if (m_get_cmd_resp.data.hswing) set_swing_mode(CLIMATE_SWING_HORIZONTAL); + else if (m_get_cmd_resp.data.hswing) set_swing_mode(CLIMATE_SWING_HORIZONTAL);*/ - if (m_get_cmd_resp.data.vswing_mv == 0x01) set_vswing_pos("Move full"); - else if (m_get_cmd_resp.data.vswing_mv == 0x02) set_vswing_pos("Move upper"); - else if (m_get_cmd_resp.data.vswing_mv == 0x03) set_vswing_pos("Move lower"); - else if (m_get_cmd_resp.data.vswing_fix == 0x01) set_vswing_pos("Fix top"); - else if (m_get_cmd_resp.data.vswing_fix == 0x02) set_vswing_pos("Fix upper"); - else if (m_get_cmd_resp.data.vswing_fix == 0x03) set_vswing_pos("Fix mid"); - else if (m_get_cmd_resp.data.vswing_fix == 0x04) set_vswing_pos("Fix lower"); - else if (m_get_cmd_resp.data.vswing_fix == 0x05) set_vswing_pos("Fix bottom"); - else set_vswing_pos("Last position"); + if (m_get_cmd_resp.data.vswing_mv == 0x01) set_swingV(VS_MoveFull); + else if (m_get_cmd_resp.data.vswing_mv == 0x02) set_swingV(VS_MoveUpper); + else if (m_get_cmd_resp.data.vswing_mv == 0x03) set_swingV(VS_MoveLower); + else if (m_get_cmd_resp.data.vswing_fix == 0x01) set_swingV(VS_FixTop); + else if (m_get_cmd_resp.data.vswing_fix == 0x02) set_swingV(VS_FixUpper); + else if (m_get_cmd_resp.data.vswing_fix == 0x03) set_swingV(VS_FixMid); + else if (m_get_cmd_resp.data.vswing_fix == 0x04) set_swingV(VS_FixLower); + else if (m_get_cmd_resp.data.vswing_fix == 0x05) set_swingV(VS_FixBottom); + else { + //set_swingV("Last position"); + } - if (m_get_cmd_resp.data.hswing_mv == 0x01) set_hswing_pos("Move full"); - else if (m_get_cmd_resp.data.hswing_mv == 0x02) set_hswing_pos("Move left"); - else if (m_get_cmd_resp.data.hswing_mv == 0x03) set_hswing_pos("Move mid"); - else if (m_get_cmd_resp.data.hswing_mv == 0x04) set_hswing_pos("Move right"); - else if (m_get_cmd_resp.data.hswing_fix == 0x01) set_hswing_pos("Fix left"); - else if (m_get_cmd_resp.data.hswing_fix == 0x02) set_hswing_pos("Fix mid left"); - else if (m_get_cmd_resp.data.hswing_fix == 0x03) set_hswing_pos("Fix mid"); - else if (m_get_cmd_resp.data.hswing_fix == 0x04) set_hswing_pos("Fix mid right"); - else if (m_get_cmd_resp.data.hswing_fix == 0x05) set_hswing_pos("Fix right"); - else set_hswing_pos("Last position");*/ + if (m_get_cmd_resp.data.hswing_mv == 0x01) set_swingH(HS_MOVE_FULL); + else if (m_get_cmd_resp.data.hswing_mv == 0x02) set_swingH(HS_MOVE_LEFT); + else if (m_get_cmd_resp.data.hswing_mv == 0x03) set_swingH(HS_MOVE_MID); + else if (m_get_cmd_resp.data.hswing_mv == 0x04) set_swingH(HS_MOVE_RIGHT); + else if (m_get_cmd_resp.data.hswing_fix == 0x01) set_swingH(HS_FIX_LEFT); + else if (m_get_cmd_resp.data.hswing_fix == 0x02) set_swingH(HS_FIX_MID_LEFT); + else if (m_get_cmd_resp.data.hswing_fix == 0x03) set_swingH(HS_FIX_MID); + else if (m_get_cmd_resp.data.hswing_fix == 0x04) set_swingH(HS_FIX_MID_RIGHT); + else if (m_get_cmd_resp.data.hswing_fix == 0x05) set_swingH(HS_FIX_RIGHT); + else { + //set_swingH("Last position"); + } ADDLOG_WARN(LOG_FEATURE_ENERGYMETER, "fan %02X", m_get_cmd_resp.data.fan); ADDLOG_WARN(LOG_FEATURE_ENERGYMETER, "mode %02X", m_get_cmd_resp.data.mode); @@ -482,26 +595,13 @@ void TCL_UART_TryToGetNextPacket() { } } } -void TCL_UART_RunEverySecond(void) { - uint8_t req_cmd[] = { 0xBB, 0x00, 0x01, 0x04, 0x02, 0x01, 0x00, 0xBD }; - - if (ready_to_send_set_cmd_flag) { - ADDLOG_WARN(LOG_FEATURE_ENERGYMETER, "Sending data"); - ready_to_send_set_cmd_flag = false; - write_array(m_set_cmd.raw, sizeof(m_set_cmd.raw)); - } - else { - write_array(req_cmd, sizeof(req_cmd)); - } - TCL_UART_TryToGetNextPacket(); -} static commandResult_t CMD_ACMode(const void* context, const char* cmd, const char* args, int cmdFlags) { int mode; Tokenizer_TokenizeString(args, 0); - mode = Tokenizer_GetArgInteger(0); + mode = parseClimate(Tokenizer_GetArg(0)); OBK_SetClimate(mode); return CMD_RES_OK; } @@ -510,7 +610,7 @@ static commandResult_t CMD_FANMode(const void* context, const char* cmd, const c Tokenizer_TokenizeString(args, 0); - mode = Tokenizer_GetArgInteger(0); + mode = parseFanMode(Tokenizer_GetArg(0)); OBK_SetFanMode(mode); return CMD_RES_OK; } @@ -520,7 +620,9 @@ void TCL_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState } else { - + hprintf255(request, "

SwingH: %s

", climateModeToStr(g_mode)); + hprintf255(request, "

SwingV: %s

", climateModeToStr(g_mode)); + hprintf255(request, "

Mode: %s

", climateModeToStr(g_mode)); hprintf255(request, "

Current temperature: %f

", current_temperature); hprintf255(request, "

Target temperature: %f

", target_temperature); } @@ -531,7 +633,7 @@ static commandResult_t CMD_SwingH(const void* context, const char* cmd, const ch Tokenizer_TokenizeString(args, 0); - mode = Tokenizer_GetArgInteger(0); + mode = parse_horizontal_swing(Tokenizer_GetArg(0)); control_horizontal_swing(mode); return CMD_RES_OK; } @@ -549,10 +651,28 @@ static commandResult_t CMD_SwingV(const void* context, const char* cmd, const ch Tokenizer_TokenizeString(args, 0); - mode = Tokenizer_GetArgInteger(0); + mode = parse_vertical_swing(Tokenizer_GetArg(0)); control_vertical_swing(mode); return CMD_RES_OK; } +static commandResult_t CMD_Display(const void* context, const char* cmd, const char* args, int cmdFlags) { + int display; + + Tokenizer_TokenizeString(args, 0); + + display = Tokenizer_GetArgInteger(0); + OBK_SetDisplay(display); + return CMD_RES_OK; +} +static commandResult_t CMD_Buzzer(const void* context, const char* cmd, const char* args, int cmdFlags) { + int buzzer; + + Tokenizer_TokenizeString(args, 0); + + buzzer = Tokenizer_GetArgInteger(0); + OBK_SetBuzzer(buzzer); + return CMD_RES_OK; +} void TCL_Init(void) { UART_InitUART(TCL_baudRate, 2, false); @@ -563,4 +683,80 @@ void TCL_Init(void) { CMD_RegisterCommand("SwingH", CMD_SwingH, NULL); CMD_RegisterCommand("SwingV", CMD_SwingV, NULL); CMD_RegisterCommand("TargetTemperature", CMD_TargetTemperature, NULL); + CMD_RegisterCommand("Buzzer", CMD_Buzzer, NULL); + CMD_RegisterCommand("Display", CMD_Display, NULL); } + +void TCL_UART_RunEverySecond(void) { + uint8_t req_cmd[] = { 0xBB, 0x00, 0x01, 0x04, 0x02, 0x01, 0x00, 0xBD }; + + MQTT_PublishMain_StringInt("CurrentTemperature", (int)current_temperature, 0); + MQTT_PublishMain_StringInt("TargetTemperature", (int)target_temperature, 0); + MQTT_PublishMain_StringString("ACMode", climateModeToStr(g_mode), 0); + MQTT_PublishMain_StringString("FANMode", fanModeToStr(g_fanMode), 0); + MQTT_PublishMain_StringInt("Buzzer", g_buzzer, 0); + MQTT_PublishMain_StringInt("Display", g_disp, 0); + MQTT_PublishMain_StringString("SwingH", getSwingHLabel(g_swingH), 0); + MQTT_PublishMain_StringString("SwingV", getSwingVLabel(g_swingV), 0); + + if (ready_to_send_set_cmd_flag) { + ADDLOG_WARN(LOG_FEATURE_ENERGYMETER, "Sending data"); + ready_to_send_set_cmd_flag = false; + write_array(m_set_cmd.raw, sizeof(m_set_cmd.raw)); + } + else { + write_array(req_cmd, sizeof(req_cmd)); + } + TCL_UART_TryToGetNextPacket(); +} +#include "../httpserver/hass.h" +// backlog startDriver TCL; scheduleHADiscovery +void TCL_DoDiscovery(const char *topic) { + HassDeviceInfo* dev_info = NULL; + + + dev_info = hass_createHVAC(15,30,0.5f, fanOptions, 4); + MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN); + hass_free_device_info(dev_info); + + //dev_info = hass_createFanWithModes("Fan Speed", "~/FANMode/get", "FANMode", fanOptions, 4); + //MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN); + //hass_free_device_info(dev_info); + + dev_info = hass_createToggle("Buzzer","~/Buzzer/get","Buzzer"); + MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN); + hass_free_device_info(dev_info); + + dev_info = hass_createToggle("Display", "~/Display/get", "Display"); + MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN); + hass_free_device_info(dev_info); + + + char command_topic[64]; + + // Vertical Swing Entity + sprintf(command_topic, "cmnd/%s/SwingV", CFG_GetMQTTClientId()); + dev_info = hass_createSelectEntity( + "~/SwingV/get", // state_topic + command_topic, // command_topic + 9, // numoptions (VerticalSwingMode has 9 values) + vertical_swing_options, // fanOptions array + "Vertical Swing Mode" // title + ); + MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN); + hass_free_device_info(dev_info); + + // Horizontal Swing Entity + sprintf(command_topic, "cmnd/%s/SwingH", CFG_GetMQTTClientId()); + dev_info = hass_createSelectEntity( + "~/SwingH/get", // state_topic + command_topic, // command_topic + 10, // numoptions (HorizontalSwing has 10 values) + horizontal_swing_options, // fanOptions array + "Horizontal Swing Mode" // title + ); + MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN); + hass_free_device_info(dev_info); + +} + diff --git a/src/driver/drv_test_drivers.h b/src/driver/drv_test_drivers.h index ef4e5a04a..2fd4adb9a 100644 --- a/src/driver/drv_test_drivers.h +++ b/src/driver/drv_test_drivers.h @@ -14,3 +14,7 @@ void Test_LED_Driver_RunEverySecond(void); void Test_LED_Driver_OnChannelChanged(int ch, int value); +void Test_UART_Init(); +void Test_UART_RunEverySecond(); +void Test_UART_AppendInformationToHTTPIndexPage(http_request_t *request); + diff --git a/src/driver/drv_test_uart.c b/src/driver/drv_test_uart.c new file mode 100644 index 000000000..563cc8dde --- /dev/null +++ b/src/driver/drv_test_uart.c @@ -0,0 +1,21 @@ +#include "drv_test_drivers.h" + +#include "../obk_config.h" + +#include + +#include "../cmnds/cmd_public.h" + +void Test_UART_Init(void) { + +} + +void Test_UART_RunEverySecond(void) { + + +} + +void Test_UART_AppendInformationToHTTPIndexPage(http_request_t *request) { + +} + diff --git a/src/httpserver/hass.c b/src/httpserver/hass.c index 82ce40201..273e87dd4 100644 --- a/src/httpserver/hass.c +++ b/src/httpserver/hass.c @@ -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) { diff --git a/src/httpserver/hass.h b/src/httpserver/hass.h index 5611753fc..b12109b54 100644 --- a/src/httpserver/hass.h +++ b/src/httpserver/hass.h @@ -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); diff --git a/src/httpserver/http_fns.c b/src/httpserver/http_fns.c index 011209d30..d0e1f0c6e 100644 --- a/src/httpserver/http_fns.c +++ b/src/httpserver/http_fns.c @@ -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); diff --git a/src/new_pins.c b/src/new_pins.c index 245f962ed..a647923f8 100644 --- a/src/new_pins.c +++ b/src/new_pins.c @@ -2220,6 +2220,9 @@ const char* g_channelTypeNames[] = { "Tds", "Motion_n", "Frequency_div1000", + "OpenStopClose", + "error", + "error", "error", "error", }; diff --git a/src/new_pins.h b/src/new_pins.h index 47e92deb9..86cbd9978 100644 --- a/src/new_pins.h +++ b/src/new_pins.h @@ -1001,6 +1001,8 @@ typedef enum channelType_e { //chandetail:"file":"new_pins.h", //chandetail:"driver":""} ChType_Frequency_div1000, + + ChType_OpenStopClose, //chandetail:{"name":"Max", //chandetail:"title":"TODO", //chandetail:"descr":"This is the current total number of available channel types.", diff --git a/src/selftest/selftest_hass_discovery_ext.c b/src/selftest/selftest_hass_discovery_ext.c index bc06afcb5..86c6c34df 100644 --- a/src/selftest/selftest_hass_discovery_ext.c +++ b/src/selftest/selftest_hass_discovery_ext.c @@ -382,6 +382,61 @@ void Test_HassDiscovery_Channel_LowMidHigh() { SELFTEST_ASSERT_JSON_VALUE_STRING("dev", "mdl", PLATFORM_MCU_NAME); } +void Test_HassDiscovery_Channel_BatteryLevelPercent() { + const char *shortName = "WinCustom"; + const char *fullName = "Windows Fake Custom"; + const char *mqttName = "testCustom"; + SIM_ClearOBK(shortName); + SIM_ClearAndPrepareForMQTTTesting(mqttName, "bekens"); + + CFG_SetShortDeviceName(shortName); + CFG_SetDeviceName(fullName); + + CHANNEL_SetType(4, ChType_BatteryLevelPercent); + + SIM_ClearMQTTHistory(); + CMD_ExecuteCommand("scheduleHADiscovery 1", 0); + Sim_RunSeconds(5, false); + + // OBK device should publish JSON on MQTT topic "homeassistant" + SELFTEST_ASSERT_HAS_MQTT_JSON_SENT("homeassistant", true); + SELFTEST_ASSERT_JSON_VALUE_STRING("dev", "name", shortName); + SELFTEST_ASSERT_JSON_VALUE_STRING("dev", "sw", USER_SW_VER); + SELFTEST_ASSERT_JSON_VALUE_STRING("dev", "mf", MANUFACTURER); + SELFTEST_ASSERT_JSON_VALUE_STRING("dev", "mdl", PLATFORM_MCU_NAME); + + SELFTEST_ASSERT_JSON_VALUE_STRING(0, "stat_t", "~/4/get"); + SELFTEST_ASSERT_JSON_VALUE_STRING(0, "dev_cla", "battery"); + SELFTEST_ASSERT_JSON_VALUE_STRING(0, "unit_of_meas", "%"); +} +void Test_HassDiscovery_Channel_Smoke() { + const char *shortName = "WinCustom"; + const char *fullName = "Windows Fake Custom"; + const char *mqttName = "testCustom"; + SIM_ClearOBK(shortName); + SIM_ClearAndPrepareForMQTTTesting(mqttName, "bekens"); + + CFG_SetShortDeviceName(shortName); + CFG_SetDeviceName(fullName); + + CHANNEL_SetType(4, ChType_SmokePercent); + + SIM_ClearMQTTHistory(); + CMD_ExecuteCommand("scheduleHADiscovery 1", 0); + Sim_RunSeconds(5, false); + + // OBK device should publish JSON on MQTT topic "homeassistant" + SELFTEST_ASSERT_HAS_MQTT_JSON_SENT("homeassistant", true); + SELFTEST_ASSERT_JSON_VALUE_STRING("dev", "name", shortName); + SELFTEST_ASSERT_JSON_VALUE_STRING("dev", "sw", USER_SW_VER); + SELFTEST_ASSERT_JSON_VALUE_STRING("dev", "mf", MANUFACTURER); + SELFTEST_ASSERT_JSON_VALUE_STRING("dev", "mdl", PLATFORM_MCU_NAME); + + SELFTEST_ASSERT_JSON_VALUE_STRING(0, "stat_t", "~/4/get"); + SELFTEST_ASSERT_JSON_VALUE_STRING(0, "unit_of_meas", "%"); + SELFTEST_ASSERT_JSON_VALUE_STRING(0, "name", "Smoke"); +} + void Test_HassDiscovery_Channel_Custom() { const char *shortName = "WinCustom"; const char *fullName = "Windows Fake Custom"; @@ -617,8 +672,10 @@ void Test_HassDiscovery_Ext() { Test_HassDiscovery_Channel_Illuminance(); Test_HassDiscovery_Channel_LowMidHigh(); Test_HassDiscovery_Channel_Custom(); + Test_HassDiscovery_Channel_BatteryLevelPercent(); + Test_HassDiscovery_Channel_Smoke(); - + }