diff --git a/CLAUDE.md b/CLAUDE.md index 00d43ed..6cba898 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,5 +90,8 @@ GPIO0 to GND, tap RST to GND, then release GPIO0. 2. Skipped (the disassembly was enough): bit-stepping test sketch. 3. Done: 74HC595-multiplex driver (`nixie595.ino`) + CLOCK_90 in ESP_UniClock, ArduinoOTA, EU DST fix. Runs on the clock. -4. Next: Home Assistant controls over MQTT (display on/off, brightness, LEDs; upstream MQTT - only publishes sensors), OTA password via `secrets.h`. +4. Done: Home Assistant over MQTT (broker 10.1.1.175, discovery prefix `homeassistant`). + `mqtt.ino` rewritten for ArduinoHA 2.1.0: switches Tubes/Auto dim, numbers Day/Night + brightness, light Backlight (on/off, brightness, RGB -> UniClock colour wheel), select LED + effect, button Cathode protect, sensors Light/WiFi signal. Broker settings are read at boot. +5. Open: OTA password via `secrets.h`; ArduinoOTA after the AP -> WiFi switch. diff --git a/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino b/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino index f81fa73..568294e 100644 --- a/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino +++ b/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino @@ -247,6 +247,9 @@ DNSServer dnsServer; #ifdef USE_ARDUINO_OTA #include #endif +#ifdef USE_MQTT + #include //here and not only in mqtt.ino: PlatformIO's generated prototypes need its types early +#endif #if defined(USE_NEOPIXEL) || defined(WORDCLOCK) #include @@ -338,6 +341,7 @@ volatile boolean EEPROMsaving = false; //saving in progress - stop display refre bool colonBlinkState = false; boolean radarON = true; boolean mqttRadarON = true; +volatile boolean tubesOn = true; //Tubes switch from Home Assistant (runtime only, not saved); honoured by nixie595.ino unsigned long radarLastOn = 0; boolean makeFirmwareUpdate = false; boolean makeCathodeProtect = false; diff --git a/firmware/uniclock/ESP32_UniClock2/clocks.h b/firmware/uniclock/ESP32_UniClock2/clocks.h index e7b1041..19a0a61 100644 --- a/firmware/uniclock/ESP32_UniClock2/clocks.h +++ b/firmware/uniclock/ESP32_UniClock2/clocks.h @@ -141,6 +141,8 @@ byte tubePixels[] = {0, 0, 1, 1, 2, 2, 3, 3}; //8 WS2812, two per tube (strip #define AP_NAME "UNICLOCK" #define AP_PASSWORD "" #define WEBNAME "IN-12 Nixie Clock" +#define USE_MQTT //Home Assistant: broker address/user/password on the web page, then reboot +#define MQTT_PREFIX "homeassistant" //Home Assistant's default discovery prefix #define FIRMWARE_SERVER "" //no web-triggered HTTP update; use ArduinoOTA #define USE_ARDUINO_OTA #define OTA_HOSTNAME "nixie-in12" diff --git a/firmware/uniclock/ESP32_UniClock2/mqtt.ino b/firmware/uniclock/ESP32_UniClock2/mqtt.ino index 0ec4c64..ee3fd79 100644 --- a/firmware/uniclock/ESP32_UniClock2/mqtt.ino +++ b/firmware/uniclock/ESP32_UniClock2/mqtt.ino @@ -2,127 +2,107 @@ //========================================================================================================================= // mqtt.ino - Home Assistant / MQTT integration (compiled only if USE_MQTT is defined for the active CLOCK_NN in clocks.h) // -// Library: ArduinoHA (https://github.com/dawidchyrzynski/arduino-home-assistant), which wraps PubSubClient -// (https://pubsubclient.knolleary.net/) and adds Home Assistant MQTT Discovery on top of it. Because ArduinoHA -// is used, this file never hand-builds HA discovery JSON payloads itself - HADevice/HAMqtt/HASensor below -// publish the discovery config topics automatically the first time mqtt.begin()/mqtt.loop() connects. +// Library: ArduinoHA 2.x (https://github.com/dawidchyrzynski/arduino-home-assistant), which wraps PubSubClient +// (https://pubsubclient.knolleary.net/) and adds Home Assistant MQTT Discovery on top of it. The HA* objects +// below publish their discovery config automatically whenever the broker connection is (re)established. +// (Rewritten for ArduinoHA 2.1: the bundled 1.3 had no light/number/select/button entities.) // -// MASTER / SLAVE sensor-sharing architecture (the distinctive part of this subsystem): +// Entities (all grouped under one HA device, unique IDs prefixed with the MAC address): +// Sensors: temperature/humidity/pressure/lux/radar (whichever exist on this clock), WiFi signal +// Controls: Tubes (switch, runtime only), Day/Night brightness (numbers), Auto dim (switch), +// Cathode protect (button); with USE_NEOPIXEL also Backlight (light: on/off, brightness, colour) +// and LED effect (select). +// Changes made on the clock's own web page are reported back to HA by mqttSend(). +// Settings changed from HA are saved to EEPROM a few seconds after the last change (one flash write per burst). +// +// MASTER / SLAVE sensor-sharing architecture: // Every clock that defines USE_MQTT connects to the same broker (prm.mqttBrokerAddr/User/Psw, set on the web UI) and -// PUBLISHES its own local sensors (temperature/humidity/pressure/lux/radar, whichever are physically present - see -// HASensor objects below) under a "/sensor//..." style topic, discovered via HA MQTT Discovery. -// A clock that does NOT have its own sensor (or wants to reuse another clock's reading) can instead be built with -// USE_MASTER_CLOCK, and SUBSCRIBE to another clock's already-published topics: +// PUBLISHES its own local sensors. A clock that does NOT have its own sensor (or wants to reuse another clock's +// reading) can instead be built with USE_MASTER_CLOCK, and SUBSCRIBE to another clock's already-published topics: // USE_MASTER_TEMP + MASTER_TEMPERATURE_TOPIC -> mirrors the master's temperature into local temperature[] array // USE_MASTER_HUMID + MASTER_HUMIDITY_TOPIC -> mirrors the master's humidity into local humid[] array // USE_MASTER_RADAR + MASTER_RADAR_TOPIC -> mirrors the master's PIR/radar presence state into mqttRadarON -// This lets one "master" clock's physical sensor (e.g. a dedicated sensor box, or a clock with a real radar/PIR -// sensor wired to RADAR_PIN) feed several "slave" clocks that have no sensor of their own, purely over MQTT. -// IMPORTANT: if a "slave" clock DOES also have its own local RADAR_PIN wired, the local pin always wins - the radar -// topic subscription is skipped entirely in onMqttConnected() (see RADAR_PIN>=0 check there), and the core loop's -// checkTubePowerOnOff() also prefers digitalRead(RADAR_PIN) over the MQTT-derived mqttRadarON when both exist. -// As of the current clocks.h: CLOCK_99 ("Dummy clock, sensor box with MQTT connector") is a pure MASTER/publisher -// (USE_MQTT on, USE_MASTER_CLOCK off, has its own RADAR_PIN). CLOCK_26 has USE_MQTT + USE_MASTER_CLOCK + -// USE_MASTER_RADAR all on, so it both publishes its own temperature/humidity sensors AND subscribes to another -// clock's radar topic. CLOCK_23/CLOCK_23A define the USE_MASTER_* macros too but currently have USE_MQTT commented -// out in clocks.h, so for those configs this whole file is compiled out and the master settings are presently inert. +// If a "slave" clock DOES also have its own local RADAR_PIN wired, the local pin always wins - the radar topic +// subscription is skipped in onMqttConnected(), and checkTubePowerOnOff() prefers digitalRead(RADAR_PIN). +// NOTE: the topic layout changed with ArduinoHA 2.x: sensor values are now published on +// "///stat_t" (data prefix "aha" by default), so MASTER_*_TOPIC settings +// written for the old library must be updated. // -// Call pattern from the core .ino: setupMqtt() is called once from setup() when USE_MQTT is defined; mqttSend() is -// called every loop() iteration (but only while WiFi is connected and prm.mqttEnable is set) - it internally rate-limits -// actual publishes to once every prm.mqttBrokerRefresh seconds, while still pumping mqtt.loop() (which also drives -// PubSubClient's/ArduinoHA's own reconnect handling) on every call. +// Call pattern from the core .ino: setupMqtt() is called once from setup() when USE_MQTT is defined (the broker +// address/user/password are read then, so a change on the web UI needs a reboot); mqttSend() is called every loop() +// iteration while WiFi is connected and prm.mqttEnable is set. It pumps mqtt.loop() on every call and refreshes +// sensor values every prm.mqttBrokerRefresh seconds (at least every MQTT_MIN_REFRESH seconds). //_______________________________________________________________________________________________________________________ //To use MQTT services the following settings should be defined in clocks.h //#define USE_MQTT -//#define MQTT_PREFIX "UniClock32" +//#define MQTT_PREFIX "homeassistant" //HA discovery prefix (Home Assistant's default is "homeassistant") //#define USE_MASTER_CLOCK //enable it, if you want to get any data from MASTER CLOCK. This will be the sensor#0 //#define USE_MASTER_TEMP //enable it, if you want to get temperature from MASTER CLOCK //#define USE_MASTER_HUMID //enable it, if you want to get humidity from MASTER CLOCK -//#define USE_MASTER_RADAR //enable it, if you want to get radar from MASTER CLOCK -//#define MASTER_TEMPERATURE_TOPIC "homeassistant/sensor/10521c5e14c4/temperature/state" -//#define MASTER_HUMIDITY_TOPIC "homeassistant/sensor/10521c5e14c4/humidity/state" -//#define MASTER_RADAR_TOPIC "homeassistant/sensor/10521c5e14c4/radar/state" +//#define USE_MASTER_RADAR //enable it, if you want to get radar state from MASTER CLOCK +//#define MASTER_TEMPERATURE_TOPIC "..." +//#define MASTER_HUMIDITY_TOPIC "..." +//#define MASTER_RADAR_TOPIC "..." //_______________________________________________________________________________________________________________________ -// (the block above is documentation only, shown here as a template of what to copy into a CLOCK_NN section of clocks.h - -// none of these #define lines are active in this file; the real values come from clocks.h at compile time.) - -//To subscribe to my sensors: You can check it with Chrome browser + MQTTlens addon) MAC_ADDRESS: for example: 3e090b3e090b -// homeassistant/sensor/3e090b3e090b/temperature/value -// homeassistant/sensor/3e090b3e090b/humidity/value -// homeassistant/sensor/3e090b3e090b/pressure/value -// homeassistant/sensor/3e090b3e090b/lux/value - -// old solution, now moved to prm.mqttBrokerAddr, available on clock's web page -//#define BROKER_ADDR IPAddress(192,168,1,241) -//#define BROKER_USERNAME "" //moved to prm.mqttBrokerUser -//#define BROKER_PASSWORD "" //moved to mqttBrokerPsw -//_______________________________________________________________________________________________________________________ -// Credential handling: broker IP/user/password are NOT compile-time constants (see the commented-out BROKER_* macros -// above, kept only as historical documentation) - they live in prm.mqttBrokerAddr/mqttBrokerUser/mqttBrokerPsw, which -// are persisted settings editable from the clock's own web UI (and read here in setupMqtt()/mqttSend()). This lets a -// user point any already-flashed clock at a different broker without recompiling. -// SECURITY: factoryReset() in the core .ino seeds these with a hardcoded default -// (strcpy(prm.mqttBrokerAddr,"10.10.0.202"); strcpy(prm.mqttBrokerUser,"mqtt"); strcpy(prm.mqttBrokerPsw,"mqtt")) - -// i.e. a real broker IP and a trivial user/password baked into the firmware as the out-of-the-box default. Not this -// file's code, but directly relevant here since it is exactly the credential pair mqttSend()/setupMqtt() use below. #include //https://github.com/dawidchyrzynski/arduino-home-assistant -//https://pubsubclient.knolleary.net/ https://github.com/knolleary/pubsubclient #if !defined(MQTT_PREFIX) - #define MQTT_PREFIX "UniClock32" //used both as the HA discovery prefix and as the first path segment of this device's own published topics + #define MQTT_PREFIX "UniClock32" //HA discovery prefix (upstream default; Home Assistant itself listens on "homeassistant") #endif +#define MQTT_MIN_REFRESH 10 //seconds between sensor refreshes if prm.mqttBrokerRefresh is lower +#define MQTT_SAVE_DELAY 3000 //ms after the last change from HA before the settings are written to EEPROM +#define MQTT_WHITE_INDEX 192 //same as WHITE_INDEX in z_neopixel.ino (that file is compiled after this one) -byte mac[6]; //this device's WiFi MAC, used as a per-device unique ID/topic segment (see setupMqtt()) -char masterTemperatureTopic[80] = ""; //resolved from MASTER_TEMPERATURE_TOPIC (clocks.h) if USE_MASTER_CLOCK/USE_MASTER_TEMP is used - topic this clock SUBSCRIBES to -char masterHumidityTopic[80] = ""; //resolved from MASTER_HUMIDITY_TOPIC (clocks.h) if USE_MASTER_CLOCK/USE_MASTER_HUMID is used - topic this clock SUBSCRIBES to -char masterRadarTopic[80] = ""; //resolved from MASTER_RADAR_TOPIC (clocks.h) if USE_MASTER_CLOCK/USE_MASTER_RADAR is used - topic this clock SUBSCRIBES to - -// Index into the shared temperature[]/humid[] arrays (declared in the core .ino, up to 6 slots) reserved for the -// MASTER clock's mirrored reading, so the rest of the firmware (display rotation, web UI, etc.) can treat the -// MQTT-sourced master reading exactly like one more local sensor. Assigned in setupMqtt() as "next free slot" -// (useTemp/useHumid BEFORE incrementing) - despite the doc comment above saying "this will be the sensor#0", it is -// only actually sensor#0 if this clock has no other local temperature/humidity sensor already counted. +byte mac[6]; //this device's WiFi MAC, used as the HA device ID +char masterTemperatureTopic[80] = ""; //resolved from MASTER_TEMPERATURE_TOPIC (clocks.h) - topic this clock SUBSCRIBES to +char masterHumidityTopic[80] = ""; //resolved from MASTER_HUMIDITY_TOPIC (clocks.h) - topic this clock SUBSCRIBES to +char masterRadarTopic[80] = ""; //resolved from MASTER_RADAR_TOPIC (clocks.h) - topic this clock SUBSCRIBES to byte MASTERtempPtr=0; byte MASTERhumidPtr=0; +char mqttConfigUrl[40]; +unsigned long mqttSaveRequested = 0; //millis() of the last change from HA that still has to be saved, 0 = none WiFiClient client; HADevice device; -HAMqtt mqtt(client, device); //ArduinoHA wrapper around PubSubClient; mqtt.loop() also drives PubSubClient's own reconnect/keepalive handling -//Sensors of local clock, to send data (each one auto-publishes its HA MQTT Discovery config the first time it connects): -HASensor mqttTemp("temperature"); -HASensor mqttHumid("humidity"); -HASensor mqttPress("pressure"); -HASensor mqttLux("lux"); -HASensor mqttRadar("radar"); +HAMqtt mqtt(client, device, 20); //up to 20 entities + +//Sensors that only some clocks have are created in setupMqtt(): every constructed entity registers itself with +//mqtt and is published to HA, so unused ones would show up as empty entities. +HASensorNumber* mqttTemp = nullptr; +HASensorNumber* mqttHumid = nullptr; +HASensorNumber* mqttPress = nullptr; +HASensorNumber* mqttLux = nullptr; +HABinarySensor* mqttRadar = nullptr; +HASensorNumber mqttRssi("rssi"); +HASwitch mqttTubes("tubes"); +HANumber mqttDayBright("day_brightness"); +HANumber mqttNightBright("night_brightness"); +HASwitch mqttAutoDim("auto_dim"); +HAButton mqttCathodeProtect("cathode_protect"); +#ifdef USE_NEOPIXEL + HALight mqttBacklight("backlight", HALight::BrightnessFeature | HALight::RGBFeature); + HASelect mqttLedEffect("led_effect"); + byte mqttLastEffect = 2; //effect restored when the backlight is switched on from HA (rainbow flow by default) +#endif + +//Bounded copy of an MQTT payload into a zero-terminated buffer. +// FIXED: the old onMqttMessage() memcpy'd the payload into a 100-byte stack buffer without a length check or a +// terminator (possible stack overflow with a long payload). +static void payloadToString(const uint8_t* payload, uint16_t length, char* buf, size_t bufSize) { + size_t n = min((size_t)length, bufSize - 1); + memcpy(buf, payload, n); + buf[n] = 0; +} -// This is the SLAVE side of the master/slave architecture: called by ArduinoHA/PubSubClient whenever a message -// arrives on any topic this clock is subscribed to (see onMqttConnected() below for what gets subscribed - only -// the MASTER_* topics selected via USE_MASTER_TEMP/USE_MASTER_HUMID/USE_MASTER_RADAR in clocks.h). The payload is -// the raw value published by the MASTER clock's own mqttSend()/HASensor.setValue() (a plain ASCII number for -// temperature/humidity, "0"/"1" for radar - matching HASensor's default text encoding). void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) { float tempTMP,humidTMP; - char tmp[100]; - // This callback is called when message from MQTT broker is received. - // Please note that you should always verify if the message's topic is the one you expect. - // For example: if (memcmp(topic, "myCustomTopic") == 0) { ... } + char tmp[32]; + payloadToString(payload, length, tmp, sizeof(tmp)); DPRINT("New message on topic: "); DPRINT(topic); - - // POSSIBLE BUG: `length` (the broker-reported payload size) is copied into the fixed 100-byte `tmp` buffer with - // no bounds check - a payload of 100 bytes or more (e.g. a misconfigured/malicious publisher on the same topic) - // overflows `tmp` on the stack. Also, memcpy() does NOT null-terminate `tmp`: it is used afterwards as a C string - // (DPRINTLN(tmp), atof(tmp), tmp[0]) with whatever byte happens to follow the copied data in memory, which is - // undefined behavior if that byte isn't already '\0'. A `if (length >= sizeof(tmp)) length = sizeof(tmp)-1;` - // guard plus `tmp[length] = '\0';` after the memcpy would fix both issues. - memcpy(tmp,payload,length); DPRINT(" Data: "); DPRINTLN(tmp); - // Topic match: compares up to sizeof(masterTemperatureTopic) (80, the fixed buffer size) bytes rather than the - // actual topic length. This still behaves like strcmp() in practice because strncmp() stops as soon as it hits - // the '\0' terminator on either side, but it is fragile - if MASTER_TEMPERATURE_TOPIC in clocks.h were ever 80 - // characters or longer, strncpy() in setupMqtt() would not null-terminate masterTemperatureTopic, and this - // comparison could silently misbehave. Using strcmp(topic, masterTemperatureTopic) would be more robust. + if ((strlen(masterTemperatureTopic)>0) && (strncmp(topic,masterTemperatureTopic,sizeof(masterTemperatureTopic)) ==0)) { tempTMP = atof(tmp); temperature[MASTERtempPtr] = tempTMP; //overwrite the reserved "master" slot in the shared temperature[] array (see MASTERtempPtr above) @@ -134,12 +114,8 @@ void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) { humid[MASTERhumidPtr] = humidTMP; //overwrite the reserved "master" slot in the shared humid[] array (see MASTERhumidPtr above) } if ((strlen(masterRadarTopic)>0) && (strncmp(topic,masterRadarTopic,sizeof(masterRadarTopic)) ==0)) { - // Master clock's radar/PIR presence state, mirrored into mqttRadarON (and radarON) so the rest of the - // firmware (night animation, tube power on/off, etc.) reacts to it exactly like a local sensor reading. - // We only ever get here if this clock did NOT subscribe to the radar topic due to having its own local - // RADAR_PIN (see the RADAR_PIN>=0 guard in onMqttConnected() below) - local hardware always wins. DPRINT("MASTER radar:"); DPRINTLN(tmp[0]); - if (tmp[0] == '0') { + if ((tmp[0] == '0') || (strcmp(tmp, "OFF") == 0)) { mqttRadarON = false; radarON = mqttRadarON; } @@ -149,17 +125,11 @@ void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) { radarLastOn = millis(); } DPRINT("mqttRadarON:"); DPRINTLN(mqttRadarON); - } - //mqtt.publish("myPublishTopic", "hello"); } -// Called by ArduinoHA once the broker connection (re)establishes - this is where the SLAVE side actually subscribes -// to the MASTER clock's topics. Runs again on every reconnect (not just the first connect), so subscriptions survive -// broker restarts/WiFi drops without any extra logic needed in mqttSend(). void onMqttConnected() { DPRINTLN("Connected to the broker!"); - // You can subscribe to custom topic if you need #ifdef USE_MASTER_CLOCK DPRINTLN(F("MASTER_CLOCK enabled! Subscribe to following topics:")); #ifdef USE_MASTER_TEMP @@ -181,10 +151,6 @@ void onMqttConnected() { } #endif #ifdef USE_MASTER_RADAR - // WHY: if this clock has its own physical radar/PIR sensor wired to RADAR_PIN, that local reading always - // takes priority (see checkTubePowerOnOff() in the core .ino, which prefers digitalRead(RADAR_PIN) over - // mqttRadarON when both exist). So there is no point subscribing to the master's radar topic at all in that - // case - onMqttMessage()'s radar branch would just be mirrored into a value nothing ever reads. if (RADAR_PIN>=0) { DPRINTLN(F("Local radar enabled! MQTT MASTER radar disabled!")); } @@ -201,65 +167,213 @@ void onMqttConnected() { #endif } +//--- commands from Home Assistant --------------------------------------------------------------------------------------- + +void mqttRequestSave() { + mqttSaveRequested = millis(); + if (mqttSaveRequested == 0) mqttSaveRequested = 1; +} + +void onTubesCommand(bool state, HASwitch* sender) { + tubesOn = state; + DPRINT("MQTT: tubes "); DPRINTLN(state ? "on" : "off"); + sender->setState(state); +} + +void onBrightnessCommand(HANumeric number, HANumber* sender) { + if (!number.isSet()) return; + int v = constrain((int)number.toInt16(), 0, MAXBRIGHTNESS); + if (sender == &mqttDayBright) prm.dayBright = v; + else prm.nightBright = v; + sender->setState(v); + mqttRequestSave(); +} + +void onAutoDimCommand(bool state, HASwitch* sender) { + prm.enableAutoDim = state; + autoBrightness = state; //same as the web UI handler: takes effect at once + sender->setState(state); + mqttRequestSave(); +} + +void onCathodeProtectCommand(HAButton* sender) { + makeCathodeProtect = true; //executed from loop(), like the web UI button +} + +#ifdef USE_NEOPIXEL +//Inverse of Wheel() in z_neopixel.ino: the wheel position whose hue is closest to an RGB colour (saturation and +//brightness are ignored; grey/white maps to the white position). +byte rgbToWheel(byte r, byte g, byte b) { + byte lo = min(r, min(g, b)); + int q; + if ((max(r, max(g, b)) - lo) < 16) return MQTT_WHITE_INDEX; + r -= lo; g -= lo; b -= lo; + if (g == 0) q = (85 * b) / (r + b); //Wheel q 0..84: (255-3k, 0, 3k) + else if (r == 0) q = 85 + (85 * g) / (g + b); //Wheel q 85..169: (0, 3k, 255-3k) + else q = 170 + (85 * r) / (r + g); //Wheel q 170..254: (3k, 255-3k, 0) + q = min(q, 254); + byte pos = 255 - q; //Wheel() inverts its argument + if (pos == MQTT_WHITE_INDEX) pos++; //192 is reserved for white + return pos; +} + +HALight::RGBColor wheelToRgb(byte pos) { + RgbColor c = Wheel(pos); + HALight::RGBColor color; + color.red = c.R; color.green = c.G; color.blue = c.B; + color.isSet = true; + return color; +} + +void onBacklightStateCommand(bool state, HALight* sender) { + if (state) { + if (prm.rgbEffect == 0) prm.rgbEffect = (mqttLastEffect > 0) ? mqttLastEffect : 1; + } + else { + if (prm.rgbEffect > 0) mqttLastEffect = prm.rgbEffect; + prm.rgbEffect = 0; + } + sender->setState(state); + mqttLedEffect.setState(prm.rgbEffect); + mqttRequestSave(); +} + +void onBacklightBrightnessCommand(uint8_t brightness, HALight* sender) { + prm.rgbBrightness = brightness; + sender->setBrightness(brightness); + mqttRequestSave(); +} + +void onBacklightColorCommand(HALight::RGBColor color, HALight* sender) { + if (!color.isSet) return; + prm.rgbFixColor = rgbToWheel(color.red, color.green, color.blue); + prm.rgbEffect = 1; //a colour from HA means "fixed colour" + sender->setRGBColor(wheelToRgb(prm.rgbFixColor)); + mqttLedEffect.setState(prm.rgbEffect); + mqttRequestSave(); +} + +void onLedEffectCommand(int8_t index, HASelect* sender) { + if ((index < 0) || (index > 10)) return; + prm.rgbEffect = index; + if (index > 0) mqttLastEffect = index; + sender->setState(index); + mqttBacklight.setState(index > 0); + mqttRequestSave(); +} +#endif + void setupMqtt() { char mactmp[20]; WiFi.macAddress(mac); device.setUniqueId(mac, sizeof(mac)); //set device MAC, as unique ID - sprintf(mactmp,"%02x%02x%02x%02x%02x%02x",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]); + device.enableExtendedUniqueIds(); //entity unique IDs = _, so several clocks don't clash in HA + sprintf(mactmp,"%02x%02x%02x%02x%02x%02x",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]); + #if defined(MASTER_TEMPERATURE_TOPIC) strncpy(masterTemperatureTopic,MASTER_TEMPERATURE_TOPIC,sizeof(masterTemperatureTopic)); #endif #if defined(MASTER_HUMIDITY_TOPIC) strncpy(masterHumidityTopic,MASTER_HUMIDITY_TOPIC,sizeof(masterHumidityTopic)); - #endif + #endif #if defined(MASTER_RADAR_TOPIC) strncpy(masterRadarTopic,MASTER_RADAR_TOPIC,sizeof(masterRadarTopic)); #endif - DPRINT("Starting MQTT CLient: "); DPRINT(MQTT_PREFIX); DPRINT("/sensor/"); DPRINTLN(mactmp); - + + DPRINT("Starting MQTT Client, discovery prefix: "); DPRINT(MQTT_PREFIX); DPRINT(" device: "); DPRINTLN(mactmp); mqtt.setDiscoveryPrefix(MQTT_PREFIX); - device.setName(FW); - device.setSoftwareVersion("3.0"); + device.setName(webName); + device.setManufacturer("UniClock"); + device.setModel(tubeDriver); + device.setSoftwareVersion(FW); + snprintf(mqttConfigUrl, sizeof(mqttConfigUrl), "http://%s/", WiFi.localIP().toString().c_str()); + device.setConfigurationUrl(mqttConfigUrl); + device.enableSharedAvailability(); + device.enableLastWill(); + + //useTemp/useHumid count the master clock's sensors too, which are added at the end of this function - + //so as before, a master-only sensor is not republished by this clock if (useTemp>0) { - mqttTemp.setUnitOfMeasurement("°C"); - mqttTemp.setDeviceClass("sensor"); - mqttTemp.setName("Inside temperature"); + mqttTemp = new HASensorNumber("temperature", HASensorNumber::PrecisionP1); + mqttTemp->setName("Temperature"); + mqttTemp->setDeviceClass("temperature"); + mqttTemp->setUnitOfMeasurement("°C"); } if (useHumid>0) { - mqttHumid.setUnitOfMeasurement("%"); - mqttHumid.setDeviceClass("sensor"); - mqttHumid.setName("Inside humidity"); + mqttHumid = new HASensorNumber("humidity", HASensorNumber::PrecisionP1); + mqttHumid->setName("Humidity"); + mqttHumid->setDeviceClass("humidity"); + mqttHumid->setUnitOfMeasurement("%"); } if (usePress>0) { - mqttPress.setUnitOfMeasurement("hPa"); - mqttPress.setDeviceClass("sensor"); - mqttPress.setName("Air pressure"); + mqttPress = new HASensorNumber("pressure", HASensorNumber::PrecisionP1); + mqttPress->setName("Air pressure"); + mqttPress->setDeviceClass("pressure"); + mqttPress->setUnitOfMeasurement("hPa"); } if (useLux>0) { - mqttLux.setUnitOfMeasurement("lux"); - mqttLux.setDeviceClass("sensor"); - mqttLux.setName("Inside lux"); + mqttLux = new HASensorNumber("lux"); + mqttLux->setName("Light"); + mqttLux->setDeviceClass("illuminance"); + mqttLux->setUnitOfMeasurement("lx"); } #if RADAR_PIN>=0 - mqttRadar.setUnitOfMeasurement("on/off"); - mqttRadar.setDeviceClass("sensor"); - mqttRadar.setName("Radar sensor"); + mqttRadar = new HABinarySensor("radar"); + mqttRadar->setName("Radar"); + mqttRadar->setDeviceClass("occupancy"); + #endif + mqttRssi.setName("WiFi signal"); + mqttRssi.setDeviceClass("signal_strength"); + mqttRssi.setUnitOfMeasurement("dBm"); + + mqttTubes.setName("Tubes"); + mqttTubes.setIcon("mdi:numeric"); + mqttTubes.onCommand(onTubesCommand); + + mqttDayBright.setName("Day brightness"); + mqttNightBright.setName("Night brightness"); + for (HANumber* n : {&mqttDayBright, &mqttNightBright}) { + n->setIcon("mdi:brightness-6"); + n->setMin(0); + n->setMax(MAXBRIGHTNESS); + n->setMode(HANumber::ModeSlider); + n->onCommand(onBrightnessCommand); + } + + mqttAutoDim.setName("Auto dim"); + mqttAutoDim.setIcon("mdi:brightness-auto"); + mqttAutoDim.onCommand(onAutoDimCommand); + + mqttCathodeProtect.setName("Cathode protect"); + mqttCathodeProtect.setIcon("mdi:autorenew"); + mqttCathodeProtect.onCommand(onCathodeProtectCommand); + + #ifdef USE_NEOPIXEL + mqttBacklight.setName("Backlight"); + mqttBacklight.onStateCommand(onBacklightStateCommand); + mqttBacklight.onBrightnessCommand(onBacklightBrightnessCommand); + mqttBacklight.onRGBColorCommand(onBacklightColorCommand); + if (prm.rgbEffect > 0) mqttLastEffect = prm.rgbEffect; + + mqttLedEffect.setName("LED effect"); + mqttLedEffect.setIcon("mdi:led-strip-variant"); + //order = prm.rgbEffect values, see doAnimationMakuna() in z_neopixel.ino + mqttLedEffect.setOptions("Off;Fixed colour;Rainbow flow;Rainbow step;Colour dimmer;Colour stepper;" + "Stepflow table;Stepflow random;Stepflow random pixels;Stepflow random all;KITT"); + mqttLedEffect.onCommand(onLedEffectCommand); #endif mqtt.onMessage(onMqttMessage); mqtt.onConnected(onMqttConnected); - + IPAddress tmp; tmp.fromString(String(prm.mqttBrokerAddr)); if (strlen(prm.mqttBrokerUser)==0) mqtt.begin(tmp); - else + else mqtt.begin(tmp, prm.mqttBrokerUser, prm.mqttBrokerPsw); - - //mqtt.begin(BROKER_ADDR, BROKER_USERNAME, BROKER_PASSWORD); - device.enableLastWill(); #ifdef USE_MASTER_CLOCK DPRINTLN("MQTT MASTER_CLOCK enabled!"); DPRINT("MASTER_TEMPERATURE_TOPIC:"); DPRINTLN(masterTemperatureTopic); @@ -270,37 +384,69 @@ void setupMqtt() { MASTERtempPtr = useTemp; //remember my ID-s useTemp++; //increase the number of sensors #endif - #ifdef USE_MASTER_HUMID + #ifdef USE_MASTER_HUMID DPRINTLN(" - MQTT MASTER_HUMID sensor added!"); MASTERhumidPtr = useHumid; useHumid++; - #endif + #endif #endif } +//Report the current settings to HA. The library only publishes values that changed, so this is cheap; it keeps +//HA in sync with changes made on the clock's web page. +void mqttReportState() { + mqttTubes.setState(tubesOn); + mqttDayBright.setState((int16_t)prm.dayBright); + mqttNightBright.setState((int16_t)prm.nightBright); + mqttAutoDim.setState(prm.enableAutoDim); + #ifdef USE_NEOPIXEL + mqttBacklight.setState(prm.rgbEffect > 0); + mqttBacklight.setBrightness(prm.rgbBrightness); + mqttBacklight.setRGBColor(wheelToRgb(prm.rgbFixColor)); + mqttLedEffect.setState(prm.rgbEffect); + #endif +} + void mqttSend() { - static unsigned long lastRun = millis(); - //if (!mqtt.isConnected()) return; + static unsigned long lastRun = 0; + static unsigned long lastReport = 0; + static bool wasConnected = false; + mqtt.loop(); - - if ((millis() - lastRun) < prm.mqttBrokerRefresh*1000) return; - lastRun = millis(); - if (mqtt.isConnected()) DPRINT("MQTT Connected."); - else { - DPRINTLN("MQTT Failed to connect."); + + if (mqttSaveRequested && ((millis() - mqttSaveRequested) > MQTT_SAVE_DELAY)) { + mqttSaveRequested = 0; + DPRINTLN("MQTT: saving settings"); + saveEEPROM(); + } + + if (!mqtt.isConnected()) { + wasConnected = false; return; } + if (!wasConnected) { //(re)connected: force the first sensor refresh now + wasConnected = true; + lastRun = millis() - 1000ul * max((int)prm.mqttBrokerRefresh, MQTT_MIN_REFRESH); + } + + if ((millis() - lastReport) > 1000) { + lastReport = millis(); + mqttReportState(); + } + + if ((millis() - lastRun) < 1000ul * max((int)prm.mqttBrokerRefresh, MQTT_MIN_REFRESH)) return; + lastRun = millis(); + DPRINT("MQTT send:"); - if (useTemp>0) {mqttTemp.setValue(temperature[0]+ prm.corrT0); DPRINT(temperature[0]+ prm.corrT0); DPRINT(", ");} - if (useHumid>0) {mqttHumid.setValue(humid[0]+ prm.corrH0); DPRINT(humid[0]+ prm.corrH0); DPRINT(", ");} - if (usePress>0) {mqttPress.setValue(pressur[0]); DPRINT(pressur[0]); DPRINT(", ");} - if (useLux>0) {mqttLux.setValue(lx); DPRINT(lx); DPRINT(", ");} - #if RADAR_PIN>=0 - mqttRadar.setValue(radarON); DPRINT(radarON); - #endif + if (mqttTemp) {mqttTemp->setValue(temperature[0]+ prm.corrT0); DPRINT(temperature[0]+ prm.corrT0); DPRINT(", ");} + if (mqttHumid) {mqttHumid->setValue(humid[0]+ prm.corrH0); DPRINT(humid[0]+ prm.corrH0); DPRINT(", ");} + if (mqttPress) {mqttPress->setValue(pressur[0]); DPRINT(pressur[0]); DPRINT(", ");} + if (mqttLux) {mqttLux->setValue((int32_t)lx); DPRINT(lx); DPRINT(", ");} + if (mqttRadar) {mqttRadar->setState(radarON); DPRINT(radarON);} + mqttRssi.setValue((int32_t)WiFi.RSSI()); DPRINTLN(" "); } - + #else void mqttSend() {} #endif diff --git a/firmware/uniclock/ESP32_UniClock2/nixie595.ino b/firmware/uniclock/ESP32_UniClock2/nixie595.ino index 557f621..198195b 100644 --- a/firmware/uniclock/ESP32_UniClock2/nixie595.ino +++ b/firmware/uniclock/ESP32_UniClock2/nixie595.ino @@ -128,7 +128,7 @@ void IRAM_ATTR writeDisplay() { //timer1 ISR, single-shot: must re-arm timer1 } if (timer < 500) timer = 500; //a too short timer1 reload can miss the interrupt - if ((brightness == 0) || (state == 3) || (!radarON)) { + if ((brightness == 0) || (state == 3) || (!radarON) || (!tubesOn)) { state = 0; w = 0; } diff --git a/firmware/uniclock/README.md b/firmware/uniclock/README.md index 23e2c1c..978bbf4 100644 --- a/firmware/uniclock/README.md +++ b/firmware/uniclock/README.md @@ -7,6 +7,8 @@ Peter Gautier (GPL-3.0-or-later), vendored from upstream commit `1189810` (2026- It keeps the upstream layout, so it still opens in the Arduino IDE. - `lib/`: the library versions bundled upstream (`libraries/*.zip`), only those the ESP8266 build needs. The upstream manuals, datasheets, photos and ESP32-only libraries were left out. + Exception: `arduino-home-assistant` is v2.1.0 (upstream bundles 1.3.0, which has no light, + number, select or button entities); `mqtt.ino` was rewritten for it. - `platformio.ini`: PlatformIO build for the ESP-12 (4 MB flash, 2 MB SPIFFS). Local changes are separate commits on top of the import (`git log -- firmware/uniclock`). diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/CHANGELOG.md b/firmware/uniclock/lib/arduino-home-assistant-main/CHANGELOG.md deleted file mode 100644 index 4573d10..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/CHANGELOG.md +++ /dev/null @@ -1,37 +0,0 @@ -# Changelog - -## 1.3.0 - -**New features:** -* Added `onMessage()` method to HAMqtt class -* Added support for HA Covers -* Added support for setting different prefix for non-discovery topics (see [Advanced MQTT example](examples/mqtt-advanced/mqtt-advanced.ino)) -* Added `setName` method to HASensor -* Added `setName` method to HASwitch -* Added `onBeforeStateChanged` callback to HASwitch - -**Updates:** -* Removed legacy properties from HAFan (Home Assistant 2021.4.4). Deprecated methods will be removed after a quarter (2021.7) -* Separated `uniqueID` field from `name` in all devices types - -## 1.2.0 - -**Breaking changes:** -* Refactored HASensor implementation. Please take a look at [updated example](examples/sensor/sensor.ino) - -**New features:** -* Added support for HVAC -* Added support for excluding devices types from the compilation using defines (see [src/ArduinoHADefines.h](src/ArduinoHADefines.h)) -* Added support for setting icon in HASwitch and HASensor -* Added support for setting retain flag in HASwitch -* Added support for text (const char*) payload in HASensor -* Added support for fans (HAFan) -* Added support for connecting to the MQTT broker using hostname -* Added `onConnected()` method in the HAMqtt -* Added `onConnectionFailed()` method in the HAMqtt -* Added support for MQTT LWT (see [Advanced Availability example](examples/advanced-availability/advanced-availability.ino)) - -**Updates:** -* Optimized codebase and logic in all devices types -* Updated all examples -* Fixed compilation warnings in all classes diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/README.md b/firmware/uniclock/lib/arduino-home-assistant-main/README.md deleted file mode 100644 index 9957e95..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/README.md +++ /dev/null @@ -1,85 +0,0 @@ -# Arduino Home Assistant integration - -ArduinoHA allows to integrate an Arduino/ESP based device with Home Assistant using MQTT. -The library is designed to use as little resources (RAM/flash) as possible. -Initially it was optimized to work on Arduino Uno with Ethernet Shield, -but I successfully use it on ESP8266/ESP8255 boards in my projects. - -## Features - -* MQTT discovery (device is added to the Home Assistant panel automatically) -* MQTT Last Will and Testament -* Support for custom MQTT messages (publishing and subscribing) -* Auto reconnect with MQTT broker -* Reporting availability (online/offline states) of a device - -## Supported HA types - -* Binary sensors -* Covers -* Fans -* Device triggers -* Switches -* Sensors -* Tag scanner -* HVACs *(side note: HVACs requires more flash size than other HA types. It's not suitable for Arduino Nano/Uno)* - -## Examples - -* [Binary Sensor](examples/binary-sensor/binary-sensor.ino) -* [Cover](examples/cover/cover.ino) -* [Fan](examples/fan/fan.ino) -* [LED switch](examples/led-switch/led-switch.ino) -* [Multi-state button](examples/multi-state-button/multi-state-button.ino) -* [Sensor (temperature, humidity, etc.)](examples/sensor/sensor.ino) -* [HVAC](examples/hvac/hvac.ino) -* [NodeMCU Wi-Fi](examples/nodemcu/nodemcu.ino) -* [Arduino Nano 33 IoT Wi-Fi (SAMD)](examples/nano33iot/nano33iot.ino) -* [Availability feature](examples/availability) -* [Advanced availability (MQTT LWT)](examples/advanced-availability/advanced-availability.ino) -* [MQTT with credentials](examples/mqtt-with-credentials/mqtt-with-credentials.ino) -* [MQTT advanced](examples/mqtt-advanced/mqtt-advanced.ino) - -## Tested boards - -* Arduino Uno -* Arduino Mega -* NodeMCU -* ESP-01 -* Generic ESP8266/ESP8255 -* Arduino Nano 33 IoT (SAMD) - -## Tested devices - -* Controllino Maxi (standard/pure/automation/power) -* Controllino Mega (standard/pure) -* Sonoff Dual R2 -* Sonoff Basic -* Sonoff Mini -* Tuya Wi-Fi switch module -* Tuya Wi-Fi curtain module - -## Tested Arduino Shields - -* Arduino Ethernet Shield (WizNet W5100) - -## Roadmap - -* FAQ + Home Assistant setup instructions -* Documentation of the library -* Unit tests -* Reduce flash memory usage -* Add support for HA lights - -## Unsupported features - -The library doesn't support all features of the MQTT integration. -If you need support for a new feature please open a new issue in the repository. - -# License - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License v3.0 as published by the Free Software Foundation. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/gpl.html diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/advanced-availability/advanced-availability.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/advanced-availability/advanced-availability.ino deleted file mode 100644 index 64df5bf..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/advanced-availability/advanced-availability.ino +++ /dev/null @@ -1,66 +0,0 @@ -#include -#include - -#define INPUT_PIN 9 -#define BROKER_ADDR IPAddress(192,168,0,17) - -byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; -unsigned long lastReadAt = millis(); -unsigned long lastAvailabilityToggleAt = millis(); -bool lastInputState = false; - -EthernetClient client; -HADevice device(mac, sizeof(mac)); -HAMqtt mqtt(client, device); - -// "input" is unique ID of the sensor. You should define you own ID. -// "door" is device class (based on the class HA displays different icons in the panel) -// "true" is initial state of the sensor. In this example it's "true" as we use pullup resistor -HABinarySensor sensor("input", "door", true); - -void setup() { - pinMode(INPUT_PIN, INPUT_PULLUP); - lastInputState = digitalRead(INPUT_PIN); - - // you don't need to verify return status - Ethernet.begin(mac); - - lastReadAt = millis(); - lastAvailabilityToggleAt = millis(); - - // set device's details (optional) - device.setName("Arduino"); - device.setSoftwareVersion("1.0.0"); - - sensor.setName("Door sensor"); // optional - - // This method enables availability for all device types registered on the device. - // For example, if you have 5 sensors on the same device, you can enable - // shared availability and change availability state of all sensors using - // single method call "device.setAvailability(false|true)" - device.enableSharedAvailability(); - - // Optionally, you can enable MQTT LWT feature. If device will lose connection - // to the broker, all device types related to it will be marked as offline in - // the Home Assistant Panel. - device.enableLastWill(); - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - Ethernet.maintain(); - mqtt.loop(); - - if ((millis() - lastReadAt) > 30) { // read in 30ms interval - // library produces MQTT message if a new state is different than the previous one - sensor.setState(digitalRead(INPUT_PIN)); - lastInputState = sensor.getState(); - lastReadAt = millis(); - } - - if ((millis() - lastAvailabilityToggleAt) > 5000) { - device.setAvailability(!device.isOnline()); - lastAvailabilityToggleAt = millis(); - } -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/availability/README.md b/firmware/uniclock/lib/arduino-home-assistant-main/examples/availability/README.md deleted file mode 100644 index 76f471d..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/availability/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Home Assistant availability example - -This example shows how to use "availability" feature of the Home Assistant. -It's supported only by following device types: -* Binary sensor -* Sensor -* Switch -* HVACs -* Fans - -## Initialization and usage - -Availability feature is turned off by default. In order to turn it on for the specific -sensor/switch you need to set initial state of the unit by calling `setAvailability` method. - -Example: -```cpp -... -HABinarySensor sensor("input", "door", true, mqtt); - -void setup() { - // ... - - // method must be called before `mqtt.begin(...)` - sensor.setAvailability(false); // offline - - mqtt.begin(BROKER_ADDR); -} -... -``` - -After availability is set for the very first time you can call `setAvailability` in any time. -Each time the method is called, the device will publish MQTT message to the broker. diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/availability/availability.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/availability/availability.ino deleted file mode 100644 index d3325ab..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/availability/availability.ino +++ /dev/null @@ -1,59 +0,0 @@ -#include -#include - -#define INPUT_PIN 9 -#define BROKER_ADDR IPAddress(192,168,0,17) - -byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; -unsigned long lastReadAt = millis(); -unsigned long lastAvailabilityToggleAt = millis(); -bool lastInputState = false; - -EthernetClient client; -HADevice device(mac, sizeof(mac)); -HAMqtt mqtt(client, device); - -// "input" is unique ID of the sensor. You should define you own ID. -// "door" is device class (based on the class HA displays different icons in the panel) -// "true" is initial state of the sensor. In this example it's "true" as we use pullup resistor -HABinarySensor sensor("input", "door", true); - -void setup() { - pinMode(INPUT_PIN, INPUT_PULLUP); - lastInputState = digitalRead(INPUT_PIN); - - // you don't need to verify return status - Ethernet.begin(mac); - - // turn on "availability" feature - // this method also sets initial availability so you can use "true" or "false" - sensor.setAvailability(false); - - lastReadAt = millis(); - lastAvailabilityToggleAt = millis(); - - // set device's details (optional) - device.setName("Arduino"); - device.setSoftwareVersion("1.0.0"); - - sensor.setName("Door sensor"); // optional - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - Ethernet.maintain(); - mqtt.loop(); - - if ((millis() - lastReadAt) > 30) { // read in 30ms interval - // library produces MQTT message if a new state is different than the previous one - sensor.setState(digitalRead(INPUT_PIN)); - lastInputState = sensor.getState(); - lastReadAt = millis(); - } - - if ((millis() - lastAvailabilityToggleAt) > 5000) { - sensor.setAvailability(!sensor.isOnline()); - lastAvailabilityToggleAt = millis(); - } -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/binary-sensor/binary-sensor.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/binary-sensor/binary-sensor.ino deleted file mode 100644 index 8b0862f..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/binary-sensor/binary-sensor.ino +++ /dev/null @@ -1,46 +0,0 @@ -#include -#include - -#define INPUT_PIN 9 -#define BROKER_ADDR IPAddress(192,168,0,17) - -byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; -unsigned long lastReadAt = millis(); -bool lastInputState = false; - -EthernetClient client; -HADevice device(mac, sizeof(mac)); -HAMqtt mqtt(client, device); - -// "input" is unique ID of the sensor. You should define you own ID. -// "door" is device class (based on the class HA displays different icons in the panel) -// "true" is initial state of the sensor. In this example it's "true" as we use pullup resistor -HABinarySensor sensor("input", "door", true); - -void setup() { - pinMode(INPUT_PIN, INPUT_PULLUP); - lastInputState = digitalRead(INPUT_PIN); - - // you don't need to verify return status - Ethernet.begin(mac); - - // set device's details (optional) - device.setName("Arduino"); - device.setSoftwareVersion("1.0.0"); - - sensor.setName("Door sensor"); // optional - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - Ethernet.maintain(); - mqtt.loop(); - - if ((millis() - lastReadAt) > 30) { // read in 30ms interval - // library produces MQTT message if a new state is different than the previous one - sensor.setState(digitalRead(INPUT_PIN)); - lastInputState = sensor.getState(); - lastReadAt = millis(); - } -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/cover/cover.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/cover/cover.ino deleted file mode 100644 index adfc920..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/cover/cover.ino +++ /dev/null @@ -1,49 +0,0 @@ -#include -#include - -#define BROKER_ADDR IPAddress(192,168,0,17) - -byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; - -EthernetClient client; -HADevice device(mac, sizeof(mac)); -HAMqtt mqtt(client, device); -HACover cover("my-cover"); // "my-cover" is unique ID of the cover. You should define your own ID. - -void onCoverCommand(HACover::CoverCommand cmd) { - if (cmd == HACover::CommandOpen) { - Serial.println("Command: Open"); - cover.setState(HACover::StateOpening); - } else if (cmd == HACover::CommandClose) { - Serial.println("Command: Close"); - cover.setState(HACover::StateClosing); - } else if (cmd == HACover::CommandStop) { - Serial.println("Command: Stop"); - cover.setState(HACover::StateStopped); - } - - // Available states: - // HACover::StateClosed - // HACover::StateClosing - // HACover::StateOpen - // HACover::StateOpening - // HACover::StateStopped - - // You can also report position using setPosition() method -} - -void setup() { - Serial.begin(9600); - Ethernet.begin(mac); - - cover.onCommand(onCoverCommand); - cover.setName("My cover"); // optional - // cover.setRetain(true); // optionally you can set retain flag - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - Ethernet.maintain(); - mqtt.loop(); -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/fan/fan.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/fan/fan.ino deleted file mode 100644 index dddfdcf..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/fan/fan.ino +++ /dev/null @@ -1,53 +0,0 @@ -#include -#include - -#define BROKER_ADDR IPAddress(192,168,0,17) - -byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; - -EthernetClient client; -HADevice device(mac, sizeof(mac)); -HAMqtt mqtt(client, device); - -// HAFan::SpeedsFeature enables support for setting different speeds of fan. -// You can skip this argument if you don't need speed management. -// "ventilation" is unique ID of the fan. You should define your own ID. -HAFan fan("ventilation", HAFan::SpeedsFeature); - -void onStateChanged(bool state) { - Serial.print("State: "); - Serial.println(state); -} - -void onSpeedChanged(uint16_t speed) { - Serial.print("Speed: "); - Serial.println(speed); -} - -void setup() { - Serial.begin(9600); - - // you don't need to verify return status - Ethernet.begin(mac); - - // set device's details (optional) - device.setName("Arduino"); - device.setSoftwareVersion("1.0.0"); - - // configure fan (optional) - fan.setName("Bathroom"); - fan.setRetain(true); - fan.setSpeedRangeMin(1); - fan.setSpeedRangeMax(100); - - // handle fan states - fan.onStateChanged(onStateChanged); - fan.onSpeedChanged(onSpeedChanged); - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - Ethernet.maintain(); - mqtt.loop(); -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/hvac/hvac.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/hvac/hvac.ino deleted file mode 100644 index 5bd2d90..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/hvac/hvac.ino +++ /dev/null @@ -1,102 +0,0 @@ -#include -#include - -#define BROKER_ADDR IPAddress(192,168,0,17) -#define WIFI_SSID "MyNetwork" -#define WIFI_PASSWORD "MyPassword" - -WiFiClient client; -HADevice device; -HAMqtt mqtt(client, device); - -// see src/device-types/HAHVAC.h header for more details -HAHVAC hvac("my_name", HAHVAC::AuxHeatingFeature | HAHVAC::AwayModeFeature | HAHVAC::HoldFeature); - -unsigned long lastTempPublishAt = 0; -double lastTemp = 0; - -void onAuxHeatingStateChanged(bool state) { - Serial.print("Aux heating: "); - Serial.println(state); -} - -void onAwayStateChanged(bool state) { - Serial.print("Away state: "); - Serial.println(state); -} - -void onHoldStateChanged(bool state) { - Serial.print("Hold state: "); - Serial.println(state); -} - -void onTargetTemperatureChanged(double temp) { - Serial.print("Target temperature: "); - Serial.println(temp); -} - -void onModeChanged(HAHVAC::Mode mode) { - Serial.print("Mode: "); - if (mode == HAHVAC::OffMode) { - Serial.println("off"); - } else if (mode == HAHVAC::AutoMode) { - Serial.println("auto"); - } else if (mode == HAHVAC::CoolMode) { - Serial.println("cool"); - } else if (mode == HAHVAC::HeatMode) { - Serial.println("heat"); - } else if (mode == HAHVAC::DryMode) { - Serial.println("dry"); - } else if (mode == HAHVAC::FanOnlyMode) { - Serial.println("fan only"); - } -} - -void setup() { - Serial.begin(9600); - Serial.println("Starting..."); - - // Unique ID must be set! - byte mac[WL_MAC_ADDR_LENGTH]; - WiFi.macAddress(mac); - device.setUniqueId(mac, sizeof(mac)); - - // connect to wifi - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - while (WiFi.status() != WL_CONNECTED) { - Serial.print("."); - delay(500); // waiting for the connection - } - Serial.println(); - Serial.println("Connected to the network"); - - // set device's details (optional) - device.setName("NodeMCU"); - device.setSoftwareVersion("1.0.0"); - - // assign callbacks (optional) - hvac.onAuxHeatingStateChanged(onAuxHeatingStateChanged); - hvac.onAwayStateChanged(onAwayStateChanged); - hvac.onHoldStateChanged(onHoldStateChanged); - hvac.onTargetTemperatureChanged(onTargetTemperatureChanged); - hvac.onModeChanged(onModeChanged); - - // configure HVAC (optional) - hvac.setName("My HVAC"); - hvac.setMinTemp(10); - hvac.setMaxTemp(30); - hvac.setTempStep(0.5); - hvac.setRetain(true); - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - mqtt.loop(); - - if ((millis() - lastTempPublishAt) > 3000) { - hvac.setCurrentTemperature(lastTemp); - lastTempPublishAt = millis(); - lastTemp += 0.5; - } -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/led-switch/led-switch.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/led-switch/led-switch.ino deleted file mode 100644 index 52b61c6..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/led-switch/led-switch.ino +++ /dev/null @@ -1,50 +0,0 @@ -#include -#include - -#define LED_PIN 9 -#define BROKER_ADDR IPAddress(192,168,0,17) - -byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; - -EthernetClient client; -HADevice device(mac, sizeof(mac)); -HAMqtt mqtt(client, device); -HASwitch led("led", false); // "led" is unique ID of the switch. You should define your own ID. - -void onBeforeSwitchStateChanged(bool state, HASwitch* s) -{ - // this callback will be called before publishing new state to HA - // in some cases there may be delay before onStateChanged is called due to network latency -} - -void onSwitchStateChanged(bool state, HASwitch* s) -{ - digitalWrite(LED_PIN, (state ? HIGH : LOW)); -} - -void setup() { - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - - // you don't need to verify return status - Ethernet.begin(mac); - - // set device's details (optional) - device.setName("Arduino"); - device.setSoftwareVersion("1.0.0"); - - // set icon (optional) - led.setIcon("mdi:lightbulb"); - led.setName("My LED"); - - // handle switch state - led.onBeforeStateChanged(onBeforeSwitchStateChanged); // optional - led.onStateChanged(onSwitchStateChanged); - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - Ethernet.maintain(); - mqtt.loop(); -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/mqtt-advanced/mqtt-advanced.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/mqtt-advanced/mqtt-advanced.ino deleted file mode 100644 index e2c7c62..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/mqtt-advanced/mqtt-advanced.ino +++ /dev/null @@ -1,56 +0,0 @@ -#include -#include - -#define BROKER_ADDR IPAddress(192,168,0,17) - -byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; - -EthernetClient client; -HADevice device(mac, sizeof(mac)); -HAMqtt mqtt(client, device); - -void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) { - // This callback is called when message from MQTT broker is received. - // Please note that you should always verify if the message's topic is the one you expect. - // For example: if (memcmp(topic, "myCustomTopic") == 0) { ... } - - Serial.print("New message on topic: "); - Serial.println(topic); - Serial.print("Data: "); - Serial.println((const char*)payload); - - mqtt.publish("myPublishTopic", "hello"); -} - -void onMqttConnected() { - Serial.println("Connected to the broker!"); - - // You can subscribe to custom topic if you need - mqtt.subscribe("myCustomTopic"); -} - -void onMqttConnectionFailed() { - Serial.println("Failed to connect to the broker!"); -} - -void setup() { - Serial.begin(9600); - Ethernet.begin(mac); - - mqtt.onMessage(onMqttMessage); - mqtt.onConnected(onMqttConnected); - mqtt.onConnectionFailed(onMqttConnectionFailed); - - // If you use custom discovery prefix you can change it as following: - // mqtt.setDiscoveryPrefix("customPrefix"); - - // If you want to change prefix only for non-discovery prefix: - // mqtt.setDataPrefix("data"); - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - Ethernet.maintain(); - mqtt.loop(); -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/mqtt-with-credentials/mqtt-with-credentials.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/mqtt-with-credentials/mqtt-with-credentials.ino deleted file mode 100644 index c4d5a92..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/mqtt-with-credentials/mqtt-with-credentials.ino +++ /dev/null @@ -1,42 +0,0 @@ -#include -#include - -#define LED_PIN 9 -#define BROKER_ADDR IPAddress(192,168,0,17) -#define BROKER_USERNAME "user" // replace with your credentials -#define BROKER_PASSWORD "pass" - -byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; - -EthernetClient client; -HADevice device(mac, sizeof(mac)); -HAMqtt mqtt(client, device); -HASwitch led("led", false); // "led" is unique ID of the switch. You should define your own ID. - -void onSwitchStateChanged(bool state, HASwitch* s) -{ - digitalWrite(LED_PIN, (state ? HIGH : LOW)); -} - -void setup() { - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - - // you don't need to verify return status - Ethernet.begin(mac); - - // set device's details (optional) - device.setName("Arduino"); - device.setSoftwareVersion("1.0.0"); - - // handle switch state - led.onStateChanged(onSwitchStateChanged); - led.setName("My LED"); // optional - - mqtt.begin(BROKER_ADDR, BROKER_USERNAME, BROKER_PASSWORD); -} - -void loop() { - Ethernet.maintain(); - mqtt.loop(); -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/multi-state-button/multi-state-button.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/multi-state-button/multi-state-button.ino deleted file mode 100644 index 219efef..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/multi-state-button/multi-state-button.ino +++ /dev/null @@ -1,52 +0,0 @@ -#include -#include - -// This example uses JC Button library -// https://github.com/JChristensen/JC_Button -#include - -#define BUTTON_PIN 9 -#define BUTTON_NAME "mybtn" -#define BROKER_ADDR IPAddress(192,168,0,17) - -byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; - -EthernetClient client; -HADevice device(mac, sizeof(mac)); -HAMqtt mqtt(client, device); -HATriggers triggers; -Button btn(BUTTON_PIN); -bool holdingBtn = false; - -void setup() { - // you don't need to verify return status - Ethernet.begin(mac); - - // set device's details (optional) - device.setName("Arduino"); - device.setSoftwareVersion("1.0.0"); - - // setup triggers - triggers.add("button_short_press", BUTTON_NAME); - triggers.add("button_long_press", BUTTON_NAME); - btn.begin(); - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - Ethernet.maintain(); - mqtt.loop(); - btn.read(); - - if (btn.pressedFor(3000) && !holdingBtn) { - triggers.trigger("button_long_press", BUTTON_NAME); - holdingBtn = true; - } else if (btn.wasReleased()) { - if (holdingBtn) { - holdingBtn = false; - } else { - triggers.trigger("button_short_press", BUTTON_NAME); - } - } -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/nano33iot/nano33iot.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/nano33iot/nano33iot.ino deleted file mode 100644 index 1156194..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/nano33iot/nano33iot.ino +++ /dev/null @@ -1,53 +0,0 @@ -#include -#include - -#define LED_PIN 9 -#define BROKER_ADDR IPAddress(192,168,0,17) -#define WIFI_SSID "MyNetwork" -#define WIFI_PASSWORD "MyPassword" - -WiFiClient client; -HADevice device; -HAMqtt mqtt(client, device); -HASwitch led("led", false); // "led" is unique ID of the switch. You should define your own ID. - -void onSwitchStateChanged(bool state, HASwitch* s) -{ - digitalWrite(LED_PIN, (state ? HIGH : LOW)); -} - -void setup() { - Serial.begin(9600); - Serial.println("Starting..."); - - // Unique ID must be set! - byte mac[WL_MAC_ADDR_LENGTH]; - WiFi.macAddress(mac); - device.setUniqueId(mac, sizeof(mac)); - - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - - // connect to wifi - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - while (WiFi.status() != WL_CONNECTED) { - Serial.print("."); - delay(500); // waiting for the connection - } - Serial.println(); - Serial.println("Connected to the network"); - - // set device's details (optional) - device.setName("Nano 33 IoT"); - device.setSoftwareVersion("1.0.0"); - - // handle switch state - led.onStateChanged(onSwitchStateChanged); - led.setName("My LED"); // optional - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - mqtt.loop(); -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/nodemcu/nodemcu.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/nodemcu/nodemcu.ino deleted file mode 100644 index 6a38d62..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/nodemcu/nodemcu.ino +++ /dev/null @@ -1,60 +0,0 @@ -#include -#include - -#define LED_PIN D0 -#define BROKER_ADDR IPAddress(192,168,0,17) -#define WIFI_SSID "MyNetwork" -#define WIFI_PASSWORD "MyPassword" - -WiFiClient client; -HADevice device; -HAMqtt mqtt(client, device); -HASwitch led("led", false); // "led" is unique ID of the switch. You should define your own ID. - -void onBeforeSwitchStateChanged(bool state, HASwitch* s) -{ - // this callback will be called before publishing new state to HA - // in some cases there may be delay before onStateChanged is called due to network latency -} - -void onSwitchStateChanged(bool state, HASwitch* s) -{ - digitalWrite(LED_PIN, (state ? HIGH : LOW)); -} - -void setup() { - Serial.begin(9600); - Serial.println("Starting..."); - - // Unique ID must be set! - byte mac[WL_MAC_ADDR_LENGTH]; - WiFi.macAddress(mac); - device.setUniqueId(mac, sizeof(mac)); - - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - - // connect to wifi - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - while (WiFi.status() != WL_CONNECTED) { - Serial.print("."); - delay(500); // waiting for the connection - } - Serial.println(); - Serial.println("Connected to the network"); - - // set device's details (optional) - device.setName("NodeMCU"); - device.setSoftwareVersion("1.0.0"); - - // handle switch state - led.onBeforeStateChanged(onBeforeSwitchStateChanged); // optional - led.onStateChanged(onSwitchStateChanged); - led.setName("My LED"); // optional - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - mqtt.loop(); -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/examples/sensor/sensor.ino b/firmware/uniclock/lib/arduino-home-assistant-main/examples/sensor/sensor.ino deleted file mode 100644 index 95c166b..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/examples/sensor/sensor.ino +++ /dev/null @@ -1,54 +0,0 @@ -#include -#include - -#define BROKER_ADDR IPAddress(192,168,0,17) - -byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; -unsigned long lastSentAt = millis(); -double lastValue = 0; - -EthernetClient client; -HADevice device(mac, sizeof(mac)); -HAMqtt mqtt(client, device); -HASensor temp("temp"); // "temp" is unique ID of the sensor. You should define your own ID. - -void onBeforeSwitchStateChanged(bool state, HASwitch* s) -{ - // this callback will be called before publishing new state to HA - // in some cases there may be delay before onStateChanged is called due to network latency -} - -void setup() { - // you don't need to verify return status - Ethernet.begin(mac); - - // set device's details (optional) - device.setName("Arduino"); - device.setSoftwareVersion("1.0.0"); - - // configure sensor (optional) - temp.setUnitOfMeasurement("°C"); - temp.setDeviceClass("temperature"); - temp.setIcon("mdi:home"); - temp.setName("Home temperature"); - - mqtt.begin(BROKER_ADDR); -} - -void loop() { - Ethernet.maintain(); - mqtt.loop(); - - if ((millis() - lastSentAt) >= 5000) { - lastSentAt = millis(); - lastValue = lastValue + 0.5; - temp.setValue(lastValue); - - // Supported data types: - // uint32_t (uint16_t, uint8_t) - // int32_t (int16_t, int8_t) - // double - // float - // const char* - } -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/ArduinoHA.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/ArduinoHA.h deleted file mode 100644 index 696ddec..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/ArduinoHA.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef AHA_ARDUINOHA_H -#define AHA_ARDUINOHA_H - -#include "HADevice.h" -#include "HAMqtt.h" -#include "HAUtils.h" -#include "device-types/HABinarySensor.h" -#include "device-types/HACover.h" -#include "device-types/HAFan.h" -#include "device-types/HAHVAC.h" -#include "device-types/HASensor.h" -#include "device-types/HASwitch.h" -#include "device-types/HATagScanner.h" -#include "device-types/HATriggers.h" - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/ArduinoHADefines.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/ArduinoHADefines.h deleted file mode 100644 index d0cf0cb..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/ArduinoHADefines.h +++ /dev/null @@ -1,23 +0,0 @@ -// Turns on debug information of the ArduinoHA core. -// Please note that you need to initialize serial interface manually -// by calling Serial.begin([baudRate]) before initializing ArduinoHA. -// #define ARDUINOHA_DEBUG - -// You can reduce Flash size of the compiled library by commenting unused components below -#define ARDUINOHA_BINARY_SENSOR -#define ARDUINOHA_COVER -#define ARDUINOHA_FAN -#define ARDUINOHA_HVAC -#define ARDUINOHA_SENSOR -#define ARDUINOHA_SWITCH -#define ARDUINOHA_TAG_SCANNER -#define ARDUINOHA_TRIGGERS - -#ifdef __GNUC__ -#define AHA_DEPRECATED(func) func __attribute__ ((deprecated)) -#elif defined(_MSC_VER) -#define AHA_DEPRECATED(func) __declspec(deprecated) func -#else -#warning "Arduino Home Assistant: You may miss deprecation warnings." -#define AHA_DEPRECATED(func) func -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/HADevice.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/HADevice.cpp deleted file mode 100644 index f7b57f0..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/HADevice.cpp +++ /dev/null @@ -1,198 +0,0 @@ -#include - -#include "ArduinoHADefines.h" -#include "HADevice.h" -#include "HAUtils.h" -#include "HAMqtt.h" -#include "device-types/DeviceTypeSerializer.h" - -#define HADEVICE_INIT \ - _manufacturer(nullptr), \ - _model(nullptr), \ - _name(nullptr), \ - _softwareVersion(nullptr), \ - _sharedAvailability(false), \ - _availabilityTopic(nullptr), \ - _available(true) // device will be available by default - -HADevice::HADevice() : - _uniqueId(nullptr), - HADEVICE_INIT -{ - -} - -HADevice::HADevice(const char* uniqueId) : - _uniqueId(uniqueId), - HADEVICE_INIT -{ - -} - -HADevice::HADevice(const byte* uniqueId, const uint16_t& length) : - _uniqueId(HAUtils::byteArrayToStr(uniqueId, length)), - HADEVICE_INIT -{ - -} - -void HADevice::setAvailability(bool online) -{ - _available = online; - publishAvailability(); -} - -bool HADevice::enableSharedAvailability() -{ - if (_sharedAvailability) { - return false; - } - -#if defined(ARDUINOHA_DEBUG) - Serial.println(F("Enabling shared availability in the device")); -#endif - - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - nullptr, - nullptr, - DeviceTypeSerializer::AvailabilityTopic - ); - if (topicSize == 0) { - return false; - } - - _availabilityTopic = (char*)malloc(topicSize); - _sharedAvailability = true; - - DeviceTypeSerializer::generateTopic( - _availabilityTopic, - nullptr, - nullptr, - DeviceTypeSerializer::AvailabilityTopic - ); - return true; -} - -bool HADevice::enableLastWill() -{ - HAMqtt* mqtt = HAMqtt::instance(); - if (mqtt == nullptr || _availabilityTopic == nullptr) { - return false; - } - - mqtt->setLastWill( - _availabilityTopic, - DeviceTypeSerializer::Offline, - false - ); - return true; -} - -bool HADevice::setUniqueId(const byte* uniqueId, const uint16_t& length) -{ - if (_uniqueId != nullptr) { - return false; - } - - _uniqueId = HAUtils::byteArrayToStr(uniqueId, length); - return true; -} - -void HADevice::publishAvailability() -{ - if (!_sharedAvailability) { - return; - } - - HAMqtt* mqtt = HAMqtt::instance(); - if (mqtt == nullptr) { - return; - } - - mqtt->publish( - _availabilityTopic, - ( - _available ? - DeviceTypeSerializer::Online : - DeviceTypeSerializer::Offline - ), - true - ); -} - -uint16_t HADevice::calculateSerializedLength() const -{ - uint16_t size = - 3 + // opening and closing bracket + null terminator - strlen(_uniqueId) + 8; // 8 - length of the JSON data for this field - - if (_manufacturer != nullptr) { - size += strlen(_manufacturer) + 8; // 8 - length of the JSON data for this field - } - - if (_model != nullptr) { - size += strlen(_model) + 9; // 9 - length of the JSON data for this field - } - - if (_name != nullptr) { - size += strlen(_name) + 10; // 10 - length of the JSON data for this field - } - - if (_softwareVersion != nullptr) { - size += strlen(_softwareVersion) + 8; // 8 - length of the JSON data for this field - } - - return size; -} - -uint16_t HADevice::serialize(char* dst) const -{ - static const char QuotationSign[] PROGMEM = {"\""}; - - { - static const char DataBefore[] PROGMEM = {"{\"ids\":\""}; - - strcpy_P(dst, DataBefore); - strcat(dst, _uniqueId); - strcat_P(dst, QuotationSign); - } - - if (_manufacturer != nullptr) { - static const char DataBefore[] PROGMEM = {",\"mf\":\""}; - - strcat_P(dst, DataBefore); - strcat(dst, _manufacturer); - strcat_P(dst, QuotationSign); - } - - if (_model != nullptr) { - static const char DataBefore[] PROGMEM = {",\"mdl\":\""}; - - strcat_P(dst, DataBefore); - strcat(dst, _model); - strcat_P(dst, QuotationSign); - } - - if (_name != nullptr) { - static const char DataBefore[] PROGMEM = {",\"name\":\""}; - - strcat_P(dst, DataBefore); - strcat(dst, _name); - strcat_P(dst, QuotationSign); - } - - if (_softwareVersion != nullptr) { - static const char DataBefore[] PROGMEM = {",\"sw\":\""}; - - strcat_P(dst, DataBefore); - strcat(dst, _softwareVersion); - strcat_P(dst, QuotationSign); - } - - { - static const char Data[] PROGMEM = {"}"}; - strcat_P(dst, Data); - } - - return strlen(dst) + 1; // size with null terminator -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/HADevice.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/HADevice.h deleted file mode 100644 index 0c5c2b4..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/HADevice.h +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef AHA_HADEVICE_H -#define AHA_HADEVICE_H - -#include - -class HADevice -{ -public: - HADevice(); - HADevice(const char* uniqueId); - HADevice(const byte* uniqueId, const uint16_t& length); - - inline const char* getUniqueId() const - { return _uniqueId; } - - inline void setManufacturer(const char* manufacturer) - { _manufacturer = manufacturer; } - - inline void setModel(const char* model) - { _model = model; } - - inline void setName(const char* name) - { _name = name; } - - inline void setSoftwareVersion(const char* softwareVersion) - { _softwareVersion = softwareVersion; } - - inline bool isSharedAvailabilityEnabled() const - { return _sharedAvailability; } - - inline bool isOnline() const - { return _available; } - - void setAvailability(bool online); - bool enableSharedAvailability(); - bool enableLastWill(); - bool setUniqueId(const byte* uniqueId, const uint16_t& length); - void publishAvailability(); - uint16_t calculateSerializedLength() const; - uint16_t serialize(char* dst) const; - -private: - const char* _uniqueId; - const char* _manufacturer; - const char* _model; - const char* _name; - const char* _softwareVersion; - bool _sharedAvailability; - char* _availabilityTopic; - bool _available; -}; - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/HAMqtt.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/HAMqtt.cpp deleted file mode 100644 index d2d5b4d..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/HAMqtt.cpp +++ /dev/null @@ -1,331 +0,0 @@ -#include - -#include "HAMqtt.h" -#include "HADevice.h" -#include "ArduinoHADefines.h" -#include "device-types/BaseDeviceType.h" - -static const char* DefaultDiscoveryPrefix = "esp"; -HAMqtt* HAMqtt::_instance = nullptr; - -void onMessageReceived(char* topic, uint8_t* payload, unsigned int length) -{ - if (HAMqtt::instance() == nullptr || length > UINT16_MAX) { - return; - } - - HAMqtt::instance()->processMessage(topic, payload, static_cast(length)); -} - -HAMqtt::HAMqtt(Client& netClient, HADevice& device) : - _netClient(netClient), - _device(device), - _messageCallback(nullptr), - _connectedCallback(nullptr), - _connectionFailedCallback(nullptr), - _initialized(false), - _discoveryPrefix(DefaultDiscoveryPrefix), - _dataPrefix(nullptr), - _mqtt(new PubSubClient(netClient)), - _username(nullptr), - _password(nullptr), - _lastConnectionAttemptAt(0), - _devicesTypesNb(0), - _devicesTypes(nullptr), - _lastWillTopic(nullptr), - _lastWillMessage(nullptr), - _lastWillRetain(false) -{ - _instance = this; -} - -bool HAMqtt::begin( - const IPAddress& serverIp, - const uint16_t& serverPort, - const char* username, - const char* password -) -{ -#if defined(ARDUINOHA_DEBUG) - Serial.println(F("Initializing ArduinoHA")); - Serial.print(F("Server address: ")); - Serial.print(serverIp); - Serial.print(F(":")); - Serial.print(serverPort); - Serial.println(); -#endif - - if (_device.getUniqueId() == nullptr) { -#if defined(ARDUINOHA_DEBUG) - Serial.println(F("Failed to initialize ArduinoHA. Missing device's unique ID.")); -#endif - - return false; - } - - if (_initialized) { -#if defined(ARDUINOHA_DEBUG) - Serial.println(F("ArduinoHA is already initialized")); -#endif - - return false; - } - - _username = username; - _password = password; - _initialized = true; - - _mqtt->setServer(serverIp, serverPort); - _mqtt->setCallback(onMessageReceived); - - return true; -} - -bool HAMqtt::begin( - const IPAddress& serverIp, - const char* username, - const char* password -) -{ - return begin(serverIp, HAMQTT_DEFAULT_PORT, username, password); -} - -bool HAMqtt::begin( - const char* hostname, - const uint16_t& serverPort, - const char* username, - const char* password -) -{ -#if defined(ARDUINOHA_DEBUG) - Serial.println(F("Initializing ArduinoHA")); - Serial.print(F("Server address: ")); - Serial.print(hostname); - Serial.print(F(":")); - Serial.print(serverPort); - Serial.println(); -#endif - - if (_device.getUniqueId() == nullptr) { -#if defined(ARDUINOHA_DEBUG) - Serial.println(F("Failed to initialize ArduinoHA. Missing device's unique ID.")); -#endif - - return false; - } - - if (_initialized) { -#if defined(ARDUINOHA_DEBUG) - Serial.println(F("ArduinoHA is already initialized")); -#endif - - return false; - } - - _username = username; - _password = password; - _initialized = true; - - _mqtt->setServer(hostname, serverPort); - _mqtt->setCallback(onMessageReceived); - - return true; -} - -bool HAMqtt::begin( - const char* hostname, - const char* username, - const char* password -) -{ - return begin(hostname, HAMQTT_DEFAULT_PORT, username, password); -} - -bool HAMqtt::disconnect(bool sendLastWill) -{ - if (!_initialized) { - return false; - } - -#if defined(ARDUINOHA_DEBUG) - Serial.println(F("Closing connection with MQTT broker")); -#endif - - if (sendLastWill && - _lastWillTopic != nullptr && - _lastWillMessage != nullptr) { - publish(_lastWillTopic, _lastWillMessage, _lastWillRetain); - } - - _initialized = false; - _lastConnectionAttemptAt = 0; - _mqtt->disconnect(); - - return true; -} - -void HAMqtt::loop() -{ - if (_initialized && !_mqtt->loop()) { - connectToServer(); - } -} - -bool HAMqtt::isConnected() -{ - return _mqtt->connected(); -} - -void HAMqtt::addDeviceType(BaseDeviceType* deviceType) -{ - BaseDeviceType** data = (BaseDeviceType**)realloc( - _devicesTypes, - sizeof(BaseDeviceType*) * (_devicesTypesNb + 1) - ); - - if (data != nullptr) { - _devicesTypes = data; - _devicesTypes[_devicesTypesNb] = deviceType; - _devicesTypesNb++; - } -} - -bool HAMqtt::publish(const char* topic, const char* payload, bool retained) -{ - if (!isConnected()) { - return false; - } - -#if defined(ARDUINOHA_DEBUG) - Serial.print(F("Publishing: ")); - Serial.print(topic); - Serial.print(F(", len: ")); - Serial.print(strlen(payload)); - Serial.println(); -#endif - - _mqtt->beginPublish(topic, strlen(payload), retained); - _mqtt->write((const uint8_t*)(payload), strlen(payload)); - return _mqtt->endPublish(); -} - -bool HAMqtt::beginPublish( - const char* topic, - uint16_t payloadLength, - bool retained -) -{ -#if defined(ARDUINOHA_DEBUG) - Serial.print(F("Publishing: ")); - Serial.print(topic); - Serial.print(F(", len: ")); - Serial.print(payloadLength); - Serial.println(); -#endif - - return _mqtt->beginPublish(topic, payloadLength, retained); -} - -bool HAMqtt::writePayload(const char* data, uint16_t length) -{ - return (_mqtt->write((const uint8_t*)(data), length) > 0); -} - -bool HAMqtt::writePayload_P(const char* src) -{ - char data[strlen_P(src) + 1]; - strcpy_P(data, src); - - return _mqtt->write((const uint8_t*)(data), strlen(data)); -} - -bool HAMqtt::endPublish() -{ - return _mqtt->endPublish(); -} - -bool HAMqtt::subscribe(const char* topic) -{ -#if defined(ARDUINOHA_DEBUG) - Serial.print(F("Subscribing topic: ")); - Serial.print(topic); - Serial.println(); -#endif - - return _mqtt->subscribe(topic); -} - -void HAMqtt::processMessage(char* topic, uint8_t* payload, uint16_t length) -{ -#if defined(ARDUINOHA_DEBUG) - Serial.print(F("Received message on topic: ")); - Serial.print(topic); - Serial.print(F(", payload length: ")); - Serial.print(length); - Serial.println(); -#endif - - if (_messageCallback) { - _messageCallback(topic, payload, length); - } - - for (uint8_t i = 0; i < _devicesTypesNb; i++) { - _devicesTypes[i]->onMqttMessage(topic, payload, length); - } -} - -void HAMqtt::connectToServer() -{ - if (_lastConnectionAttemptAt > 0 && - (millis() - _lastConnectionAttemptAt) < ReconnectInterval) { - return; - } - - _lastConnectionAttemptAt = millis(); - -#if defined(ARDUINOHA_DEBUG) - Serial.print(F("Connecting to the MQTT broker... Client ID: ")); - Serial.print(_device.getUniqueId()); - Serial.println(); -#endif - - _mqtt->connect( - _device.getUniqueId(), - _username, - _password, - _lastWillTopic, - 0, - _lastWillRetain, - _lastWillMessage, - true - ); - - if (isConnected()) { -#if defined(ARDUINOHA_DEBUG) - Serial.println(F("Connected to the broker")); -#endif - - onConnectedLogic(); - } else { -#if defined(ARDUINOHA_DEBUG) - Serial.println(F("Failed to connect to the broker")); - - if (_connectionFailedCallback) { - _connectionFailedCallback(); - } -#endif - } -} - -void HAMqtt::onConnectedLogic() -{ - if (_connectedCallback) { - _connectedCallback(); - } - - _device.publishAvailability(); - - for (uint8_t i = 0; i < _devicesTypesNb; i++) { - _devicesTypes[i]->onMqttConnected(); - } -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/HAMqtt.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/HAMqtt.h deleted file mode 100644 index 6f84a90..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/HAMqtt.h +++ /dev/null @@ -1,243 +0,0 @@ -#ifndef AHA_HAMQTT_H -#define AHA_HAMQTT_H - -#include -#include - -#define HAMQTT_CALLBACK(name) void (*name)() -#define HAMQTT_MESSAGE_CALLBACK(name) void (*name)(const char* topic, const uint8_t* payload, uint16_t length) -#define HAMQTT_DEFAULT_PORT 1883 - -class PubSubClient; -class HADevice; -class BaseDeviceType; - -class HAMqtt -{ -public: - static const uint16_t ReconnectInterval = 5000; // ms - - inline static HAMqtt* instance() - { return _instance; } - - HAMqtt(Client& netClient, HADevice& device); - - /** - * Sets prefix for Home Assistant discovery. - * It needs to match prefix set in the HA admin panel. - * The default prefix is "homeassistant". - * - * @param prefix - */ - inline void setDiscoveryPrefix(const char* prefix) - { _discoveryPrefix = prefix; } - - /** - * Returns discovery prefix. - */ - inline const char* getDiscoveryPrefix() const - { return _discoveryPrefix; } - - /** - * Sets prefix that will be used for topics different than discovery. - * It may be useful if you want to pass MQTT trafic through bridge. - * - * @param prefix - */ - inline void setDataPrefix(const char* prefix) - { _dataPrefix = prefix; } - - /** - * Returns data prefix. - */ - inline const char* getDataPrefix() const - { return _dataPrefix; } - - /** - * Returns instance of the device assigned to the HAMqtt class. - */ - inline HADevice const* getDevice() const - { return &_device; } - - /** - * Given callback will be called for each received message from the broker. - * - * @param callback - */ - inline void onMessage(HAMQTT_MESSAGE_CALLBACK(callback)) - { _messageCallback = callback; } - - /** - * Given callback will be called each time the connection with broker is acquired. - * - * @param callback - */ - inline void onConnected(HAMQTT_CALLBACK(callback)) - { _connectedCallback = callback; } - - /** - * Given callback will be called each time the library fails to connect to the broker. - * - * @param callback - */ - inline void onConnectionFailed(HAMQTT_CALLBACK(callback)) - { _connectionFailedCallback = callback; } - - /** - * Sets parameters of the connection to the MQTT broker. - * The library will try to connect to the broker in first loop cycle. - * Please note that the library automatically reconnects to the broker if connection is lost. - * - * @param serverIp IP address of the MQTT broker. - * @param serverPort Port of the MQTT broker. - * @param username Username for authentication. - * @param password Password for authentication. - */ - bool begin( - const IPAddress& serverIp, - const uint16_t& serverPort = HAMQTT_DEFAULT_PORT, - const char* username = nullptr, - const char* password = nullptr - ); - bool begin( - const IPAddress& serverIp, - const char* username, - const char* password - ); - - /** - * Connects to the MQTT broker using hostname. - * - * @param hostname Hostname of the MQTT broker. - * @param serverPort Port of the MQTT broker. - * @param username Username for authentication. - * @param password Password for authentication. - */ - bool begin( - const char* hostname, - const uint16_t& serverPort = HAMQTT_DEFAULT_PORT, - const char* username = nullptr, - const char* password = nullptr - ); - bool begin( - const char* hostname, - const char* username, - const char* password - ); - - /** - * Closes connection with the MQTT broker. - * - * @param sendLastWill Set to true if you want to publish device unavailability before closing the connection. - */ - bool disconnect(bool sendLastWill = true); - - /** - * ArduinoHA's ticker. - */ - void loop(); - - /** - * Returns true if connection to the MQTT broker is established. - */ - bool isConnected(); - - /** - * Adds a new device's type to the MQTT. - * Each time the connection with MQTT broker is acquired, the HAMqtt class - * calls "onMqttConnected" method in all devices' types instances. - * - * @param deviceType Instance of the device's type (eg. HATriggers). - */ - void addDeviceType(BaseDeviceType* deviceType); - - /** - * Publishes MQTT message with given topic and payload. - * Message won't be published if connection with MQTT broker is not established. - * In this case method returns false. - * - * @param topic Topic to publish. - * @param payload Payload to publish (it may be empty const char). - * @param retained Determines whether message should be retained. - */ - bool publish(const char* topic, const char* payload, bool retained = false); - - bool beginPublish(const char* topic, uint16_t payloadLength, bool retained = false); - bool writePayload(const char* data, uint16_t length); - bool writePayload_P(const char* src); - bool endPublish(); - - /** - * Subscribes to the given topic. - * Whenever a new message is received the onMqttMessage callback in all - * devices types is called. - * - * Please note that you need to subscribe topic each time the connection - * with the broker is acquired. - * - * @param topic Topic to subscribe - */ - bool subscribe(const char* topic); - - /** - * Enables last will message that will be produced when device disconnects from the broker. - * If you want to change availability of the device in Home Assistant panel - * please use enableLastWill() method in the HADevice object instead. - * - * @param lastWillTopic - * @param lastWillMessage - * @param lastWillRetain - */ - inline void setLastWill( - const char* lastWillTopic, - const char* lastWillMessage, - bool lastWillRetain - ) { - _lastWillTopic = lastWillTopic; - _lastWillMessage = lastWillMessage; - _lastWillRetain = lastWillRetain; - } - - /** - * Processes MQTT message received from the broker (subscription). - * - * @param topic Topic of the message. - * @param payload Content of the message. - * @param length Length of the message. - */ - void processMessage(char* topic, uint8_t* payload, uint16_t length); - -private: - static HAMqtt* _instance; - - /** - * Attempts to connect to the MQTT broker. - * The method uses properties passed to the "begin" method. - */ - void connectToServer(); - - /** - * This method is called each time the connection with MQTT broker is acquired. - */ - void onConnectedLogic(); - - Client& _netClient; - HADevice& _device; - HAMQTT_MESSAGE_CALLBACK(_messageCallback); - HAMQTT_CALLBACK(_connectedCallback); - HAMQTT_CALLBACK(_connectionFailedCallback); - bool _initialized; - const char* _discoveryPrefix; - const char* _dataPrefix; - PubSubClient* _mqtt; - const char* _username; - const char* _password; - uint32_t _lastConnectionAttemptAt; - uint8_t _devicesTypesNb; - BaseDeviceType** _devicesTypes; - const char* _lastWillTopic; - const char* _lastWillMessage; - bool _lastWillRetain; -}; - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/HAUtils.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/HAUtils.cpp deleted file mode 100644 index 5120048..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/HAUtils.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#ifdef ARDUINO_ARCH_SAMD -#include -#endif - -#include - -#include "HAUtils.h" - -bool HAUtils::endsWith(const char* str, const char* suffix) -{ - if (str == nullptr || suffix == nullptr) { - return false; - } - - const uint16_t& lenstr = strlen(str); - const uint16_t& lensuffix = strlen(suffix); - - if (lensuffix > lenstr) { - return false; - } - - return (strncmp(str + lenstr - lensuffix, suffix, lensuffix) == 0); -} - -void HAUtils::byteArrayToStr( - char* dst, - const byte* src, - const uint16_t& length -) -{ - const uint16_t& finalLength = (length * 2); - static const char map[] PROGMEM = {"0123456789abcdef"}; - - for (uint8_t i = 0; i < length; i++) { - dst[i*2] = pgm_read_byte(&map[((char)src[i] & 0XF0) >> 4]); - dst[i*2+1] = pgm_read_byte(&map[((char)src[i] & 0x0F)]); - } - - dst[finalLength] = '\0'; -} - -char* HAUtils::byteArrayToStr( - const byte* src, - const uint16_t& length -) -{ - char* dst = (char*)malloc((length * 2) + 1); // include null terminator - byteArrayToStr(dst, src, length); - - return dst; -} - -void HAUtils::tempToStr( - char* dst, - const double& temp -) -{ - memset(dst, 0, sizeof(AHA_SERIALIZED_TEMP_SIZE)); - dtostrf(temp, 0, 2, dst); -} - -double HAUtils::strToTemp( - const char* src -) -{ - return atof(src); -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/HAUtils.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/HAUtils.h deleted file mode 100644 index 39db0fa..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/HAUtils.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef AHA_HAUTILS_H -#define AHA_HAUTILS_H - -#include - -#define AHA_SERIALIZED_TEMP_SIZE 8 - -class HAUtils -{ -public: - static bool endsWith( - const char* str, - const char* suffix - ); - static void byteArrayToStr( - char* dst, - const byte* src, - const uint16_t& length - ); - static char* byteArrayToStr( - const byte* src, - const uint16_t& length - ); - static void tempToStr( - char* dst, - const double& temp - ); - static double strToTemp( - const char* src - ); -}; - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.cpp deleted file mode 100644 index 86756d2..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include "BaseDeviceType.h" -#include "../HAMqtt.h" -#include "../HADevice.h" -#include "../HAUtils.h" - -BaseDeviceType::BaseDeviceType( - const char* componentName, - const char* uniqueId -) : - _componentName(componentName), - _uniqueId(uniqueId), - _availability(AvailabilityDefault), - _name(nullptr) -{ - mqtt()->addDeviceType(this); -} - -BaseDeviceType::~BaseDeviceType() -{ - -} - -void BaseDeviceType::setAvailability(bool online) -{ - _availability = (online ? AvailabilityOnline : AvailabilityOffline); - publishAvailability(); -} - -HAMqtt* BaseDeviceType::mqtt() const -{ - return HAMqtt::instance(); -} - -void BaseDeviceType::onMqttMessage( - const char* topic, - const uint8_t* payload, - const uint16_t& length -) -{ - (void)topic; - (void)payload; - (void)length; -} - -void BaseDeviceType::publishConfig() -{ - const HADevice* device = mqtt()->getDevice(); - if (device == nullptr) { - return; - } - - const uint16_t& deviceLength = device->calculateSerializedLength(); - if (deviceLength == 0) { - return; - } - - char serializedDevice[deviceLength]; - if (device->serialize(serializedDevice) == 0) { - return; - } - - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::ConfigTopic, - true, - true - ); - const uint16_t& dataLength = calculateSerializedLength(serializedDevice); - - if (topicLength == 0 || dataLength == 0) { - return; - } - - char topic[topicLength]; - DeviceTypeSerializer::generateTopic( - topic, - componentName(), - uniqueId(), - DeviceTypeSerializer::ConfigTopic, - true - ); - - if (strlen(topic) == 0) { - return; - } - - if (mqtt()->beginPublish(topic, dataLength, true)) { - writeSerializedData(serializedDevice); - mqtt()->endPublish(); - } -} - -void BaseDeviceType::publishAvailability() -{ - const HADevice* device = HAMqtt::instance()->getDevice(); - if (device == nullptr) { - return; - } - - if (_availability == AvailabilityDefault || - !mqtt()->isConnected() || - strlen(uniqueId()) == 0 || - strlen(componentName()) == 0 || - device->isSharedAvailabilityEnabled()) { - return; - } - - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::AvailabilityTopic - ); - if (topicSize == 0) { - return; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - topic, - componentName(), - uniqueId(), - DeviceTypeSerializer::AvailabilityTopic - ); - - if (strlen(topic) == 0) { - return; - } - - mqtt()->publish( - topic, - ( - _availability == AvailabilityOnline ? - DeviceTypeSerializer::Online : - DeviceTypeSerializer::Offline - ), - true - ); -} - -bool BaseDeviceType::compareTopics(const char* topic, const char* expectedTopic) -{ - if (topic == nullptr || expectedTopic == nullptr) { - return false; - } - - if (strlen(uniqueId()) == 0) { - return false; - } - - static const char Slash[] PROGMEM = {"/"}; - - // unique ID + cmd topic + two slashes + null terminator - uint8_t suffixLength = strlen(uniqueId()) + strlen(expectedTopic) + 3; - char suffix[suffixLength]; - - strcpy_P(suffix, Slash); - strcat(suffix, uniqueId()); - strcat_P(suffix, Slash); - strcat(suffix, expectedTopic); - - return HAUtils::endsWith(topic, suffix); -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.h deleted file mode 100644 index a46a8d4..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.h +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef AHA_BASEDEVICETYPE_H -#define AHA_BASEDEVICETYPE_H - -#include - -#include "../ArduinoHADefines.h" -#include "DeviceTypeSerializer.h" - -class HAMqtt; - -class BaseDeviceType -{ -public: - BaseDeviceType( - const char* componentName, - const char* uniqueId - ); - virtual ~BaseDeviceType(); - - inline const char* uniqueId() const - { return _uniqueId; } - - inline const char* componentName() const - { return _componentName; } - - inline bool isAvailabilityConfigured() const - { return (_availability != AvailabilityDefault); } - - inline bool isOnline() const - { return (_availability == AvailabilityOnline); } - - inline void setName(const char* name) - { _name = name; } - - inline const char* getName() const - { return _name; } - - virtual void setAvailability(bool online); - -protected: - HAMqtt* mqtt() const; - - virtual void onMqttConnected() = 0; - virtual void onMqttMessage( - const char* topic, - const uint8_t* payload, - const uint16_t& length - ); - - virtual void publishConfig(); - virtual void publishAvailability(); - virtual bool compareTopics(const char* topic, const char* expectedTopic); - virtual uint16_t calculateSerializedLength(const char* serializedDevice) const = 0; - virtual bool writeSerializedData(const char* serializedDevice) const = 0; - - const char* const _componentName; - const char* const _uniqueId; - -private: - enum Availability { - AvailabilityDefault = 0, - AvailabilityOnline, - AvailabilityOffline - }; - - Availability _availability; - const char* _name; - - friend class HAMqtt; -}; - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.cpp deleted file mode 100644 index dc3fc3a..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.cpp +++ /dev/null @@ -1,421 +0,0 @@ -#include - -#include "DeviceTypeSerializer.h" -#include "BaseDeviceType.h" -#include "../HAMqtt.h" -#include "../HADevice.h" - -static const char CharSlash[] PROGMEM = {"/"}; -static const char CharUnderscore[] PROGMEM = {"_"}; -static const char CharQuotation[] PROGMEM = {"\""}; - -const char* DeviceTypeSerializer::ConfigTopic = "config"; -const char* DeviceTypeSerializer::EventTopic = "event"; -const char* DeviceTypeSerializer::AvailabilityTopic = "avail"; -const char* DeviceTypeSerializer::StateTopic = "state"; -const char* DeviceTypeSerializer::CommandTopic = "cmd"; -const char* DeviceTypeSerializer::Online = "online"; -const char* DeviceTypeSerializer::Offline = "offline"; -const char* DeviceTypeSerializer::StateOn = "ON"; -const char* DeviceTypeSerializer::StateOff = "OFF"; - -const char* DeviceTypeSerializer::getTopicPrefix(bool isDiscoveryTopic) -{ - if (!isDiscoveryTopic) { - const char* dataPrefix = HAMqtt::instance()->getDataPrefix(); - if (dataPrefix != nullptr) { - return dataPrefix; - } - } - - return HAMqtt::instance()->getDiscoveryPrefix(); -} - -uint16_t DeviceTypeSerializer::calculateTopicLength( - const char* component, - const char* objectId, - const char* suffix, - bool includeNullTerminator, - bool isDiscoveryTopic -) -{ - const char* prefix = getTopicPrefix(isDiscoveryTopic); - if (prefix == nullptr) { - return 0; - } - - uint16_t size = - strlen(prefix) + 1 + // prefix with slash - strlen(suffix); - - if (component != nullptr) { - size += strlen(component) + 1; // component with slash - } - - if (HAMqtt::instance()->getDevice() != nullptr) { - size += strlen(HAMqtt::instance()->getDevice()->getUniqueId()) + 1; // device ID with slash - } - - if (objectId != nullptr) { - size += strlen(objectId) + 1; // with slash - } - - if (includeNullTerminator) { - size += 1; - } - - return size; -} - -uint16_t DeviceTypeSerializer::generateTopic( - char* output, - const char* component, - const char* objectId, - const char* suffix, - bool isDiscoveryTopic -) -{ - const char* prefix = getTopicPrefix(isDiscoveryTopic); - if (prefix == nullptr) { - return 0; - } - - strcpy(output, prefix); - strcat_P(output, CharSlash); - - if (component != nullptr) { - strcat(output, component); - strcat_P(output, CharSlash); - } - - if (HAMqtt::instance()->getDevice() != nullptr) { - strcat(output, HAMqtt::instance()->getDevice()->getUniqueId()); - strcat_P(output, CharSlash); - } - - if (objectId != nullptr) { - strcat(output, objectId); - strcat_P(output, CharSlash); - } - - strcat(output, suffix); - return strlen(output) + 1; // size with null terminator -} - -uint16_t DeviceTypeSerializer::calculateBaseJsonDataSize() -{ - return 2; // opening and closing brackets of the JSON data -} - -uint16_t DeviceTypeSerializer::calculateNameFieldSize(const char* name) -{ - if (name == nullptr) { - return 0; - } - - // Field format: ,"name":"[NAME]" - return strlen(name) + 10; // 10 - length of the JSON decorators for this field -} - -uint16_t DeviceTypeSerializer::calculateUniqueIdFieldSize( - const char* uniqueId -) -{ - HADevice const* device = HAMqtt::instance()->getDevice(); - if (device == nullptr || uniqueId == nullptr) { - return 0; - } - - // Field format: ,"uniq_id":"[DEVICE ID]_[UNIQUE ID]" - return ( - strlen(device->getUniqueId()) + - strlen(uniqueId) + - 14 // 14 - length of the JSON decorators for this field - ); -} - -uint16_t DeviceTypeSerializer::calculateAvailabilityFieldSize( - const BaseDeviceType* const dt -) -{ - const HADevice* device = HAMqtt::instance()->getDevice(); - if (device == nullptr) { - return 0; - } - - const bool& sharedAvailability = device->isSharedAvailabilityEnabled(); - if (!sharedAvailability && !dt->isAvailabilityConfigured()) { - return 0; - } - - const uint16_t& availabilityTopicLength = calculateTopicLength( - (sharedAvailability ? nullptr : dt->componentName()), - (sharedAvailability ? nullptr : dt->uniqueId()), - AvailabilityTopic, - false - ); - - if (availabilityTopicLength == 0) { - return 0; - } - - // Field format: ,"avty_t":"[TOPIC]" - return availabilityTopicLength + 12; // 12 - length of the JSON decorators for this field -} - -uint16_t DeviceTypeSerializer::calculateRetainFieldSize(bool retain) -{ - if (!retain) { - return 0; - } - - // Field format: ,"ret":true - return 11; -} - -uint16_t DeviceTypeSerializer::calculateDeviceFieldSize( - const char* serializedDevice -) -{ - if (serializedDevice == nullptr) { - return 0; - } - - // Field format: ,"dev":[DEVICE] - return strlen(serializedDevice) + 7; // 7 - length of the JSON decorators for this field -} - -void DeviceTypeSerializer::mqttWriteBeginningJson() -{ - static const char Data[] PROGMEM = {"{"}; - HAMqtt::instance()->writePayload_P(Data); -} - -void DeviceTypeSerializer::mqttWriteEndJson() -{ - static const char Data[] PROGMEM = {"}"}; - HAMqtt::instance()->writePayload_P(Data); -} - -void DeviceTypeSerializer::mqttWriteConstCharField( - const char* prefix, - const char* value, - bool quoteSuffix -) -{ - if (prefix == nullptr || value == nullptr) { - return; - } - - HAMqtt::instance()->writePayload_P(prefix); - HAMqtt::instance()->writePayload(value, strlen(value)); - - if (quoteSuffix) { - HAMqtt::instance()->writePayload_P(CharQuotation); - } -} - -void DeviceTypeSerializer::mqttWriteNameField(const char* name) -{ - if (name == nullptr) { - return; - } - - static const char Prefix[] PROGMEM = {",\"name\":\""}; - mqttWriteConstCharField(Prefix, name); -} - -void DeviceTypeSerializer::mqttWriteUniqueIdField( - const char* uniqueId -) -{ - if (uniqueId == nullptr) { - return; - } - - HADevice const* device = HAMqtt::instance()->getDevice(); - if (device == nullptr) { - return; - } - - static const char Prefix[] PROGMEM = {",\"uniq_id\":\""}; - - uint8_t uniqueIdLength = strlen(uniqueId) + strlen(device->getUniqueId()) + 2; // underscore + null temrinator - char finalUniqueId[uniqueIdLength]; - strcpy(finalUniqueId, uniqueId); - strcat_P(finalUniqueId, CharUnderscore); - strcat(finalUniqueId, device->getUniqueId()); - - mqttWriteConstCharField(Prefix, finalUniqueId); -} - -void DeviceTypeSerializer::mqttWriteAvailabilityField( - const BaseDeviceType* const dt -) -{ - const HADevice* device = HAMqtt::instance()->getDevice(); - if (device == nullptr) { - return; - } - - const bool& sharedAvailability = device->isSharedAvailabilityEnabled(); - if (!sharedAvailability && !dt->isAvailabilityConfigured()) { - return; - } - - const uint16_t& topicSize = calculateTopicLength( - (sharedAvailability ? nullptr : dt->componentName()), - (sharedAvailability ? nullptr : dt->uniqueId()), - AvailabilityTopic - ); - if (topicSize == 0) { - return; - } - - char availabilityTopic[topicSize]; - generateTopic( - availabilityTopic, - (sharedAvailability ? nullptr : dt->componentName()), - (sharedAvailability ? nullptr : dt->uniqueId()), - AvailabilityTopic - ); - - if (strlen(availabilityTopic) == 0) { - return; - } - - static const char Prefix[] PROGMEM = {",\"avty_t\":\""}; - mqttWriteConstCharField(Prefix, availabilityTopic); -} - -void DeviceTypeSerializer::mqttWriteRetainField( - bool retain -) -{ - if (!retain) { - return; - } - - static const char Prefix[] PROGMEM = {",\"ret\":"}; - mqttWriteConstCharField( - Prefix, - "true", - false - ); -} - -void DeviceTypeSerializer::mqttWriteDeviceField( - const char* serializedDevice -) -{ - if (serializedDevice == nullptr) { - return; - } - - static const char Data[] PROGMEM = {",\"dev\":"}; - - HAMqtt::instance()->writePayload_P(Data); - HAMqtt::instance()->writePayload(serializedDevice, strlen(serializedDevice)); -} - -bool DeviceTypeSerializer::mqttWriteTopicField( - const BaseDeviceType* const dt, - const char* jsonPrefix, - const char* topicSuffix -) -{ - if (jsonPrefix == nullptr || topicSuffix == nullptr) { - return false; - } - - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - dt->componentName(), - dt->uniqueId() , - topicSuffix - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - topic, - dt->componentName(), - dt->uniqueId(), - topicSuffix - ); - - if (strlen(topic) == 0) { - return false; - } - - DeviceTypeSerializer::mqttWriteConstCharField(jsonPrefix, topic); - return true; -} - -bool DeviceTypeSerializer::mqttPublishMessage( - const BaseDeviceType* const dt, - const char* topicSuffix, - const char* data -) -{ - if (topicSuffix == nullptr || data == nullptr) { - return false; - } - - const uint16_t& topicSize = calculateTopicLength( - dt->componentName(), - dt->uniqueId(), - topicSuffix - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - generateTopic( - topic, - dt->componentName(), - dt->uniqueId(), - topicSuffix - ); - - if (strlen(topic) == 0) { - return false; - } - - return HAMqtt::instance()->publish( - topic, - data, - true - ); -} - -bool DeviceTypeSerializer::mqttSubscribeTopic( - const BaseDeviceType* const dt, - const char* topicSuffix -) -{ - const uint16_t& topicSize = calculateTopicLength( - dt->componentName(), - dt->uniqueId(), - topicSuffix - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - generateTopic( - topic, - dt->componentName(), - dt->uniqueId(), - topicSuffix - ); - - if (strlen(topic) == 0) { - return false; - } - - return HAMqtt::instance()->subscribe(topic); -} diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.h deleted file mode 100644 index d269b45..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.h +++ /dev/null @@ -1,117 +0,0 @@ -#ifndef AHA_DEVICETYPESERIALIZER_H -#define AHA_DEVICETYPESERIALIZER_H - -#include - -class HADevice; -class BaseDeviceType; - -class DeviceTypeSerializer -{ -public: - static const char* ConfigTopic; - static const char* EventTopic; - static const char* AvailabilityTopic; - static const char* StateTopic; - static const char* CommandTopic; - static const char* Online; - static const char* Offline; - static const char* StateOn; - static const char* StateOff; - - static const char* getTopicPrefix(bool isDiscoveryTopic); - - /** - * Calculates length of the topic with given parameters. - * Topic format: [prefix]/[component]/[deviceId]/[objectId]/[suffix] - * - * @param component - * @param objectId - * @param suffix - * @param includeNullTerminator - * @param isDiscoveryTopic Determines which prefix will be used for topic. - */ - static uint16_t calculateTopicLength( - const char* component, - const char* objectId, - const char* suffix, - bool includeNullTerminator = true, - bool isDiscoveryTopic = false - ); - - /** - * Generates topic and saves it to the given buffer. - * Please note that size of the buffer must be calculated by `calculateTopicLength` method first. - * Topic format: [prefix]/[component]/[deviceId]/[objectId]/[suffix] - * - * @param output - * @param component - * @param objectId - * @param suffix - * @param includeNullTerminator - * @param isDiscoveryTopic Determines which prefix will be used for topic. - */ - static uint16_t generateTopic( - char* output, - const char* component, - const char* objectId, - const char* suffix, - bool isDiscoveryTopic = false - ); - - static uint16_t calculateBaseJsonDataSize(); - static uint16_t calculateNameFieldSize( - const char* name - ); - static uint16_t calculateUniqueIdFieldSize( - const char* uniqueId - ); - static uint16_t calculateAvailabilityFieldSize( - const BaseDeviceType* const dt - ); - static uint16_t calculateRetainFieldSize( - bool retain - ); - static uint16_t calculateDeviceFieldSize( - const char* serializedDevice - ); - - static void mqttWriteBeginningJson(); - static void mqttWriteEndJson(); - static void mqttWriteConstCharField( - const char* prefix, - const char* value, - bool quoteSuffix = true - ); - static void mqttWriteNameField( - const char* name - ); - static void mqttWriteUniqueIdField( - const char* uniqueId - ); - static void mqttWriteAvailabilityField( - const BaseDeviceType* const dt - ); - static void mqttWriteRetainField( - bool retain - ); - static void mqttWriteDeviceField( - const char* serializedDevice - ); - static bool mqttWriteTopicField( - const BaseDeviceType* const dt, - const char* jsonPrefix, - const char* topicSuffix - ); - static bool mqttPublishMessage( - const BaseDeviceType* const dt, - const char* topicSuffix, - const char* data - ); - static bool mqttSubscribeTopic( - const BaseDeviceType* const dt, - const char* topicSuffix - ); -}; - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.cpp deleted file mode 100644 index ee7db85..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.cpp +++ /dev/null @@ -1,194 +0,0 @@ -#include "HABinarySensor.h" -#ifdef ARDUINOHA_BINARY_SENSOR - -#include "../ArduinoHADefines.h" -#include "../HAMqtt.h" -#include "../HADevice.h" - -HABinarySensor::HABinarySensor( - const char* uniqueId, - bool initialState -) : - BaseDeviceType("binary_sensor", uniqueId), - _class(nullptr), - _currentState(initialState) -{ - -} - -HABinarySensor::HABinarySensor( - const char* uniqueId, - bool initialState, - HAMqtt& mqtt -) : - HABinarySensor(uniqueId, initialState) -{ - (void)mqtt; -} - -HABinarySensor::HABinarySensor( - const char* uniqueId, - const char* deviceClass, - bool initialState -) : - BaseDeviceType("binary_sensor", uniqueId), - _class(deviceClass), - _currentState(initialState) -{ - -} - -HABinarySensor::HABinarySensor( - const char* uniqueId, - const char* deviceClass, - bool initialState, - HAMqtt& mqtt -) : - HABinarySensor(uniqueId, deviceClass, initialState) -{ - (void)mqtt; -} - -void HABinarySensor::onMqttConnected() -{ - if (strlen(uniqueId()) == 0) { - return; - } - - publishConfig(); - publishState(_currentState); - publishAvailability(); -} - -bool HABinarySensor::setState(bool state) -{ - if (state == _currentState) { - return true; - } - - if (publishState(state)) { - _currentState = state; - return true; - } - - return false; -} - -bool HABinarySensor::publishState(bool state) -{ - if (strlen(uniqueId()) == 0) { - return false; - } - - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::StateTopic - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - topic, - componentName(), - uniqueId(), - DeviceTypeSerializer::StateTopic - ); - - if (strlen(topic) == 0) { - return false; - } - - return mqtt()->publish( - topic, - ( - state ? - DeviceTypeSerializer::StateOn : - DeviceTypeSerializer::StateOff - ), - true - ); -} - -uint16_t HABinarySensor::calculateSerializedLength( - const char* serializedDevice -) const -{ - if (serializedDevice == nullptr) { - return 0; - } - - const HADevice* device = mqtt()->getDevice(); - if (device == nullptr) { - return 0; - } - - uint16_t size = 0; - size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateNameFieldSize(getName()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); - size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); - - // state topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::StateTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: "stat_t":"[TOPIC]" - size += topicLength + 11; // 11 - length of the JSON decorators for this field - } - - // device class - if (_class != nullptr) { - // Field format: ,"dev_cla":"[CLASS]" - size += strlen(_class) + 13; // 13 - length of the JSON decorators for this field - } - - return size; // exludes null terminator -} - -bool HABinarySensor::writeSerializedData(const char* serializedDevice) const -{ - if (serializedDevice == nullptr) { - return false; - } - - DeviceTypeSerializer::mqttWriteBeginningJson(); - - // state topic - { - static const char Prefix[] PROGMEM = {"\"stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - DeviceTypeSerializer::StateTopic - ); - } - - // device class - if (_class != nullptr) { - static const char Prefix[] PROGMEM = {",\"dev_cla\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(Prefix, _class); - } - - DeviceTypeSerializer::mqttWriteNameField(getName()); - DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); - DeviceTypeSerializer::mqttWriteAvailabilityField(this); - DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); - DeviceTypeSerializer::mqttWriteEndJson(); - - return true; -} - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.h deleted file mode 100644 index c886518..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.h +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef AHA_HABINARYSENSOR_H -#define AHA_HABINARYSENSOR_H - -#include "BaseDeviceType.h" - -#ifdef ARDUINOHA_BINARY_SENSOR - -class HABinarySensor : public BaseDeviceType -{ -public: - /** - * Initializes binary sensor. - * - * @param uniqueId Unique ID of the sensor. Recommended characters: [a-z0-9\-_] - * @param initialState Initial state of the sensor. - It will be published right after "config" message in order to update HA state. - */ - HABinarySensor( - const char* uniqueId, - bool initialState - ); - HABinarySensor( - const char* uniqueId, - bool initialState, - HAMqtt& mqtt - ); // legacy constructor - - /** - * Initializes binary sensor with the specified class. - * You can find list of available values here: https://www.home-assistant.io/integrations/binary_sensor/#device-class - * - * @param uniqueId Unique ID of the sensor. Recommendes characters: [a-z0-9\-_] - * @param deviceClass Name of the class (lower case). - * @param initialState Initial state of the sensor. - It will be published right after "config" message in order to update HA state. - */ - HABinarySensor( - const char* uniqueId, - const char* deviceClass, - bool initialState - ); - HABinarySensor( - const char* uniqueId, - const char* deviceClass, - bool initialState, - HAMqtt& mqtt - ); - - /** - * Publishes configuration of the sensor to the MQTT. - */ - virtual void onMqttConnected() override; - - /** - * Changes state of the sensor and publishes MQTT message. - * Please note that if a new value is the same as previous one, - * the MQTT message won't be published. - * - * @param state New state of the sensor. - * @returns Returns true if MQTT message has been published successfully. - */ - bool setState(bool state); - - /** - * Returns last known state of the sensor. - * If setState method wasn't called the initial value will be returned. - */ - inline bool getState() const - { return _currentState; } - -private: - bool publishState(bool state); - uint16_t calculateSerializedLength(const char* serializedDevice) const override; - bool writeSerializedData(const char* serializedDevice) const override; - - const char* _class; - bool _currentState; -}; - -#endif -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HACover.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HACover.cpp deleted file mode 100644 index 48d6270..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HACover.cpp +++ /dev/null @@ -1,286 +0,0 @@ -#include "HACover.h" -#ifdef ARDUINOHA_COVER - -#include "../HAMqtt.h" -#include "../HADevice.h" - -static const char ClosedStateStr[] PROGMEM = {"closed"}; -static const char ClosingStateStr[] PROGMEM = {"closing"}; -static const char OpenStateStr[] PROGMEM = {"open"}; -static const char OpeningStateStr[] PROGMEM = {"opening"}; -static const char StoppedStateStr[] PROGMEM = {"stopped"}; -static const char OpenCommandStr[] PROGMEM = {"OPEN"}; -static const char CloseCommandStr[] PROGMEM = {"CLOSE"}; -static const char StopCommandStr[] PROGMEM = {"STOP"}; - -const char* HACover::PositionTopic = "ps"; - -HACover::HACover(const char* uniqueId) : - BaseDeviceType("cover", uniqueId), - _commandCallback(nullptr), - _currentState(StateUnknown), - _currentPosition(0), - _retain(false) -{ - -} - -HACover::HACover(const char* uniqueId, HAMqtt& mqtt) : - HACover(uniqueId) -{ - (void)mqtt; -} - -void HACover::onMqttConnected() -{ - if (strlen(uniqueId()) == 0) { - return; - } - - publishConfig(); - publishAvailability(); - - DeviceTypeSerializer::mqttSubscribeTopic( - this, - DeviceTypeSerializer::CommandTopic - ); - - if (!_retain) { - publishState(_currentState); - publishPosition(_currentPosition); - } -} - -void HACover::onMqttMessage( - const char* topic, - const uint8_t* payload, - const uint16_t& length -) -{ - (void)payload; - - if (compareTopics(topic, DeviceTypeSerializer::CommandTopic)) { - char cmd[length + 1]; - memset(cmd, 0, sizeof(cmd)); - memcpy(cmd, payload, length); - handleCommand(cmd); - } -} - -bool HACover::setState(CoverState state, bool force) -{ - if (!force && _currentState == state) { - return true; - } - - if (publishState(state)) { - _currentState = state; - return true; - } - - return false; -} - -bool HACover::setPosition(int16_t position) -{ - if (_currentPosition == position) { - return true; - } - - if (publishPosition(position)) { - _currentPosition = position; - return true; - } - - return false; -} - -bool HACover::publishState(CoverState state) -{ - if (strlen(uniqueId()) == 0 || state == StateUnknown) { - return false; - } - - char stateStr[8]; - switch (state) { - case StateClosed: - strcpy_P(stateStr, ClosedStateStr); - break; - - case StateClosing: - strcpy_P(stateStr, ClosingStateStr); - break; - - case StateOpen: - strcpy_P(stateStr, OpenStateStr); - break; - - case StateOpening: - strcpy_P(stateStr, OpeningStateStr); - break; - - case StateStopped: - strcpy_P(stateStr, StoppedStateStr); - break; - - default: - return false; - } - - return DeviceTypeSerializer::mqttPublishMessage( - this, - DeviceTypeSerializer::StateTopic, - stateStr - ); -} - -bool HACover::publishPosition(int16_t position) -{ - if (strlen(uniqueId()) == 0) { - return false; - } - - uint8_t digitsNb = floor(log10(position)) + 1; - char str[digitsNb + 2]; // + null terminator + negative sign - memset(str, 0, sizeof(str)); - itoa(position, str, 10); - - return DeviceTypeSerializer::mqttPublishMessage( - this, - PositionTopic, - str - ); -} - -uint16_t HACover::calculateSerializedLength(const char* serializedDevice) const -{ - if (serializedDevice == nullptr) { - return 0; - } - - uint16_t size = 0; - size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateNameFieldSize(getName()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); - size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); - size += DeviceTypeSerializer::calculateRetainFieldSize(_retain); - - // command topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::CommandTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: "cmd_t":"[TOPIC]" - size += topicLength + 10; // 10 - length of the JSON decorators for this field - } - - // state topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::StateTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"stat_t":"[TOPIC]" - size += topicLength + 12; // 12 - length of the JSON decorators for this field - } - - // position topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - PositionTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"pos_t":"[TOPIC]" - size += topicLength + 11; // 11 - length of the JSON decorators for this field - } - - return size; // exludes null terminator -} - -bool HACover::writeSerializedData(const char* serializedDevice) const -{ - if (serializedDevice == nullptr) { - return false; - } - - DeviceTypeSerializer::mqttWriteBeginningJson(); - - // command topic - { - static const char Prefix[] PROGMEM = {"\"cmd_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - DeviceTypeSerializer::CommandTopic - ); - } - - // state topic - { - static const char Prefix[] PROGMEM = {",\"stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - DeviceTypeSerializer::StateTopic - ); - } - - // position topic - { - static const char Prefix[] PROGMEM = {",\"pos_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - PositionTopic - ); - } - - DeviceTypeSerializer::mqttWriteNameField(getName()); - DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); - DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); - DeviceTypeSerializer::mqttWriteAvailabilityField(this); - DeviceTypeSerializer::mqttWriteRetainField(_retain); - DeviceTypeSerializer::mqttWriteEndJson(); - - return true; -} - -void HACover::handleCommand(const char* cmd) -{ - if (!_commandCallback) { - return; - } - - if (strcmp_P(cmd, CloseCommandStr) == 0) { - _commandCallback(CommandClose); - } else if (strcmp_P(cmd, OpenCommandStr) == 0) { - _commandCallback(CommandOpen); - } else if (strcmp_P(cmd, StopCommandStr) == 0) { - _commandCallback(CommandStop); - } -} - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HACover.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HACover.h deleted file mode 100644 index 056da73..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HACover.h +++ /dev/null @@ -1,113 +0,0 @@ -#ifndef AHA_COVER_H -#define AHA_COVER_H - -#include "BaseDeviceType.h" - -#ifdef ARDUINOHA_COVER - -#define HACOVER_CALLBACK(name) void (*name)(CoverCommand cmd) - -class HACover : public BaseDeviceType -{ -public: - static const char* PositionTopic; - - enum CoverState { - StateUnknown = 0, - StateClosed, - StateClosing, - StateOpen, - StateOpening, - StateStopped - }; - - enum CoverCommand { - CommandOpen, - CommandClose, - CommandStop - }; - - HACover(const char* uniqueId); - HACover(const char* uniqueId, HAMqtt& mqtt); // legacy constructor - - virtual void onMqttConnected() override; - virtual void onMqttMessage( - const char* topic, - const uint8_t* payload, - const uint16_t& length - ) override; - - /** - * Changes state of the cover and publishes MQTT message. - * Please note that if a new value is the same as previous one, - * the MQTT message won't be published. - * - * @param state New state of the cover. - * @param force Forces to update state without comparing it to previous known state. - * @returns Returns true if MQTT message has been published successfully. - */ - bool setState(CoverState state, bool force = false); - - /** - * Sets current state of the cover without pushing the state to Home Assistant. - * This method may be useful if you want to change state before connection - * with MQTT broker is acquired. - * - * @param state New state of the cover. - */ - inline void setCurrentState(CoverState state) - { _currentState = state; } - - /** - * Changes position of the cover and publishes MQTT message. - * Please note that if a new value is the same as previous one, - * the MQTT message won't be published. - * - * @param position New position of the cover. - * @returns Returns true if MQTT message has been published successfully. - */ - bool setPosition(int16_t position); - - /** - * Sets current position of the cover without pushing the value to Home Assistant. - * This method may be useful if you want to change position before connection - * with MQTT broker is acquired. - * - * @param position New position of the cover. - */ - inline void setCurrentPosition(int16_t position) - { _currentPosition = position; } - - /** - * Sets `retain` flag for commands published by Home Assistant. - * By default it's set to false. - * - * @param retain - */ - inline void setRetain(bool retain) - { _retain = retain; } - - /** - * Registers callback that will be called each time the command from HA is received. - * Please note that it's not possible to register multiple callbacks for the same covers. - * - * @param callback - */ - inline void onCommand(HACOVER_CALLBACK(callback)) - { _commandCallback = callback; } - -protected: - bool publishState(CoverState state); - bool publishPosition(int16_t position); - uint16_t calculateSerializedLength(const char* serializedDevice) const override; - bool writeSerializedData(const char* serializedDevice) const override; - void handleCommand(const char* cmd); - - HACOVER_CALLBACK(_commandCallback); - CoverState _currentState; - int16_t _currentPosition; - bool _retain; -}; - -#endif -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAFan.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAFan.cpp deleted file mode 100644 index 47995e7..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAFan.cpp +++ /dev/null @@ -1,343 +0,0 @@ -#include "HAFan.h" -#ifdef ARDUINOHA_FAN - -#include "../HAMqtt.h" -#include "../HADevice.h" - -const char* HAFan::PercentageCommandTopic = {"sct"}; -const char* HAFan::PercentageStateTopic = {"sst"}; - -HAFan::HAFan(const char* uniqueId, uint8_t features) : - BaseDeviceType("fan", uniqueId), - _features(features), - _currentState(false), - _stateCallback(nullptr), - _currentSpeed(0), - _speedCallback(nullptr), - _retain(false), - _speedRangeMin(1), - _speedRangeMax(100) -{ - -} - -HAFan::HAFan(const char* uniqueId, uint8_t features, HAMqtt& mqtt) : - HAFan(uniqueId, features) -{ - (void)mqtt; -} - -void HAFan::onMqttConnected() -{ - if (strlen(uniqueId()) == 0) { - return; - } - - publishConfig(); - publishAvailability(); - - DeviceTypeSerializer::mqttSubscribeTopic( - this, - DeviceTypeSerializer::CommandTopic - ); - - if (_features & SpeedsFeature) { - DeviceTypeSerializer::mqttSubscribeTopic( - this, - PercentageCommandTopic - ); - } - - if (!_retain) { - publishState(_currentState); - publishSpeed(_currentSpeed); - } -} - -void HAFan::onMqttMessage( - const char* topic, - const uint8_t* payload, - const uint16_t& length -) -{ - (void)payload; - - if (compareTopics(topic, DeviceTypeSerializer::CommandTopic)) { - bool state = (length == strlen(DeviceTypeSerializer::StateOn)); - setState(state, true); - } else if (compareTopics(topic, PercentageCommandTopic)) { - char speedStr[length + 1]; - memset(speedStr, 0, sizeof(speedStr)); - memcpy(speedStr, payload, length); - int32_t speed = atoi(speedStr); - if (speed >= 0) { - setSpeed(speed); - } - } -} - -bool HAFan::setState(bool state, bool force) -{ - if (!force && _currentState == state) { - return true; - } - - if (publishState(state)) { - _currentState = state; - - if (_stateCallback) { - _stateCallback(state); - } - - return true; - } - - return false; -} - -bool HAFan::setSpeed(uint16_t speed) -{ - if (publishSpeed(speed)) { - _currentSpeed = speed; - - if (_speedCallback) { - _speedCallback(_currentSpeed); - } - - return true; - } - - return false; -} - -bool HAFan::publishState(bool state) -{ - if (strlen(uniqueId()) == 0) { - return false; - } - - return DeviceTypeSerializer::mqttPublishMessage( - this, - DeviceTypeSerializer::StateTopic, - ( - state ? - DeviceTypeSerializer::StateOn : - DeviceTypeSerializer::StateOff - ) - ); -} - -bool HAFan::publishSpeed(uint16_t speed) -{ - if (strlen(uniqueId()) == 0) { - return false; - } - - uint8_t digitsNb = floor(log10(speed)) + 1; - char str[digitsNb + 1]; // + null terminator - memset(str, 0, sizeof(str)); - itoa(speed, str, 10); - - return DeviceTypeSerializer::mqttPublishMessage( - this, - PercentageStateTopic, - str - ); -} - -uint16_t HAFan::calculateSerializedLength(const char* serializedDevice) const -{ - if (serializedDevice == nullptr) { - return 0; - } - - uint16_t size = 0; - size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateNameFieldSize(getName()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); - size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); - size += DeviceTypeSerializer::calculateRetainFieldSize(_retain); - - // command topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::CommandTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: "cmd_t":"[TOPIC]" - size += topicLength + 10; // 10 - length of the JSON decorators for this field - } - - // state topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::StateTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"stat_t":"[TOPIC]" - size += topicLength + 12; // 12 - length of the JSON decorators for this field - } - - // speeds - if (_features & SpeedsFeature) { - // percentage command topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - PercentageCommandTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"pct_cmd_t":"[TOPIC]" - size += topicLength + 15; // 15 - length of the JSON decorators for this field - } - - // percentage state topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - PercentageStateTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"pct_stat_t":"[TOPIC]" - size += topicLength + 16; // 16 - length of the JSON decorators for this field - } - - // speed range min - if (_speedRangeMin != 1) { - uint8_t digitsNb = floor(log10(_speedRangeMin)) + 1; - - // Field format: ,"spd_rng_min":[VALUE] - size += digitsNb + 15; // 15 - length of the JSON decorators for this field - } - - // speed range max - if (_speedRangeMax != 100) { - uint8_t digitsNb = floor(log10(_speedRangeMax)) + 1; - - // Field format: ,"spd_rng_max":[VALUE] - size += digitsNb + 15; // 15 - length of the JSON decorators for this field - } - } - - return size; // excludes null terminator -} - -bool HAFan::writeSerializedData(const char* serializedDevice) const -{ - if (serializedDevice == nullptr) { - return false; - } - - DeviceTypeSerializer::mqttWriteBeginningJson(); - - // command topic - { - static const char Prefix[] PROGMEM = {"\"cmd_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - DeviceTypeSerializer::CommandTopic - ); - } - - // state topic - { - static const char Prefix[] PROGMEM = {",\"stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - DeviceTypeSerializer::StateTopic - ); - } - - // speeds - if (_features & SpeedsFeature) { - // percentage command topic - { - static const char Prefix[] PROGMEM = {",\"pct_cmd_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - PercentageCommandTopic - ); - } - - // percentage state topic - { - static const char Prefix[] PROGMEM = {",\"pct_stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - PercentageStateTopic - ); - } - - // speed range min - if (_speedRangeMin != 1) { - uint8_t digitsNb = floor(log10(_speedRangeMin)) + 1; - char str[digitsNb + 1]; // + null terminator - memset(str, 0, sizeof(str)); - itoa(_speedRangeMin, str, 10); - - static const char Prefix[] PROGMEM = {",\"spd_rng_min\":"}; - DeviceTypeSerializer::mqttWriteConstCharField( - Prefix, - str, - false - ); - } - - // speed range max - if (_speedRangeMax != 100) { - uint8_t digitsNb = floor(log10(_speedRangeMax)) + 1; - char str[digitsNb + 1]; // + null terminator - memset(str, 0, sizeof(str)); - itoa(_speedRangeMax, str, 10); - - static const char Prefix[] PROGMEM = {",\"spd_rng_max\":"}; - DeviceTypeSerializer::mqttWriteConstCharField( - Prefix, - str, - false - ); - } - } - - DeviceTypeSerializer::mqttWriteNameField(getName()); - DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); - DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); - DeviceTypeSerializer::mqttWriteAvailabilityField(this); - DeviceTypeSerializer::mqttWriteRetainField(_retain); - DeviceTypeSerializer::mqttWriteEndJson(); - - return true; -} - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAFan.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAFan.h deleted file mode 100644 index 76685ea..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAFan.h +++ /dev/null @@ -1,156 +0,0 @@ -#ifndef AHA_FAN_H -#define AHA_FAN_H - -#include "BaseDeviceType.h" - -#ifdef ARDUINOHA_FAN - -#define HAFAN_STATE_CALLBACK_BOOL(name) void (*name)(bool) -#define HAFAN_STATE_CALLBACK_SPEED(name) void (*name)(uint16_t) -#define HAFAN_STATE_CALLBACK_SPEED_DEPRECATED(name) void (*name)(Speed) - -class HAFan : public BaseDeviceType -{ -public: - static const char* PercentageCommandTopic; - static const char* PercentageStateTopic; - - enum Features { - DefaultFeatures = 0, - SpeedsFeature = 1 - }; - - // @deprecated - enum Speed { - UnknownSpeed = 0, - OffSpeed = 1, - LowSpeed = 2, - MediumSpeed = 4, - HighSpeed = 8 - }; - - HAFan(const char* uniqueId, uint8_t features = DefaultFeatures); - HAFan(const char* uniqueId, uint8_t features, HAMqtt& mqtt); // legacy constructor - - virtual void onMqttConnected() override; - virtual void onMqttMessage( - const char* topic, - const uint8_t* payload, - const uint16_t& length - ) override; - - /** - * Changes state of the fan and publishes MQTT message. - * Please note that if a new value is the same as previous one, - * the MQTT message won't be published. - * - * @param state New state of the fan (on - true, off - false). - * @param force Forces to update state without comparing it to previous known state. - * @returns Returns true if MQTT message has been published successfully. - */ - bool setState(bool state, bool force = false); - - /** - * Alias for setState(true). - */ - inline bool turnOn() - { return setState(true); } - - /** - * Alias for setState(false). - */ - inline bool turnOff() - { return setState(false); } - - /** - * Returns last known state of the fan. - * If setState method wasn't called the initial value will be returned. - */ - inline bool getState() const - { return _currentState; } - - /** - * Registers callback that will be called each time the state of the fan changes. - * Please note that it's not possible to register multiple callbacks for the same fan. - * - * @param callback - */ - inline void onStateChanged(HAFAN_STATE_CALLBACK_BOOL(callback)) - { _stateCallback = callback; } - - /** - * Sets the list of supported fan's speeds. - * - * @param speeds - */ - AHA_DEPRECATED(inline void setSpeeds(uint8_t speeds)) - { (void)speeds; } - - /** - * Sets speed of the fan. - * - * @param speed - */ - bool setSpeed(uint16_t speed); - - /** - * Returns current speed of the fan. - */ - inline uint16_t getSpeed() const - { return _currentSpeed; } - - /** - * Registers callback that will be called each time the speed of the fan changes. - * Please note that it's not possible to register multiple callbacks for the same fan. - * - * @param callback - */ - inline void onSpeedChanged(HAFAN_STATE_CALLBACK_SPEED(callback)) - { _speedCallback = callback; } - - AHA_DEPRECATED(inline void onSpeedChanged(HAFAN_STATE_CALLBACK_SPEED_DEPRECATED(callback))) - { (void)callback; } - - /** - * Sets `retain` flag for commands published by Home Assistant. - * By default it's set to false. - * - * @param retain - */ - inline void setRetain(bool retain) - { _retain = retain; } - - /** - * Sets minimum range for slider in the HA panel. - * - * @param min - */ - inline void setSpeedRangeMin(uint16_t min) - { _speedRangeMin = min; } - - /** - * Sets maximum range for slider in the HA panel. - * - * @param min - */ - inline void setSpeedRangeMax(uint16_t max) - { _speedRangeMax = max; } - -protected: - bool publishState(bool state); - bool publishSpeed(uint16_t speed); - uint16_t calculateSerializedLength(const char* serializedDevice) const override; - bool writeSerializedData(const char* serializedDevice) const override; - - const uint8_t _features; - bool _currentState; - HAFAN_STATE_CALLBACK_BOOL(_stateCallback); - uint16_t _currentSpeed; - HAFAN_STATE_CALLBACK_SPEED(_speedCallback); - bool _retain; - uint16_t _speedRangeMin; - uint16_t _speedRangeMax; -}; - -#endif -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAHVAC.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAHVAC.cpp deleted file mode 100644 index aadc25d..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAHVAC.cpp +++ /dev/null @@ -1,1078 +0,0 @@ -#include "HAHVAC.h" -#ifdef ARDUINOHA_HVAC - -#include "../ArduinoHADefines.h" -#include "../HAMqtt.h" -#include "../HADevice.h" -#include "../HAUtils.h" - -const char* HAHVAC::ActionTopic = "at"; -const char* HAHVAC::AuxCommandTopic = "act"; -const char* HAHVAC::AuxStateTopic = "ast"; -const char* HAHVAC::AwayCommandTopic = "amct"; -const char* HAHVAC::AwayStateTopic = "amst"; -const char* HAHVAC::HoldCommandTopic = "hct"; -const char* HAHVAC::HoldStateTopic = "hst"; -const char* HAHVAC::TargetTemperatureCommandTopic = "ttct"; -const char* HAHVAC::TargetTemperatureStateTopic = "ttst"; -const char* HAHVAC::CurrentTemperatureTopic = "ctt"; -const char* HAHVAC::ModeCommandTopic = "mct"; -const char* HAHVAC::ModeStateTopic = "mst"; - -static const char OffStr[] PROGMEM = {"off"}; -static const char AutoStr[] PROGMEM = {"auto"}; -static const char CoolStr[] PROGMEM = {"cool"}; -static const char HeatStr[] PROGMEM = {"heat"}; -static const char DryStr[] PROGMEM = {"dry"}; -static const char FanOnlyStr[] PROGMEM = {"fan_only"}; - -HAHVAC::HAHVAC(const char* uniqueId, uint8_t features) : - BaseDeviceType("climate", uniqueId), - _uniqueId(uniqueId), - _features(features), - _temperatureUnit(DefaultUnit), - _action(OffAction), - _auxHeatingCallback(nullptr), - _auxHeatingState(false), - _awayCallback(nullptr), - _awayState(false), - _holdCallback(nullptr), - _holdState(false), - _currentTemperature(__DBL_MAX__), - _minTemp(__DBL_MAX__), - _maxTemp(__DBL_MAX__), - _tempStep(1), - _targetTempCallback(nullptr), - _targetTemperature(__DBL_MAX__), - _modes( - OffMode | - AutoMode | - CoolMode | - HeatMode | - DryMode | - FanOnlyMode - ), - _modeChangedCallback(nullptr), - _currentMode(Mode::UnknownMode), - _retain(false) -{ - -} - -HAHVAC::HAHVAC( - const char* uniqueId, - uint8_t features, - HAMqtt& mqtt -) : - HAHVAC(uniqueId, features) -{ - (void)mqtt; -} - -void HAHVAC::onMqttConnected() -{ - if (strlen(uniqueId()) == 0) { - return; - } - - publishConfig(); - publishCurrentTemperature(_currentTemperature); - publishTargetTemperature(_targetTemperature); - - if (!_retain) { - publishAction(_action); - publishAuxHeatingState(_auxHeatingState); - publishAwayState(_awayState); - publishHoldState(_holdState); - publishMode(_currentMode); - } - - subscribeTopics(); -} - -void HAHVAC::onMqttMessage( - const char* topic, - const uint8_t* payload, - const uint16_t& length -) -{ - if ((_features & AuxHeatingFeature) && - compareTopics(topic, AuxCommandTopic)) { - bool state = (length == strlen(DeviceTypeSerializer::StateOn)); - setAuxHeatingState(state); - } else if ((_features & AwayModeFeature) && - compareTopics(topic, AwayCommandTopic)) { - bool state = (length == strlen(DeviceTypeSerializer::StateOn)); - setAwayState(state); - } else if ((_features & HoldFeature) && - compareTopics(topic, HoldCommandTopic)) { - bool state = (length == strlen(DeviceTypeSerializer::StateOn)); - setHoldState(state); - } else if (compareTopics(topic, TargetTemperatureCommandTopic)) { - char src[length + 1]; - memset(src, 0, sizeof(src)); - memcpy(src, payload, length); - - setTargetTemperature(HAUtils::strToTemp(src)); - } else if (compareTopics(topic, ModeCommandTopic)) { - char mode[length + 1]; - memset(mode, 0, sizeof(mode)); - memcpy(mode, payload, length); - - setModeFromStr(mode); - } -} - -bool HAHVAC::setAction(Action action) -{ - if (publishAction(action)) { - _action = action; - return true; - } - - return false; -} - -bool HAHVAC::setAuxHeatingState(bool state) -{ - if (publishAuxHeatingState(state)) { - _auxHeatingState = state; - - if (_auxHeatingCallback) { - _auxHeatingCallback(_auxHeatingState); - } - - return true; - } - - return false; -} - -bool HAHVAC::setAwayState(bool state) -{ - if (publishAwayState(state)) { - _awayState = state; - - if (_awayCallback) { - _awayCallback(_awayState); - } - - return true; - } - - return false; -} - -bool HAHVAC::setHoldState(bool state) -{ - if (publishHoldState(state)) { - _holdState = state; - - if (_holdCallback) { - _holdCallback(_holdState); - } - - return true; - } - - return false; -} - -bool HAHVAC::setTargetTemperature(double temperature) -{ - if (publishTargetTemperature(temperature)) { - _targetTemperature = temperature; - - if (_targetTempCallback) { - _targetTempCallback(_targetTemperature); - } - - return true; - } - - return false; -} - -bool HAHVAC::setCurrentTemperature(double temperature) -{ - if (_currentTemperature == temperature) { - return true; - } - - if (publishCurrentTemperature(temperature)) { - _currentTemperature = temperature; - return true; - } - - return false; -} - -bool HAHVAC::setMode(Mode mode) -{ - if (mode == UnknownMode) { - return false; - } - - if (publishMode(mode)) { - _currentMode = mode; - - if (_modeChangedCallback) { - _modeChangedCallback(_currentMode); - } - - return true; - } - - return false; -} - -bool HAHVAC::setModeFromStr(const char* mode) -{ - if (strcmp_P(mode, OffStr) == 0) { - return setMode(OffMode); - } else if (strcmp_P(mode, AutoStr) == 0) { - return setMode(AutoMode); - } else if (strcmp_P(mode, CoolStr) == 0) { - return setMode(CoolMode); - } else if (strcmp_P(mode, HeatStr) == 0) { - return setMode(HeatMode); - } else if (strcmp_P(mode, DryStr) == 0) { - return setMode(DryMode); - } else if (strcmp_P(mode, FanOnlyStr) == 0) { - return setMode(FanOnlyMode); - } - - return false; -} - -bool HAHVAC::setMinTemp(double minTemp) -{ - if (minTemp == __DBL_MAX__) { - return false; - } - - _minTemp = minTemp; - return true; -} - -bool HAHVAC::setMaxTemp(double maxTemp) -{ - if (maxTemp == __DBL_MAX__) { - return false; - } - - _maxTemp = maxTemp; - return true; -} - -bool HAHVAC::setTempStep(double tempStep) -{ - if (tempStep <= 0 || tempStep >= 255) { - return false; - } - - _tempStep = tempStep; - return true; -} - -bool HAHVAC::publishAction(Action action) -{ - if (!(_features & ActionFeature) || - strlen(uniqueId()) == 0) { - return false; - } - - char actionStr[8]; - switch (action) { - case OffAction: - strcpy_P(actionStr, OffStr); - break; - - case HeatingAction: - strcpy(actionStr, "heating"); - break; - - case CoolingAction: - strcpy(actionStr, "cooling"); - break; - - case DryingAction: - strcpy(actionStr, "drying"); - break; - - case IdleAction: - strcpy(actionStr, "idle"); - break; - - case FanAction: - strcpy(actionStr, "fan"); - break; - - default: - return false; - } - - return DeviceTypeSerializer::mqttPublishMessage( - this, - ActionTopic, - actionStr - ); -} - -bool HAHVAC::publishAuxHeatingState(bool state) -{ - if (!(_features & AuxHeatingFeature) || - strlen(uniqueId()) == 0) { - return false; - } - - return DeviceTypeSerializer::mqttPublishMessage( - this, - AuxStateTopic, - ( - state ? - DeviceTypeSerializer::StateOn : - DeviceTypeSerializer::StateOff - ) - ); -} - -bool HAHVAC::publishAwayState(bool state) -{ - if (!(_features & AwayModeFeature) || - strlen(uniqueId()) == 0) { - return false; - } - - return DeviceTypeSerializer::mqttPublishMessage( - this, - AwayStateTopic, - ( - state ? - DeviceTypeSerializer::StateOn : - DeviceTypeSerializer::StateOff - ) - ); -} - -bool HAHVAC::publishHoldState(bool state) -{ - if (!(_features & HoldFeature) || - strlen(uniqueId()) == 0) { - return false; - } - - return DeviceTypeSerializer::mqttPublishMessage( - this, - HoldStateTopic, - ( - state ? - DeviceTypeSerializer::StateOn : - DeviceTypeSerializer::StateOff - ) - ); -} - -bool HAHVAC::publishCurrentTemperature(double temperature) -{ - if (strlen(uniqueId()) == 0) { - return false; - } - - if (temperature == __DBL_MAX__) { - return false; - } - - char str[AHA_SERIALIZED_TEMP_SIZE]; - HAUtils::tempToStr(str, temperature); - - return DeviceTypeSerializer::mqttPublishMessage( - this, - CurrentTemperatureTopic, - str - ); -} - -bool HAHVAC::publishTargetTemperature(double temperature) -{ - if (strlen(uniqueId()) == 0) { - return false; - } - - if (temperature == __DBL_MAX__) { - return false; - } - - char str[AHA_SERIALIZED_TEMP_SIZE]; - HAUtils::tempToStr(str, temperature); - - return DeviceTypeSerializer::mqttPublishMessage( - this, - TargetTemperatureStateTopic, - str - ); -} - -bool HAHVAC::publishMode(Mode mode) -{ - if (strlen(uniqueId()) == 0 || mode == UnknownMode) { - return false; - } - - char modeStr[9]; - switch (mode) { - case OffMode: - strcpy_P(modeStr, OffStr); - break; - - case AutoMode: - strcpy_P(modeStr, AutoStr); - break; - - case CoolMode: - strcpy_P(modeStr, CoolStr); - break; - - case HeatMode: - strcpy_P(modeStr, HeatStr); - break; - - case DryMode: - strcpy_P(modeStr, DryStr); - break; - - case FanOnlyMode: - strcpy_P(modeStr, FanOnlyStr); - break; - - default: - return false; - } - - return DeviceTypeSerializer::mqttPublishMessage( - this, - ModeStateTopic, - modeStr - ); -} - -void HAHVAC::subscribeTopics() -{ - // aux heating - if (_features & AuxHeatingFeature) { - DeviceTypeSerializer::mqttSubscribeTopic( - this, - AuxCommandTopic - ); - } - - // away mode - if (_features & AwayModeFeature) { - DeviceTypeSerializer::mqttSubscribeTopic( - this, - AwayCommandTopic - ); - } - - // hold - if (_features & HoldFeature) { - DeviceTypeSerializer::mqttSubscribeTopic( - this, - HoldCommandTopic - ); - } - - // target temperature - DeviceTypeSerializer::mqttSubscribeTopic( - this, - TargetTemperatureCommandTopic - ); - - // mode - DeviceTypeSerializer::mqttSubscribeTopic( - this, - ModeCommandTopic - ); -} - -uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const -{ - if (serializedDevice == nullptr || _uniqueId == nullptr) { - return 0; - } - - const HADevice* device = mqtt()->getDevice(); - if (device == nullptr) { - return 0; - } - - uint16_t size = 0; - size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateNameFieldSize(getName()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); - size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); - size += DeviceTypeSerializer::calculateRetainFieldSize(_retain); - - // current temperature - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - CurrentTemperatureTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: "curr_temp_t":"[TOPIC]" - size += topicLength + 16; // 16 - length of the JSON decorators for this field - } - - // action topic - if (_features & ActionFeature) { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - ActionTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"act_t":"[TOPIC]" - size += topicLength + 11; // 11 - length of the JSON decorators for this field - } - - // aux heating - if (_features & AuxHeatingFeature) { - // command topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - AuxCommandTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"aux_cmd_t":"[TOPIC]" - size += topicLength + 15; // 15 - length of the JSON decorators for this field - } - - // state topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - AuxStateTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"aux_stat_t":"[TOPIC]" - size += topicLength + 16; // 16 - length of the JSON decorators for this field - } - } - - // away mode - if (_features & AwayModeFeature) { - // command topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - AwayCommandTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"away_mode_cmd_t":"[TOPIC]" - size += topicLength + 21; // 21 - length of the JSON decorators for this field - } - - // state topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - AwayStateTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"away_mode_stat_t":"[TOPIC]" - size += topicLength + 22; // 22 - length of the JSON decorators for this field - } - } - - // hold - if (_features & HoldFeature) { - // command topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - HoldCommandTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"hold_cmd_t":"[TOPIC]" - size += topicLength + 16; // 16 - length of the JSON decorators for this field - } - - // state topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - HoldStateTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"hold_stat_t":"[TOPIC]" - size += topicLength + 17; // 17 - length of the JSON decorators for this field - } - } - - // min temp - if (_minTemp != __DBL_MAX__) { - char str[AHA_SERIALIZED_TEMP_SIZE]; - HAUtils::tempToStr(str, _minTemp); - - // Field format: ,"min_temp":[TEMP] - size += strlen(str) + 12; // 12 - length of the JSON decorators for this field - } - - // max temp - if (_maxTemp != __DBL_MAX__) { - char str[AHA_SERIALIZED_TEMP_SIZE]; - HAUtils::tempToStr(str, _maxTemp); - - // Field format: ,"max_temp":[TEMP] - size += strlen(str) + 12; // 12 - length of the JSON decorators for this field - } - - // temp step - { - char str[AHA_SERIALIZED_TEMP_SIZE]; - HAUtils::tempToStr(str, _tempStep); - - // Field format: ,"temp_step":[TEMP] - size += strlen(str) + 13; // 13 - length of the JSON decorators for this field - } - - // target temperature - { - // command topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - TargetTemperatureCommandTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"temp_cmd_t":"[TOPIC]" - size += topicLength + 16; // 16 - length of the JSON decorators for this field - } - - // state topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - TargetTemperatureStateTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"temp_stat_t":"[TOPIC]" - size += topicLength + 17; // 17 - length of the JSON decorators for this field - } - } - - // temperature unit - if (_temperatureUnit != DefaultUnit) { - // Field format: ,"temp_unit":"[UNIT]" - // UNIT may C or F - size += 15 + 1; // 15 - length of the JSON decorators for this field - } - - // modes - if (_modes != 0) { - // command topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - ModeCommandTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"mode_cmd_t":"[TOPIC]" - size += topicLength + 16; // 16 - length of the JSON decorators for this field - } - - // state topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - ModeStateTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"mode_stat_t":"[TOPIC]" - size += topicLength + 17; // 17 - length of the JSON decorators for this field - } - } - - // Supported modes - // Field format: ,"modes":MODES - size += 9 + calculateModesLength(); // 9 - length of the JSON decorators for this field - - return size; // excludes null terminator -} - -uint16_t HAHVAC::calculateModesLength() const -{ - uint16_t length = 2; // opening and closing bracket - - if (_modes & OffMode) { - // escape + comma - length += 3 + strlen_P(OffStr); - } - - if (_modes & AutoMode) { - // escape + comma - length += 3 + strlen_P(AutoStr); - } - - if (_modes & CoolMode) { - // escape + comma - length += 3 + strlen_P(CoolStr); - } - - if (_modes & HeatMode) { - // escape + comma - length += 3 + strlen_P(HeatStr); - } - - if (_modes & DryMode) { - // escape + comma - length += 3 + strlen_P(DryStr); - } - - if (_modes & FanOnlyMode) { - // escape + comma - length += 3 + strlen_P(FanOnlyStr); - } - - if (length > 2) { - length--; // remove trailing comma - } - - return length; // excludes null terminator -} - -bool HAHVAC::writeSerializedData(const char* serializedDevice) const -{ - if (serializedDevice == nullptr || _uniqueId == nullptr) { - return false; - } - - DeviceTypeSerializer::mqttWriteBeginningJson(); - - // current temperature topic - { - static const char Prefix[] PROGMEM = {"\"curr_temp_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - CurrentTemperatureTopic - ); - } - - // action topic - if (_features & ActionFeature) { - static const char Prefix[] PROGMEM = {",\"act_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - ActionTopic - ); - } - - // aux heating - if (_features & AuxHeatingFeature) { - // command topic - { - static const char Prefix[] PROGMEM = {",\"aux_cmd_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - AuxCommandTopic - ); - } - - // state topic - { - static const char Prefix[] PROGMEM = {",\"aux_stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - AuxStateTopic - ); - } - } - - // away mode - if (_features & AwayModeFeature) { - // command topic - { - static const char Prefix[] PROGMEM = {",\"away_mode_cmd_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - AwayCommandTopic - ); - } - - // state topic - { - static const char Prefix[] PROGMEM = {",\"away_mode_stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - AwayStateTopic - ); - } - } - - // hold - if (_features & HoldFeature) { - // command topic - { - static const char Prefix[] PROGMEM = {",\"hold_cmd_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - HoldCommandTopic - ); - } - - // state topic - { - static const char Prefix[] PROGMEM = {",\"hold_stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - HoldStateTopic - ); - } - } - - // min temp - if (_minTemp != __DBL_MAX__) { - char str[AHA_SERIALIZED_TEMP_SIZE]; - HAUtils::tempToStr(str, _minTemp); - - static const char Prefix[] PROGMEM = {",\"min_temp\":"}; - DeviceTypeSerializer::mqttWriteConstCharField( - Prefix, - str, - false - ); - } - - // max temp - if (_maxTemp != __DBL_MAX__) { - char str[AHA_SERIALIZED_TEMP_SIZE]; - HAUtils::tempToStr(str, _maxTemp); - - static const char Prefix[] PROGMEM = {",\"max_temp\":"}; - DeviceTypeSerializer::mqttWriteConstCharField( - Prefix, - str, - false - ); - } - - // temp step - { - char str[AHA_SERIALIZED_TEMP_SIZE]; - HAUtils::tempToStr(str, _tempStep); - - static const char Prefix[] PROGMEM = {",\"temp_step\":"}; - DeviceTypeSerializer::mqttWriteConstCharField( - Prefix, - str, - false - ); - } - - // target temperature - { - // command topic - { - static const char Prefix[] PROGMEM = {",\"temp_cmd_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - TargetTemperatureCommandTopic - ); - } - - // state topic - { - static const char Prefix[] PROGMEM = {",\"temp_stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - TargetTemperatureStateTopic - ); - } - } - - // temperature unit - if (_temperatureUnit != DefaultUnit) { - static const char Prefix[] PROGMEM = {",\"temp_unit\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField( - Prefix, - (_temperatureUnit == CelsiusUnit ? "C" : "F") - ); - } - - // modes - if (_modes != 0) { - // command topic - { - static const char Prefix[] PROGMEM = {",\"mode_cmd_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - ModeCommandTopic - ); - } - - // state topic - { - static const char Prefix[] PROGMEM = {",\"mode_stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - ModeStateTopic - ); - } - } - - // supported modes - { - static const char CharQuotation[] PROGMEM = {"\""}; - static const char CharQuotationComma[] PROGMEM = {"\","}; - static const char Prefix[] PROGMEM = {",\"modes\":"}; - - char modesStr[calculateModesLength() + 1]; // plus null terminator - memset(modesStr, 0, sizeof(modesStr)); - strcpy(modesStr, "["); - - if (_modes != 0) { - if (_modes & OffMode) { - strcat_P(modesStr, CharQuotation); - strcat_P(modesStr, OffStr); - strcat_P(modesStr, CharQuotationComma); - } - - if (_modes & AutoMode) { - strcat_P(modesStr, CharQuotation); - strcat_P(modesStr, AutoStr); - strcat_P(modesStr, CharQuotationComma); - } - - if (_modes & CoolMode) { - strcat_P(modesStr, CharQuotation); - strcat_P(modesStr, CoolStr); - strcat_P(modesStr, CharQuotationComma); - } - - if (_modes & HeatMode) { - strcat_P(modesStr, CharQuotation); - strcat_P(modesStr, HeatStr); - strcat_P(modesStr, CharQuotationComma); - } - - if (_modes & DryMode) { - strcat_P(modesStr, CharQuotation); - strcat_P(modesStr, DryStr); - strcat_P(modesStr, CharQuotationComma); - } - - if (_modes & FanOnlyMode) { - strcat_P(modesStr, CharQuotation); - strcat_P(modesStr, FanOnlyStr); - strcat_P(modesStr, CharQuotationComma); - } - - modesStr[strlen(modesStr) - 1] = ']'; - } else { - strcat(modesStr, "]"); - } - - DeviceTypeSerializer::mqttWriteConstCharField( - Prefix, - modesStr, - false - ); - } - - DeviceTypeSerializer::mqttWriteNameField(getName()); - DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); - DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); - DeviceTypeSerializer::mqttWriteAvailabilityField(this); - DeviceTypeSerializer::mqttWriteRetainField(_retain); - DeviceTypeSerializer::mqttWriteEndJson(); - - return true; -} - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAHVAC.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAHVAC.h deleted file mode 100644 index 44c4ecb..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HAHVAC.h +++ /dev/null @@ -1,336 +0,0 @@ -#ifndef AHA_HVAC_H -#define AHA_HVAC_H - -#include "BaseDeviceType.h" - -#ifdef ARDUINOHA_HVAC - -#define HAHVAC_STATE_CALLBACK_BOOL(name) void (*name)(bool) -#define HAHVAC_STATE_CALLBACK_DOUBLE(name) void (*name)(double) -#define HAHVAC_STATE_CALLBACK_MODE(name) void (*name)(HAHVAC::Mode) - -class HAHVAC : public BaseDeviceType -{ -public: - static const char* ActionTopic; - static const char* AuxCommandTopic; - static const char* AuxStateTopic; - static const char* AwayCommandTopic; - static const char* AwayStateTopic; - static const char* HoldCommandTopic; - static const char* HoldStateTopic; - static const char* TargetTemperatureCommandTopic; - static const char* TargetTemperatureStateTopic; - static const char* CurrentTemperatureTopic; - static const char* ModeCommandTopic; - static const char* ModeStateTopic; - - enum Features { - DefaultFeatures = 0, - ActionFeature = 1, - AuxHeatingFeature = 2, - AwayModeFeature = 4, - HoldFeature = 8 - }; - - enum Action { - UnknownAction = 0, - OffAction, - HeatingAction, - CoolingAction, - DryingAction, - IdleAction, - FanAction - }; - - enum Mode { - UnknownMode = 0, - OffMode = 1, - AutoMode = 2, - CoolMode = 4, - HeatMode = 8, - DryMode = 16, - FanOnlyMode = 32 - }; - - enum TemperatureUnit { - DefaultUnit = 1, - CelsiusUnit, - FahrenheitUnit - }; - - /** - * Initializes HVAC. - * - * @param uniqueId Unique ID of the HVAC. Recommendes characters: [a-z0-9\-_] - * @param features The list of additional features (flag). For example: `HAHVAC::AuxHeatingFeature | HAHVAC::AwayModeFeature` - */ - HAHVAC( - const char* uniqueId, - uint8_t features = 0 - ); - HAHVAC( - const char* uniqueId, - uint8_t features, - HAMqtt& mqtt - ); // legacy constructor - - virtual void onMqttConnected() override; - - virtual void onMqttMessage( - const char* topic, - const uint8_t* payload, - const uint16_t& length - ) override; - - /** - * Changes action of the HVAC (it's displayed in the Home Assistant panel). - * - * @param action New action. - * @returns Returns true if MQTT message has been published successfully. - */ - bool setAction(Action action); - - /** - * Returns action that was previously sent to MQTT. - */ - inline Action getAction() const - { return _action; } - - /** - * Changes default temperature unit. - * Please note that this method must be called before `mqtt.begin(...)`. - * - * @param unit See TemperatureUnit enum above - */ - inline void setTemperatureUnit(TemperatureUnit unit) - { _temperatureUnit = unit; } - - /** - * Publishes aux heating state. - * Please note that HAHVAC::AuxHeatingFeature must be set in the constructor. - * - * @param state ON (true) / OFF (false) - * @returns Returns true if MQTT message has been published successfully. - */ - bool setAuxHeatingState(bool state); - - /** - * Registers callback that will be called each time the aux heating's state changes. - * Please note that it's not possible to register multiple callbacks. - * - * @param callback - */ - inline void onAuxHeatingStateChanged(HAHVAC_STATE_CALLBACK_BOOL(callback)) - { _auxHeatingCallback = callback; } - - /** - * Returns state of the aux heating. - * - * @returns ON (true) / OFF (false) - */ - inline bool getAuxHeatingState() const - { return _auxHeatingState; } - - /** - * Publishes away state. - * Please note that HAHVAC::AwayModeFeature must be set in the constructor. - * - * @param state ON (true) / OFF (false) - * @returns Returns true if MQTT message has been published successfully. - */ - bool setAwayState(bool state); - - /** - * Registers callback that will be called each time the away's state changes. - * Please note that it's not possible to register multiple callbacks. - * - * @param callback - */ - inline void onAwayStateChanged(HAHVAC_STATE_CALLBACK_BOOL(callback)) - { _awayCallback = callback; } - - /** - * Returns away's state. - * - * @returns ON (true) / OFF (false) - */ - inline bool getAwayState() const - { return _awayState; } - - /** - * Publishes hold state. - * Please note that HAHVAC::HoldFeature must be set in the constructor. - * - * @param state ON (true) / OFF (false) - * @returns Returns true if MQTT message has been published successfully. - */ - bool setHoldState(bool state); - - /** - * Registers callback that will be called each time the hold's state changes. - * Please note that it's not possible to register multiple callbacks. - * - * @param callback - */ - inline void onHoldStateChanged(HAHVAC_STATE_CALLBACK_BOOL(callback)) - { _holdCallback = callback; } - - /** - * Returns hold's state. - * - * @returns ON (true) / OFF (false) - */ - inline bool getHoldState() const - { return _holdState; } - - /** - * Publishes given target temperature. - * - * @param targetTemperature - * @returns Returns true if MQTT message has been published successfully. - */ - bool setTargetTemperature(double targetTemperature); - - /** - * Registers callback that will be called each time the target temperature changes - * Please note that it's not possible to register multiple callbacks. - * - * @param callback - */ - inline void onTargetTemperatureChanged(HAHVAC_STATE_CALLBACK_DOUBLE(callback)) - { _targetTempCallback = callback; } - - /** - * Returns target temperature. - * - * @returns Target temperature or __DBL_MAX__ if temperature is not set. - */ - inline double getTargetTemperature() const - { return _targetTemperature; } - - /** - * Publishes current temperature. - * - * @param temperature - * @returns Returns true if MQTT message has been published successfully. - */ - bool setCurrentTemperature(double temperature); - - /** - * Return temperature that was previously set by `setCurrentTemperature` method. - * - * @returns Temperature or __DBL_MAX__ if temperature is not set. - */ - inline double getCurrentTemperature() const - { return _currentTemperature; } - - /** - * Publishes working mode of the HVAC. - * - * @param mode - * @returns Returns true if MQTT message has been published successfully. - */ - bool setMode(Mode mode); - - /** - * Same as above but input is the string representation of the mode. - * - * @param mode - * @returns Returns true if MQTT message has been published successfully. - */ - bool setModeFromStr(const char* mode); - - /** - * Registers callback that will be called each time the mode changes. - * Please note that it's not possible to register multiple callbacks. - * - * @param callback - */ - inline void onModeChanged(HAHVAC_STATE_CALLBACK_MODE(callback)) - { _modeChangedCallback = callback; } - - /** - * Returns HVAC's mode. - * - * @returns It may be UnknownMode if it's not set. - */ - inline Mode getMode() const - { return _currentMode; } - - /** - * Sets the list of supported modes. By default the list contains all available modes. - * You can merge multiple modes as following: `setModes(HAHVAC::OffMode | HAHVAC::CoolMode)` - * - * @param modes - */ - inline void setModes(uint8_t modes) - { _modes = modes; } - - /** - * Sets `retain` flag for commands published by Home Assistant. - * By default it's set to false. - * - * @param retain - */ - inline void setRetain(bool retain) - { _retain = retain; } - - /** - * Sets the minimum temperature that user will be able to select in Home Assistant panel. - * - * @param minTemp - */ - bool setMinTemp(double minTemp); - - /** - * Sets the maximum temperature that user will be able to select in Home Assistant panel. - * - * @param minTemp - */ - bool setMaxTemp(double maxTemp); - - /** - * Sets the step of the temperature's picker in the Home Assistant panel. - * - * @param tempStep - */ - bool setTempStep(double tempStep); - -private: - bool publishAction(Action action); - bool publishAuxHeatingState(bool state); - bool publishAwayState(bool state); - bool publishHoldState(bool state); - bool publishCurrentTemperature(double temperature); - bool publishTargetTemperature(double temperature); - bool publishMode(Mode mode); - void subscribeTopics(); - uint16_t calculateSerializedLength(const char* serializedDevice) const override; - uint16_t calculateModesLength() const; - bool writeSerializedData(const char* serializedDevice) const override; - - const char* _uniqueId; - const uint8_t _features; - TemperatureUnit _temperatureUnit; - Action _action; - HAHVAC_STATE_CALLBACK_BOOL(_auxHeatingCallback); - bool _auxHeatingState; - HAHVAC_STATE_CALLBACK_BOOL(_awayCallback); - bool _awayState; - HAHVAC_STATE_CALLBACK_BOOL(_holdCallback); - bool _holdState; - double _currentTemperature; - double _minTemp; - double _maxTemp; - double _tempStep; - HAHVAC_STATE_CALLBACK_DOUBLE(_targetTempCallback); - double _targetTemperature; - uint8_t _modes; - HAHVAC_STATE_CALLBACK_MODE(_modeChangedCallback); - Mode _currentMode; - bool _retain; -}; - -#endif -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASensor.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASensor.cpp deleted file mode 100644 index cc75f3c..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASensor.cpp +++ /dev/null @@ -1,218 +0,0 @@ -#ifdef ARDUINO_ARCH_SAMD -#include -#endif - -#include "HASensor.h" -#ifdef ARDUINOHA_SENSOR - -#include "../HAMqtt.h" -#include "../HADevice.h" - -HASensor::HASensor(const char* uniqueId) : - BaseDeviceType("sensor", uniqueId), - _class(nullptr), - _units(nullptr), - _icon(nullptr) -{ - -} - -HASensor::HASensor(const char* uniqueId, HAMqtt& mqtt) : - HASensor(uniqueId) -{ - (void)mqtt; -} - -void HASensor::onMqttConnected() -{ - if (strlen(uniqueId()) == 0) { - return; - } - - publishConfig(); - publishAvailability(); -} - -bool HASensor::setValue(const char* value) -{ - return publishValue(value); -} - -bool HASensor::setValue(uint32_t value) -{ - uint8_t digitsNb = floor(log10(value)) + 1; - char str[digitsNb + 1]; // + null terminator - memset(str, 0, sizeof(str)); - itoa(value, str, 10); - - return publishValue(str); -} - -bool HASensor::setValue(int32_t value) -{ - uint8_t digitsNb = floor(log10(value)) + 1; - char str[digitsNb + 2]; // + null terminator and minus sign - memset(str, 0, sizeof(str)); - itoa(value, str, 10); - - return publishValue(str); -} - -bool HASensor::setValue(double value, uint8_t precision) -{ - uint8_t digitsNb = floor(log10(floor(value))) + 1; - char str[digitsNb + 3 + precision]; // null terminator, dot, minus sign - dtostrf(value, 0, precision, str); - - return publishValue(str); -} - -bool HASensor::setValue(float value, uint8_t precision) -{ - uint8_t digitsNb = floor(log10(floor(value))) + 1; - char str[digitsNb + 3 + precision]; // null terminator, dot, minus sign - dtostrf(value, 0, precision, str); - - return publishValue(str); -} - -bool HASensor::publishValue(const char* value) -{ - if (strlen(uniqueId()) == 0 || value == nullptr) { - return false; - } - - if (!mqtt()->isConnected()) { - return false; - } - - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::StateTopic - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - topic, - componentName(), - uniqueId(), - DeviceTypeSerializer::StateTopic - ); - - if (strlen(topic) == 0) { - return false; - } - - return mqtt()->publish(topic, value, true); -} - -uint16_t HASensor::calculateSerializedLength( - const char* serializedDevice -) const -{ - if (serializedDevice == nullptr) { - return 0; - } - - const HADevice* device = mqtt()->getDevice(); - if (device == nullptr) { - return 0; - } - - uint16_t size = 0; - size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateNameFieldSize(getName()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); - size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); - - { - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::StateTopic, - false - ); - - if (topicSize == 0) { - return 0; - } - - // Format: "stat_t":"[TOPIC]" - size += topicSize + 11; // 11 - length of the JSON decorators for this field - } - - // device class - if (_class != nullptr) { - // Field format: ,"dev_cla":"[CLASS]" - size += strlen(_class) + 13; // 13 - length of the JSON decorators for this field - } - - // units of measurement - if (_units != nullptr) { - // Format: ,"unit_of_meas":"[UNITS]" - size += strlen(_units) + 18; // 18 - length of the JSON decorators for this field - } - - // icon - if (_icon != nullptr) { - // Field format: ,"ic":"[ICON]" - size += strlen(_icon) + 8; // 8 - length of the JSON decorators for this field - } - - return size; // exludes null terminator -} - -bool HASensor::writeSerializedData(const char* serializedDevice) const -{ - if (serializedDevice == nullptr) { - return false; - } - - DeviceTypeSerializer::mqttWriteBeginningJson(); - - // state topic - { - static const char Prefix[] PROGMEM = {"\"stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - DeviceTypeSerializer::StateTopic - ); - } - - // device class - if (_class != nullptr) { - static const char Prefix[] PROGMEM = {",\"dev_cla\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(Prefix, _class); - } - - // units of measurement - if (_units != nullptr) { - static const char Prefix[] PROGMEM = {",\"unit_of_meas\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(Prefix, _units); - } - - // icon - if (_icon != nullptr) { - static const char Prefix[] PROGMEM = {",\"ic\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField( - Prefix, - _icon - ); - } - - DeviceTypeSerializer::mqttWriteNameField(getName()); - DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); - DeviceTypeSerializer::mqttWriteAvailabilityField(this); - DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); - DeviceTypeSerializer::mqttWriteEndJson(); - - return true; -} - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASensor.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASensor.h deleted file mode 100644 index 908dc72..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASensor.h +++ /dev/null @@ -1,90 +0,0 @@ -#ifndef AHA_HASENSOR_H -#define AHA_HASENSOR_H - -#include "BaseDeviceType.h" - -#ifdef ARDUINOHA_SENSOR - -class HASensor : public BaseDeviceType -{ -public: - /** - * Initializes binary sensor. - * - * @param uniqueId Unique ID of the sensor. Recommendes characters: [a-z0-9\-_] - */ - HASensor( - const char* uniqueId - ); - HASensor( - const char* uniqueId, - HAMqtt& mqtt - ); // legacy constructor - - /** - * Publishes configuration of the sensor to the MQTT. - */ - virtual void onMqttConnected() override; - - /** - * Publishes new value of the sensor. - * Please note that connection to MQTT broker must be acquired. - * Otherwise method will return false. - * - * @param state Value to publish. - * @returns Returns true if MQTT message has been published successfully. - */ - bool setValue(const char* value); - bool setValue(uint32_t value); - bool setValue(int32_t value); - bool setValue(double value, uint8_t precision = 2); - bool setValue(float value, uint8_t precision = 2); - - inline bool setValue(uint8_t value) - { return setValue(static_cast(value)); } - - inline bool setValue(uint16_t value) - { return setValue(static_cast(value)); } - - inline bool setValue(int8_t value) - { return setValue(static_cast(value)); } - - inline bool setValue(int16_t value) - { return setValue(static_cast(value)); } - - /** - * The type/class of the sensor to set the icon in the frontend. - * - * @param className https://www.home-assistant.io/integrations/sensor/#device-class - */ - inline void setDeviceClass(const char* className) - { _class = className; } - - /** - * Defines the units of measurement of the sensor, if any. - * - * @param units For example: °C, % - */ - inline void setUnitOfMeasurement(const char* units) - { _units = units; } - - /** - * Sets icon of the sensor, e.g. `mdi:home`. - * - * @param icon Material Design Icon name with mdi: prefix. - */ - inline void setIcon(const char* icon) - { _icon = icon; } - -private: - bool publishValue(const char* value); - uint16_t calculateSerializedLength(const char* serializedDevice) const override; - bool writeSerializedData(const char* serializedDevice) const override; - - const char* _class; - const char* _units; - const char* _icon; -}; - -#endif -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASwitch.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASwitch.cpp deleted file mode 100644 index e62b91a..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASwitch.cpp +++ /dev/null @@ -1,211 +0,0 @@ -#include "HASwitch.h" -#ifdef ARDUINOHA_SWITCH - -#include "../ArduinoHADefines.h" -#include "../HAMqtt.h" -#include "../HADevice.h" - -HASwitch::HASwitch(const char* uniqueId, bool initialState) : - BaseDeviceType("switch", uniqueId), - _stateCallback(nullptr), - _beforeStateCallback(nullptr), - _currentState(initialState), - _icon(nullptr), - _retain(false) -{ - -} - -HASwitch::HASwitch( - const char* uniqueId, - bool initialState, - HAMqtt& mqtt -) : - HASwitch(uniqueId, initialState) -{ - (void)mqtt; -} - -void HASwitch::onMqttConnected() -{ - if (strlen(uniqueId()) == 0) { - return; - } - - publishConfig(); - publishAvailability(); - - if (!_retain) { - publishState(_currentState); - } - - DeviceTypeSerializer::mqttSubscribeTopic( - this, - DeviceTypeSerializer::CommandTopic - ); -} - -void HASwitch::onMqttMessage( - const char* topic, - const uint8_t* payload, - const uint16_t& length -) -{ - (void)payload; - - if (compareTopics(topic, DeviceTypeSerializer::CommandTopic)) { - bool state = (length == strlen(DeviceTypeSerializer::StateOn)); - setState(state, true); - } -} - -bool HASwitch::setState(bool state, bool force) -{ - if (!force && _currentState == state) { - return true; - } - - if (_beforeStateCallback) { - _beforeStateCallback(state, this); - } - - if (publishState(state)) { - _currentState = state; - - if (_stateCallback) { - _stateCallback(_currentState, this); - } - - return true; - } - - return false; -} - -bool HASwitch::publishState(bool state) -{ - if (strlen(uniqueId()) == 0) { - return false; - } - - return DeviceTypeSerializer::mqttPublishMessage( - this, - DeviceTypeSerializer::StateTopic, - ( - state ? - DeviceTypeSerializer::StateOn : - DeviceTypeSerializer::StateOff - ) - ); -} - -uint16_t HASwitch::calculateSerializedLength(const char* serializedDevice) const -{ - if (serializedDevice == nullptr) { - return 0; - } - - const HADevice* device = mqtt()->getDevice(); - if (device == nullptr) { - return 0; - } - - uint16_t size = 0; - size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateNameFieldSize(getName()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); - size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); - size += DeviceTypeSerializer::calculateRetainFieldSize(_retain); - - // cmd topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::CommandTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: "cmd_t":"[TOPIC]" - size += topicLength + 10; // 10 - length of the JSON decorators for this field - } - - // state topic - { - const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::StateTopic, - false - ); - - if (topicLength == 0) { - return 0; - } - - // Field format: ,"stat_t":"[TOPIC]" - size += topicLength + 12; // 12 - length of the JSON decorators for this field - } - - // icon - if (_icon != nullptr) { - // Field format: ,"ic":"[ICON]" - size += strlen(_icon) + 8; // 8 - length of the JSON decorators for this field - } - - return size; // exludes null terminator -} - -bool HASwitch::writeSerializedData(const char* serializedDevice) const -{ - if (serializedDevice == nullptr) { - return false; - } - - DeviceTypeSerializer::mqttWriteBeginningJson(); - - // command topic - { - static const char Prefix[] PROGMEM = {"\"cmd_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - DeviceTypeSerializer::CommandTopic - ); - } - - // state topic - { - static const char Prefix[] PROGMEM = {",\"stat_t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - DeviceTypeSerializer::StateTopic - ); - } - - // icon - if (_icon != nullptr) { - static const char Prefix[] PROGMEM = {",\"ic\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField( - Prefix, - _icon - ); - } - - DeviceTypeSerializer::mqttWriteRetainField(_retain); - DeviceTypeSerializer::mqttWriteNameField(getName()); - DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); - DeviceTypeSerializer::mqttWriteAvailabilityField(this); - DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); - DeviceTypeSerializer::mqttWriteEndJson(); - - return true; -} - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASwitch.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASwitch.h deleted file mode 100644 index 04a18f1..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HASwitch.h +++ /dev/null @@ -1,123 +0,0 @@ -#ifndef AHA_HASWITCH_H -#define AHA_HASWITCH_H - -#include "BaseDeviceType.h" - -#ifdef ARDUINOHA_SWITCH - -#define HASWITCH_CALLBACK(name) void (*name)(bool, HASwitch*) - -class HASwitch : public BaseDeviceType -{ -public: - /** - * Initializes switch. - * - * @param uniqueId Unique ID of the switch. Recommendes characters: [a-z0-9\-_] - * @param initialState Initial state of the switch. - It will be published right after "config" message in order to update HA state. - */ - HASwitch( - const char* uniqueId, - bool initialState - ); - HASwitch( - const char* uniqueId, - bool initialState, - HAMqtt& mqtt - ); // legacy constructor - - /** - * Publishes configuration of the sensor to the MQTT. - */ - virtual void onMqttConnected() override; - - /** - * Processes message received from the MQTT broker. - * The method updates state of the switch (if message matches switch'es topic). - */ - virtual void onMqttMessage( - const char* topic, - const uint8_t* payload, - const uint16_t& length - ) override; - - /** - * Changes state of the switch and publishes MQTT message. - * Please note that if a new value is the same as previous one, - * the MQTT message won't be published. - * - * @param state New state of the switch. - * @param force Forces to update state without comparing it to previous known state. - * @returns Returns true if MQTT message has been published successfully. - */ - bool setState(bool state, bool force = false); - - /** - * Alias for setState(true). - */ - inline bool turnOn() - { return setState(true); } - - /** - * Alias for setState(false). - */ - inline bool turnOff() - { return setState(false); } - - /** - * Returns last known state of the switch. - * If setState method wasn't called the initial value will be returned. - */ - inline bool getState() const - { return _currentState; } - - /** - * Registers callback that will be called each time the state of the switch changes. - * Please note that it's not possible to register multiple callbacks for the same switch. - * - * @param callback - */ - inline void onStateChanged(HASWITCH_CALLBACK(callback)) - { _stateCallback = callback; } - - /** - * Registers callback that will be called before state of the switch changes. - * The state passed to callback is a new state that is going to be set. - * - * @param callback - */ - inline void onBeforeStateChanged(HASWITCH_CALLBACK(callback)) - { _beforeStateCallback = callback; } - - /** - * Sets icon of the switch, e.g. `mdi:home`. - * - * @param icon Material Design Icon name with mdi: prefix. - */ - inline void setIcon(const char* icon) - { _icon = icon; } - - /** - * Sets `retain` flag for commands published by Home Assistant. - * By default it's set to false. - * - * @param retain - */ - inline void setRetain(bool retain) - { _retain = retain; } - -private: - bool publishState(bool state); - uint16_t calculateSerializedLength(const char* serializedDevice) const override; - bool writeSerializedData(const char* serializedDevice) const override; - - HASWITCH_CALLBACK(_stateCallback); - HASWITCH_CALLBACK(_beforeStateCallback); - bool _currentState; - const char* _icon; - bool _retain; -}; - -#endif -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATagScanner.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATagScanner.cpp deleted file mode 100644 index 1edafa7..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATagScanner.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "HATagScanner.h" -#ifdef ARDUINOHA_TAG_SCANNER - -#include "../ArduinoHADefines.h" -#include "../HAMqtt.h" -#include "../HADevice.h" - -HATagScanner::HATagScanner(const char* uniqueId) : - BaseDeviceType("tag", uniqueId) -{ - -} - -HATagScanner::HATagScanner(const char* uniqueId, HAMqtt& mqtt) : - HATagScanner(uniqueId) -{ - (void)mqtt; -} - -void HATagScanner::onMqttConnected() -{ - if (strlen(uniqueId()) == 0) { - return; - } - - publishConfig(); -} - -bool HATagScanner::tagScanned(const char* tag) -{ - if (tag == nullptr || strlen(tag) == 0 || strlen(uniqueId()) == 0) { - return false; - } - - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::EventTopic - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - topic, - componentName(), - uniqueId(), - DeviceTypeSerializer::EventTopic - ); - - if (strlen(topic) == 0) { - return false; - } - - return mqtt()->publish(topic, tag); -} - -uint16_t HATagScanner::calculateSerializedLength( - const char* serializedDevice -) const -{ - if (serializedDevice == nullptr) { - return 0; - } - - uint16_t size = 0; - size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - - // event topic - { - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - componentName(), - uniqueId(), - DeviceTypeSerializer::EventTopic, - false - ); - - // Format: "t":"[TOPIC]" - size += topicSize + 6; // 6 - length of the JSON decorators for this field - } - - return size; // exludes null terminator -} - -bool HATagScanner::writeSerializedData(const char* serializedDevice) const -{ - if (serializedDevice == nullptr) { - return false; - } - - DeviceTypeSerializer::mqttWriteBeginningJson(); - - // topic - { - static const char Prefix[] PROGMEM = {"\"t\":\""}; - DeviceTypeSerializer::mqttWriteTopicField( - this, - Prefix, - DeviceTypeSerializer::EventTopic - ); - } - - DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); - DeviceTypeSerializer::mqttWriteEndJson(); - - return true; -} - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATagScanner.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATagScanner.h deleted file mode 100644 index 2b26d1a..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATagScanner.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef AHA_HATAGSCANNER_H -#define AHA_HATAGSCANNER_H - -#include "BaseDeviceType.h" - -#ifdef ARDUINOHA_TAG_SCANNER - -class HATagScanner : public BaseDeviceType -{ -public: - /** - * Initializes tag scanner with the given name. - * - * @param uniqueId Unique ID of the scanner. Recommendes characters: [a-z0-9\-_] - */ - HATagScanner(const char* uniqueId); - HATagScanner(const char* uniqueId, HAMqtt& mqtt); // legacy constructor - - /** - * Publishes configuration of the sensor to the MQTT. - */ - virtual void onMqttConnected() override; - - /** - * Tag scanner doesn't support availability. Nothing to do here. - */ - virtual void setAvailability(bool online) override { (void)online; } - - /** - * Sends "tag scanned" event to the MQTT (Home Assistant). - * Based on this event HA may perform user-defined automation. - * - * @param tag Value of the scanned tag. - */ - bool tagScanned(const char* tag); - -private: - uint16_t calculateSerializedLength(const char* serializedDevice) const override; - bool writeSerializedData(const char* serializedDevice) const override; -}; - -#endif -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATriggers.cpp b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATriggers.cpp deleted file mode 100644 index 62b1ae3..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATriggers.cpp +++ /dev/null @@ -1,321 +0,0 @@ -#include "HATriggers.h" -#ifdef ARDUINOHA_TRIGGERS - -#include "../ArduinoHADefines.h" -#include "../HAMqtt.h" -#include "../HADevice.h" - -HATriggers::HATriggers() : - BaseDeviceType("device_automation", nullptr), - _triggers(nullptr), - _triggersNb(0) -{ - -} - -HATriggers::HATriggers(HAMqtt& mqtt) : - HATriggers() -{ - (void)mqtt; -} - -HATriggers::~HATriggers() -{ - if (_triggers != nullptr) { - free(_triggers); - } -} - -void HATriggers::onMqttConnected() -{ - publishConfig(); -} - -bool HATriggers::add(const char* type, const char* subtype) -{ - if (mqtt()->getDevice() == nullptr) { - return false; - } - - HATrigger* triggers = (HATrigger*)realloc(_triggers, sizeof(HATrigger) * (_triggersNb + 1)); - if (triggers == nullptr) { - return false; - } - -#if defined(ARDUINOHA_DEBUG) - Serial.print(F("Adding HATrigger: ")); - Serial.print(type); - Serial.print(F(" ")); - Serial.print(subtype); - Serial.println(); -#endif - - _triggers = triggers; - _triggers[_triggersNb].type = type; - _triggers[_triggersNb].subtype = subtype; - - _triggersNb++; - return true; -} - -bool HATriggers::trigger(const char* type, const char* subtype) -{ - HATrigger* trigger = nullptr; - for (uint8_t i = 0; i < _triggersNb; i++) { - if (strcmp(_triggers[i].type, type) == 0 && - strcmp(_triggers[i].subtype, subtype) == 0) { - trigger = &_triggers[i]; - break; - } - } - - if (trigger == nullptr) { - return false; - } - - const uint16_t& topicSize = calculateTopicLength( - componentName(), - trigger, - DeviceTypeSerializer::EventTopic - ); - - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - generateTopic( - topic, - componentName(), - trigger, - DeviceTypeSerializer::EventTopic - ); - - if (strlen(topic) == 0) { - return false; - } - -#if defined(ARDUINOHA_DEBUG) - Serial.print(F("Triggering HATrigger: ")); - Serial.print(type); - Serial.print(F(" ")); - Serial.print(subtype); - Serial.println(); -#endif - - return mqtt()->publish(topic, ""); -} - -void HATriggers::publishConfig() -{ - const HADevice* device = mqtt()->getDevice(); - if (device == nullptr) { - return; // device is required for triggers - } - - const uint16_t& deviceLength = device->calculateSerializedLength(); - if (deviceLength == 0) { - return; - } - - char serializedDevice[deviceLength]; - if (device->serialize(serializedDevice) == 0) { - return; - } - - for (uint8_t i = 0; i < _triggersNb; i++) { - const HATrigger* trigger = &_triggers[i]; - if (trigger == nullptr) { - continue; - } - - const uint16_t& topicLength = calculateTopicLength( - componentName(), - trigger, - DeviceTypeSerializer::ConfigTopic, - true, - true - ); - const uint16_t& dataLength = calculateSerializedLength( - trigger, - serializedDevice - ); - if (topicLength == 0 || dataLength == 0) { - continue; - } - - char topic[topicLength]; - generateTopic( - topic, - componentName(), - trigger, - DeviceTypeSerializer::ConfigTopic, - true - ); - - if (strlen(topic) == 0) { - continue; - } - - if (mqtt()->beginPublish(topic, dataLength, true)) { - writeSerializedTrigger(trigger, serializedDevice); - mqtt()->endPublish(); - } - } -} - -uint16_t HATriggers::calculateTopicLength( - const char* component, - const HATrigger *trigger, - const char* suffix, - bool includeNullTerminator, - bool isDiscoveryTopic -) const -{ - uint8_t length = strlen(trigger->type) + strlen(trigger->subtype) + 2; // underscore and slash - return DeviceTypeSerializer::calculateTopicLength( - component, - nullptr, - suffix, - includeNullTerminator, - isDiscoveryTopic - ) + length; -} - -uint16_t HATriggers::generateTopic( - char* output, - const char* component, - const HATrigger *trigger, - const char* suffix, - bool isDiscoveryTopic -) const -{ - static const char Underscore[] PROGMEM = {"_"}; - uint8_t length = strlen(trigger->type) + strlen(trigger->subtype) + 2; // underscore + null terminator - char objectId[length]; - - strcpy(objectId, trigger->subtype); - strcat_P(objectId, Underscore); - strcat(objectId, trigger->type); - - return DeviceTypeSerializer::generateTopic( - output, - component, - objectId, - suffix, - isDiscoveryTopic - ); -} - -uint16_t HATriggers::calculateSerializedLength( - const HATrigger* trigger, - const char* serializedDevice -) const -{ - if (serializedDevice == nullptr) { - return 0; - } - - uint16_t size = 0; - size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - - // automation type - { - // Format: "atype":"trigger" - size += 17; - } - - // topic - { - const uint16_t& topicSize = calculateTopicLength( - componentName(), - trigger, - DeviceTypeSerializer::EventTopic, - false - ); - if (topicSize == 0) { - return 0; - } - - // Format: ,"t":"[TOPIC]" - size += topicSize + 7; // 7 - length of the JSON decorators for this field - } - - // type - { - // Format: ,"type":"[TYPE]" - size += strlen(trigger->type) + 10; // 10 - length of the JSON decorators for this field - } - - // subtype - { - // Format: ,"stype":"[SUBTYPE]" - size += strlen(trigger->subtype) + 11; // 11 - length of the JSON decorators for this field; - } - - return size; // exludes null terminator -} - -bool HATriggers::writeSerializedTrigger( - const HATrigger* trigger, - const char* serializedDevice -) const -{ - if (serializedDevice == nullptr) { - return false; - } - - DeviceTypeSerializer::mqttWriteBeginningJson(); - - // automation type - { - static const char Data[] PROGMEM = {"\"atype\":\"trigger\""}; - mqtt()->writePayload_P(Data); - } - - // topic - { - const uint16_t& topicSize = calculateTopicLength( - componentName(), - trigger, - DeviceTypeSerializer::EventTopic - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - generateTopic( - topic, - componentName(), - trigger, - DeviceTypeSerializer::EventTopic - ); - - static const char Prefix[] PROGMEM = {",\"t\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField( - Prefix, - topic - ); - } - - // type - { - static const char Prefix[] PROGMEM = {",\"type\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(Prefix, trigger->type); - } - - // subtype - { - static const char Prefix[] PROGMEM = {",\"stype\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(Prefix, trigger->subtype); - } - - DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); - DeviceTypeSerializer::mqttWriteEndJson(); - - return true; -} - -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATriggers.h b/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATriggers.h deleted file mode 100644 index 5732e47..0000000 --- a/firmware/uniclock/lib/arduino-home-assistant-main/src/device-types/HATriggers.h +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef AHA_HATRIGGERS_H -#define AHA_HATRIGGERS_H - -#include "BaseDeviceType.h" - -#ifdef ARDUINOHA_TRIGGERS - -struct HATrigger { - const char* type; - const char* subtype; -} __attribute__((packed)); - -class HATriggers : public BaseDeviceType -{ -public: - HATriggers(); - HATriggers(HAMqtt& mqtt); // legacy constructor - virtual ~HATriggers(); - - virtual void onMqttConnected() override; - - /** - * Triggers dont't support availability. Nothing to do here. - */ - virtual void setAvailability(bool online) override { (void)online; } - - bool add(const char* type, const char* subtype); - bool trigger(const char* type, const char* subtype); - -protected: - void publishConfig() override; - - uint16_t calculateSerializedLength( - const char* serializedDevice - ) const override { (void)serializedDevice; return 0; } - - bool writeSerializedData( - const char* serializedDevice - ) const override { (void)serializedDevice; return false; } - - uint16_t calculateTopicLength( - const char* component, - const HATrigger *trigger, - const char* suffix, - bool includeNullTerminator = true, - bool isDiscoveryTopic = false - ) const; - - uint16_t generateTopic( - char* output, - const char* component, - const HATrigger *trigger, - const char* suffix, - bool isDiscoveryTopic = false - ) const; - -private: - uint16_t calculateSerializedLength( - const HATrigger* trigger, - const char* serializedDevice - ) const; - - bool writeSerializedTrigger( - const HATrigger* trigger, - const char* serializedDevice - ) const; - - HATrigger* _triggers; - uint8_t _triggersNb; -}; - -#endif -#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/LICENSE b/firmware/uniclock/lib/arduino-home-assistant/LICENSE new file mode 100644 index 0000000..29ebfa5 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/firmware/uniclock/lib/arduino-home-assistant/README.md b/firmware/uniclock/lib/arduino-home-assistant/README.md new file mode 100644 index 0000000..1790341 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/README.md @@ -0,0 +1,111 @@ +# Arduino Home Assistant integration 🏠 + +[![](https://img.shields.io/github/v/release/dawidchyrzynski/arduino-home-assistant?label=Version)](https://github.com/dawidchyrzynski/arduino-home-assistant/releases) +[![](https://img.shields.io/badge/Browse_Documentation-40BC13)](https://dawidchyrzynski.github.io/arduino-home-assistant/) +[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/dawidchyrzynski) + +ArduinoHA allows to integrate an Arduino/ESP based device with Home Assistant using MQTT. +The library is designed to use as low resources (RAM/flash) as possible. +Initially, it was optimized to work on Arduino Uno with Ethernet Shield, +but I successfully use it on ESP8266/ESP8255 boards in my projects. + +## Features + +* Two-way communication (state reporting and command execution) +* MQTT discovery (device is added to the Home Assistant panel automatically) +* MQTT Last Will and Testament +* Support for custom MQTT messages (publishing and subscribing) +* Auto reconnect with MQTT broker +* Reporting availability (online/offline states) of a device +* Doxygen documentation for all classes +* Covered by unit tests (AUnit + EpoxyDuino + AUniter) + +## Supported HA types + +| Home Assistant type | Supported | +| ------------------- | :--------: | +| Alarm control panel | ❌ | +| Binary sensor | ✅ | +| Button | ✅ | +| Camera | ✅ | +| Cover | ✅ | +| Device tracker | ✅ | +| Device trigger | ✅ | +| Event | ❌ | +| Fan | ✅ | +| Humidifier | ❌ | +| Image | ❌ | +| HVAC | ✅ | +| Lawn mower | ❌ | +| Light | ✅ | +| Lock | ✅ | +| Number | ✅ | +| Scene | ✅ | +| Select | ✅ | +| Sensor | ✅ | +| Siren | ❌ | +| Switch | ✅ | +| Update | ❌ | +| Tag scanner | ✅ | +| Text | ❌ | +| Vacuum | ❌ | +| Valve | ❌ | +| Water heater | ❌ | + +## Examples + +|Example|Description | +|-------|-----------------------------| +|[Binary sensor](examples/binary-sensor/binary-sensor.ino)|Using the binary sensor as a door contact sensor.| +|[Button](examples/button/button.ino)|Adding simple buttons to the Home Assistant panel.| +|[Camera](examples/esp32-cam/esp32-cam.ino)|Publishing the preview from the ESP32-CAM module.| +|[Cover](examples/cover/cover.ino)|Controlling a window cover (open / close / stop).| +|[Device trigger](examples/multi-state-button/multi-state-button.ino)|Implementation of a simple wall switch that reports press and hold states.| +|[Fan](examples/fan/fan.ino)|Controlling a simple fan (state + speed).| +|[HVAC](examples/hvac/hvac.ino)|HVAC controller with multiple modes, power control and target temperature.| +|[Lock](examples/lock/lock.ino)|A simple door lock that's controlled by the Home Assistant.| +|[Light](examples/light/light.ino)|A simple light that allows changing brightness, color temperature and RGB color.| +|[Number](examples/number/number.ino)|Adding an interactive numeric slider in the Home Assistant panel.| +|[Scene](examples/scene/scene.ino)|Adding a custom scene in the Home Assistant panel. | +|[Select](examples/select/select.ino)|A dropdown selector that's displayed in the Home Assistant panel.| +|[Sensor](examples/sensor/sensor.ino)|A simple sensor that reports a state in a string representation (open / opening / close).| +|[Analog sensor](examples/sensor-analog/sensor-analog.ino)|Reporting the analog pin's voltage to the Home Assistant.| +|[Integer sensor](examples/sensor-integer/sensor-integer.ino)|Reporting the device's uptime to the Home Assistant.| +|[Switch](examples/led-switch/led-switch.ino)|The LED that's controlled by the Home Assistant.| +|[Multi-switch](examples/multi-switch/multi-switch.ino)|Multiple switches controlled by the Home Assistant.| +|[Tag scanner](examples/tag-scanner/tag-scanner.ino)|Scanning RFID tags using the MFRC522 module.| +|[Availability](examples/availability/availability.ino)|Reporting entities' availability (online / offline) to the Home Assistant.| +|[Advanced availability](examples/advanced-availability/advanced-availability.ino)|Advanced availability reporting with MQTT LWT (Last Will and Testament).| +|[MQTT advanced](examples/mqtt-advanced/mqtt-advanced.ino)|Subscribing to custom topics and publishing custom messages.| +|[MQTT with credentials](examples/mqtt-with-credentials/mqtt-with-credentials.ino)|Establishing connection with a MQTT broker using the credentials. | +|[NodeMCU (ESP8266)](examples/nodemcu/nodemcu.ino)|Basic example for ESP8266 devices.| +|[Arduino Nano 33 IoT](examples/nano33iot/nano33iot.ino)|Basic example for Arduino Nano 33 IoT (SAMD family).| +|[mDNS discovery](examples/mdns/mdns.ino)|Make your ESP8266 discoverable via the mDNS.| + +## Compatible hardware + +The library uses the Arduino Ethernet Client API for interacting with the network hardware. +It should work fine as long as the `Client` class is available. + +Here is the list of devices on which the library was tested: + +* Arduino Uno +* Arduino Mega +* Arduino Nano +* Arduino Pro Mini +* Arduino Nano 33 IoT +* Arduino Due +* NodeMCU +* Controllino Mega (Pure) +* Controllino Maxi (Pure) +* ESP-01 +* ESP32-CAM +* Sonoff Dual R2 +* Sonoff Dual R3 +* Sonoff Basic +* Sonoff Mini +* Tuya Wi-Fi switch module +* Tuya Wi-Fi curtain module + +Please note that it's not the complete list of supported devices. +You may try to use the library on any device that uses Arduino core. diff --git a/firmware/uniclock/lib/arduino-home-assistant-main/library.properties b/firmware/uniclock/lib/arduino-home-assistant/library.properties similarity index 72% rename from firmware/uniclock/lib/arduino-home-assistant-main/library.properties rename to firmware/uniclock/lib/arduino-home-assistant/library.properties index d4d9667..fc504ab 100644 --- a/firmware/uniclock/lib/arduino-home-assistant-main/library.properties +++ b/firmware/uniclock/lib/arduino-home-assistant/library.properties @@ -1,7 +1,7 @@ name=home-assistant-integration -version=1.3.0 -author=Dawid Chyrzynski -maintainer=Dawid Chyrzynski +version=2.1.0 +author=Dawid Chyrzynski +maintainer=Dawid Chyrzynski sentence=Home Assistant MQTT integration for Arduino paragraph=Lightweight library that provides easy to use API for integrating your Arduino/ESP based device with Home Assistant. category=Communication diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/ArduinoHA.h b/firmware/uniclock/lib/arduino-home-assistant/src/ArduinoHA.h new file mode 100644 index 0000000..974a516 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/ArduinoHA.h @@ -0,0 +1,33 @@ +#ifndef AHA_ARDUINOHA_H +#define AHA_ARDUINOHA_H + +#include "HADevice.h" +#include "HAMqtt.h" +#include "device-types/HABinarySensor.h" +#include "device-types/HAButton.h" +#include "device-types/HACamera.h" +#include "device-types/HACover.h" +#include "device-types/HADeviceTracker.h" +#include "device-types/HADeviceTrigger.h" +#include "device-types/HAFan.h" +#include "device-types/HAHVAC.h" +#include "device-types/HALight.h" +#include "device-types/HALock.h" +#include "device-types/HANumber.h" +#include "device-types/HAScene.h" +#include "device-types/HASelect.h" +#include "device-types/HASensor.h" +#include "device-types/HASensorNumber.h" +#include "device-types/HASwitch.h" +#include "device-types/HATagScanner.h" +#include "utils/HAUtils.h" +#include "utils/HANumeric.h" + +#ifdef ARDUINOHA_TEST +#include "mocks/AUnitHelpers.h" +#include "mocks/PubSubClientMock.h" +#include "utils/HADictionary.h" +#include "utils/HASerializer.h" +#endif + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/ArduinoHADefines.h b/firmware/uniclock/lib/arduino-home-assistant/src/ArduinoHADefines.h new file mode 100644 index 0000000..a27c741 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/ArduinoHADefines.h @@ -0,0 +1,42 @@ +// Turns on debug information of the ArduinoHA core. +// Please note that you need to initialize serial interface manually +// by calling Serial.begin([baudRate]) before initializing ArduinoHA. +// #define ARDUINOHA_DEBUG + +// These macros allow to exclude some parts of the library to save more resources. +// #define EX_ARDUINOHA_BINARY_SENSOR +// #define EX_ARDUINOHA_BUTTON +// #define EX_ARDUINOHA_CAMERA +// #define EX_ARDUINOHA_COVER +// #define EX_ARDUINOHA_DEVICE_TRACKER +// #define EX_ARDUINOHA_DEVICE_TRIGGER +// #define EX_ARDUINOHA_FAN +// #define EX_ARDUINOHA_HVAC +// #define EX_ARDUINOHA_LIGHT +// #define EX_ARDUINOHA_LOCK +// #define EX_ARDUINOHA_NUMBER +// #define EX_ARDUINOHA_SCENE +// #define EX_ARDUINOHA_SELECT +// #define EX_ARDUINOHA_SENSOR +// #define EX_ARDUINOHA_SWITCH +// #define EX_ARDUINOHA_TAG_SCANNER + +#if defined(ARDUINOHA_DEBUG) + #include + + #define ARDUINOHA_DEBUG_INIT() Serial.begin(115200); + #define ARDUINOHA_DEBUG_PRINTLN(x) Serial.println(x); + #define ARDUINOHA_DEBUG_PRINT(x) Serial.print(x); +#else + #define ARDUINOHA_DEBUG_INIT() + #define ARDUINOHA_DEBUG_PRINTLN(x) + #define ARDUINOHA_DEBUG_PRINT(x) +#endif + + +#if defined(__SAMD21G18A__) or defined(__SAM3X8E__) + #define ARDUINOHA_INT_OVERLOAD +#endif + +#define AHATOFSTR(x) reinterpret_cast(x) +#define AHAFROMFSTR(x) reinterpret_cast(x) diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/HADevice.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/HADevice.cpp new file mode 100644 index 0000000..bdf4776 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/HADevice.cpp @@ -0,0 +1,155 @@ +#include "ArduinoHADefines.h" +#include "HADevice.h" +#include "HAMqtt.h" +#include "utils/HAUtils.h" +#include "utils/HASerializer.h" + +#define HADEVICE_INIT \ + _ownsUniqueId(false), \ + _serializer(new HASerializer(nullptr, 6)), \ + _availabilityTopic(nullptr), \ + _sharedAvailability(false), \ + _available(true), \ + _extendedUniqueIds(false) + +HADevice::HADevice() : + _uniqueId(nullptr), + HADEVICE_INIT +{ + +} + +HADevice::HADevice(const char* uniqueId) : + _uniqueId(uniqueId), + HADEVICE_INIT +{ + _serializer->set(AHATOFSTR(HADeviceIdentifiersProperty), _uniqueId); +} + +HADevice::HADevice(const byte* uniqueId, const uint16_t length) : + _uniqueId(HAUtils::byteArrayToStr(uniqueId, length)), + HADEVICE_INIT +{ + _ownsUniqueId = true; + _serializer->set(AHATOFSTR(HADeviceIdentifiersProperty), _uniqueId); +} + +HADevice::~HADevice() +{ + delete _serializer; + + if (_availabilityTopic) { + delete _availabilityTopic; + } + + if (_ownsUniqueId) { + delete[] _uniqueId; + } +} + +bool HADevice::setUniqueId(const byte* uniqueId, const uint16_t length) +{ + if (_uniqueId) { + return false; // unique ID cannot be changed at runtime once it's set + } + + _uniqueId = HAUtils::byteArrayToStr(uniqueId, length); + _ownsUniqueId = true; + _serializer->set(AHATOFSTR(HADeviceIdentifiersProperty), _uniqueId); + return true; +} + +void HADevice::setManufacturer(const char* manufacturer) +{ + _serializer->set(AHATOFSTR(HADeviceManufacturerProperty), manufacturer); +} + +void HADevice::setModel(const char* model) +{ + _serializer->set(AHATOFSTR(HADeviceModelProperty), model); +} + +void HADevice::setName(const char* name) +{ + _serializer->set(AHATOFSTR(HANameProperty), name); +} + +void HADevice::setSoftwareVersion(const char* softwareVersion) +{ + _serializer->set( + AHATOFSTR(HADeviceSoftwareVersionProperty), + softwareVersion + ); +} + +void HADevice::setConfigurationUrl(const char* url) +{ + _serializer->set( + AHATOFSTR(HADeviceConfigurationUrlProperty), + url + ); +} + +void HADevice::setAvailability(bool online) +{ + _available = online; + publishAvailability(); +} + +bool HADevice::enableSharedAvailability() +{ + if (_sharedAvailability) { + return true; // already enabled + } + + const uint16_t topicLength = HASerializer::calculateDataTopicLength( + nullptr, + AHATOFSTR(HAAvailabilityTopic) + ); + if (topicLength == 0) { + return false; + } + + _availabilityTopic = new char[topicLength]; + + if (HASerializer::generateDataTopic( + _availabilityTopic, + nullptr, + AHATOFSTR(HAAvailabilityTopic) + ) > 0) { + _sharedAvailability = true; + return true; + } + + return false; +} + +void HADevice::enableLastWill() +{ + HAMqtt* mqtt = HAMqtt::instance(); + if (!mqtt || !_availabilityTopic) { + return; + } + + mqtt->setLastWill( + _availabilityTopic, + "offline", + true + ); +} + +void HADevice::publishAvailability() const +{ + HAMqtt* mqtt = HAMqtt::instance(); + if (!_availabilityTopic || !mqtt) { + return; + } + + const char* payload = _available ? HAOnline : HAOffline; + const uint16_t length = strlen_P(payload); + + if (mqtt->beginPublish(_availabilityTopic, length, true)) { + mqtt->writePayload(AHATOFSTR(payload)); + mqtt->endPublish(); + } +} diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/HADevice.h b/firmware/uniclock/lib/arduino-home-assistant/src/HADevice.h new file mode 100644 index 0000000..0949982 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/HADevice.h @@ -0,0 +1,183 @@ +#ifndef AHA_HADEVICE_H +#define AHA_HADEVICE_H + +#include + +class HASerializer; + +/** + * This class represents your device that's going to be registered in the Home Assistant devices registry. + * Each entity (HABinarySensor, HASensor, etc.) that you use will be owned by this device. + */ +class HADevice +{ +public: + /** + * Constructs HADevice without the unique ID. + * + * @note You will need to set the ID using HADevice::setUniqueId method. Otherwise none of the entities will work. + */ + HADevice(); + + /** + * Constructs HADevice with the given unique ID (string). + * Keep the unique ID short to save the memory. + * + * @param uniqueId String with the null terminator. + */ + HADevice(const char* uniqueId); + + /** + * Constructs HADevice using the given byte array as the unique ID. + * It works in the same way as HADevice::setUniqueId method. + * + * @param uniqueId Bytes array that's going to be converted into the string. + * @param length Number of bytes in the array. + */ + HADevice(const byte* uniqueId, const uint16_t length); + + /** + * Deletes HASerializer and the availability topic if the shared availability was enabled. + */ + ~HADevice(); + + /** + * Returns pointer to the unique ID. It can be nullptr if the device has no ID assigned. + */ + inline const char* getUniqueId() const + { return _uniqueId; } + + /** + * Returns the instance of the HASerializer used by the device. + * This method is used by all entities to serialize device's representation. + */ + inline const HASerializer* getSerializer() const + { return _serializer; } + + /** + * Returns true if the shared availability is enabled for the device. + */ + inline bool isSharedAvailabilityEnabled() const + { return _sharedAvailability; } + + /** + * Returns true if the extended unique IDs feature is enabled for the device. + */ + inline bool isExtendedUniqueIdsEnabled() const + { return _extendedUniqueIds; } + + /** + * Returns availability topic generated by the HADevice::enableSharedAvailability method. + * It can be nullptr if the shared availability is not enabled. + */ + inline const char* getAvailabilityTopic() const + { return _availabilityTopic; } + + /** + * Returns online/offline state of the device. + */ + inline bool isAvailable() const + { return _available; } + + /** + * Enables the use of extended unique IDs for all registered device types. + * The unique ID of each device type will be prefixed with the device's ID once enabled. + */ + inline void enableExtendedUniqueIds() + { _extendedUniqueIds = true; } + + /** + * Sets unique ID of the device based on the given byte array. + * Each byte is converted into a hex string representation, so the final length of the unique ID will be twice as given. + * + * @param uniqueId Bytes array that's going to be converted into the string. + * @param length Number of bytes in the array. + * @note The unique ID can be set only once (via constructor or using this method). + */ + bool setUniqueId(const byte* uniqueId, const uint16_t length); + + /** + * Sets the "manufacturer" property that's going to be displayed in the Home Assistant. + * + * @param manufacturer Any string. Keep it short to save the memory. + */ + void setManufacturer(const char* manufacturer); + + /** + * Sets the "model" property that's going to be displayed in the Home Assistant. + * + * @param model Any string. Keep it short to save the memory. + */ + void setModel(const char* model); + + /** + * Sets the "name" property that's going to be displayed in the Home Assistant. + * + * @param name Any string. Keep it short to save the memory. + */ + void setName(const char* name); + + /** + * Sets the "software version" property that's going to be displayed in the Home Assistant. + * + * @param softwareVersion Any string. Keep it short to save the memory. + */ + void setSoftwareVersion(const char* softwareVersion); + + /** + * Sets the "configuration URL" property that's going to be used by the Home Assistant. + * + * @param url Configuration URL to publish. + */ + void setConfigurationUrl(const char* url); + + /** + * Sets device's availability and publishes MQTT message on the availability topic. + * If the device is not connected to an MQTT broker or the shared availability is not enabled then nothing happens. + * + * @param online Set to true if the device should be displayed as available in the HA panel. + */ + void setAvailability(bool online); + + /** + * Enables the shared availability feature. + */ + bool enableSharedAvailability(); + + /** + * Enables MQTT LWT feature. + * Please note that the shared availability needs to be enabled first. + */ + void enableLastWill(); + + /** + * Publishes current availability of the device on the availability topic. + * If the device is not connected to an MQTT broker or the shared availability is not enabled then nothing happens. + * This method is called by the HAMqtt when the connection to an MQTT broker is acquired. + */ + void publishAvailability() const; + +private: + /// The unique ID of the device. It can be a memory allocated by HADevice::setUniqueId method. + const char* _uniqueId; + + /// Specifies whether HADevice class owns the _uniqueId pointer. + bool _ownsUniqueId; + + /// JSON serializer of the HADevice class. It's allocated in the constructor. + HASerializer* _serializer; + + /// The availability topic allocated by HADevice::enableSharedAvailability method. + char* _availabilityTopic; + + /// Specifies whether the shared availability is enabled. + bool _sharedAvailability; + + /// Specifies whether the device is available (online / offline). + bool _available; + + /// Specifies whether extended unique IDs feature is enabled. + bool _extendedUniqueIds; +}; + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/HAMqtt.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/HAMqtt.cpp new file mode 100644 index 0000000..ed1c15c --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/HAMqtt.cpp @@ -0,0 +1,359 @@ +#include "HAMqtt.h" + +#ifndef ARDUINOHA_TEST +#include +#endif + +#include "HADevice.h" +#include "device-types/HABaseDeviceType.h" +#include "mocks/PubSubClientMock.h" + +#define HAMQTT_INIT \ + _device(device), \ + _messageCallback(nullptr), \ + _connectedCallback(nullptr), \ + _disconnectedCallback(nullptr), \ + _stateChangedCallback(nullptr), \ + _initialized(false), \ + _discoveryPrefix(DefaultDiscoveryPrefix), \ + _dataPrefix(DefaultDataPrefix), \ + _username(nullptr), \ + _password(nullptr), \ + _lastConnectionAttemptAt(0), \ + _devicesTypesNb(0), \ + _maxDevicesTypesNb(maxDevicesTypesNb), \ + _devicesTypes(new HABaseDeviceType*[maxDevicesTypesNb]), \ + _lastWillTopic(nullptr), \ + _lastWillMessage(nullptr), \ + _lastWillRetain(false), \ + _currentState(StateDisconnected) + +static const char* DefaultDiscoveryPrefix = "homeassistant"; +static const char* DefaultDataPrefix = "aha"; + +HAMqtt* HAMqtt::_instance = nullptr; + +void onMessageReceived(char* topic, uint8_t* payload, unsigned int length) +{ + if (HAMqtt::instance() == nullptr || length > UINT16_MAX) { + return; + } + + HAMqtt::instance()->processMessage(topic, payload, static_cast(length)); +} + +#ifdef ARDUINOHA_TEST +HAMqtt::HAMqtt( + PubSubClientMock* pubSub, + HADevice& device, + uint8_t maxDevicesTypesNb +) : + _mqtt(pubSub), + HAMQTT_INIT +{ + _instance = this; +} +#else +HAMqtt::HAMqtt( + Client& netClient, + HADevice& device, + uint8_t maxDevicesTypesNb +) : + _mqtt(new PubSubClient(netClient)), + HAMQTT_INIT +{ + _instance = this; +} +#endif + +HAMqtt::~HAMqtt() +{ + delete[] _devicesTypes; + + if (_mqtt) { + delete _mqtt; + } + + _instance = nullptr; +} + +bool HAMqtt::begin( + const IPAddress serverIp, + const uint16_t serverPort, + const char* username, + const char* password +) +{ + ARDUINOHA_DEBUG_PRINT(F("AHA: init server ")) + ARDUINOHA_DEBUG_PRINT(serverIp) + ARDUINOHA_DEBUG_PRINT(F(":")) + ARDUINOHA_DEBUG_PRINTLN(serverPort) + + if (_device.getUniqueId() == nullptr) { + ARDUINOHA_DEBUG_PRINTLN(F("AHA: init failed. Missing device unique ID")) + return false; + } + + if (_initialized) { + ARDUINOHA_DEBUG_PRINTLN(F("AHA: already initialized")) + return false; + } + + _username = username; + _password = password; + _initialized = true; + + _mqtt->setServer(serverIp, serverPort); + _mqtt->setCallback(onMessageReceived); + + return true; +} + +bool HAMqtt::begin( + const IPAddress serverIp, + const char* username, + const char* password +) +{ + return begin(serverIp, HAMQTT_DEFAULT_PORT, username, password); +} + +bool HAMqtt::begin( + const char* serverHostname, + const uint16_t serverPort, + const char* username, + const char* password +) +{ + ARDUINOHA_DEBUG_PRINT(F("AHA: init server ")) + ARDUINOHA_DEBUG_PRINT(serverHostname) + ARDUINOHA_DEBUG_PRINT(F(":")) + ARDUINOHA_DEBUG_PRINTLN(serverPort) + + if (_device.getUniqueId() == nullptr) { + ARDUINOHA_DEBUG_PRINTLN(F("AHA: init failed. Missing device unique ID")) + return false; + } + + if (_initialized) { + ARDUINOHA_DEBUG_PRINTLN(F("AHA: already initialized")) + return false; + } + + _username = username; + _password = password; + _initialized = true; + + _mqtt->setServer(serverHostname, serverPort); + _mqtt->setCallback(onMessageReceived); + + return true; +} + +bool HAMqtt::begin( + const char* serverHostname, + const char* username, + const char* password +) +{ + return begin(serverHostname, HAMQTT_DEFAULT_PORT, username, password); +} + +bool HAMqtt::disconnect() +{ + if (!_initialized) { + return false; + } + + ARDUINOHA_DEBUG_PRINTLN(F("AHA: disconnecting")) + + _initialized = false; + _lastConnectionAttemptAt = 0; + _mqtt->disconnect(); + + return true; +} + +void HAMqtt::loop() +{ + if (!_initialized) { + return; + } + + bool result = _mqtt->loop(); + if (_currentState != _mqtt->state()) { + setState(static_cast(_mqtt->state())); + } + + if (!result) { + connectToServer(); + } +} + +bool HAMqtt::isConnected() const +{ + return _mqtt->connected(); +} + +void HAMqtt::setKeepAlive(uint16_t keepAlive) +{ + _mqtt->setKeepAlive(keepAlive); +} + +bool HAMqtt::setBufferSize(uint16_t size) +{ + return _mqtt->setBufferSize(size); +} + +void HAMqtt::addDeviceType(HABaseDeviceType* deviceType) +{ + if (_devicesTypesNb + 1 > _maxDevicesTypesNb) { + return; + } + + _devicesTypes[_devicesTypesNb++] = deviceType; +} + +bool HAMqtt::publish(const char* topic, const char* payload, bool retained) +{ + if (!isConnected()) { + return false; + } + + ARDUINOHA_DEBUG_PRINT(F("AHA: publishing ")) + ARDUINOHA_DEBUG_PRINT(topic) + ARDUINOHA_DEBUG_PRINT(F(", len: ")) + ARDUINOHA_DEBUG_PRINTLN(strlen(payload)) + + _mqtt->beginPublish(topic, strlen(payload), retained); + _mqtt->write((const uint8_t*)(payload), strlen(payload)); + return _mqtt->endPublish(); +} + +bool HAMqtt::beginPublish( + const char* topic, + uint16_t payloadLength, + bool retained +) +{ + ARDUINOHA_DEBUG_PRINT(F("AHA: begin publish ")) + ARDUINOHA_DEBUG_PRINT(topic) + ARDUINOHA_DEBUG_PRINT(F(", len: ")) + ARDUINOHA_DEBUG_PRINTLN(payloadLength) + + return _mqtt->beginPublish(topic, payloadLength, retained); +} + +void HAMqtt::writePayload(const char* data, const uint16_t length) +{ + writePayload(reinterpret_cast(data), length); +} + +void HAMqtt::writePayload(const uint8_t* data, const uint16_t length) +{ + _mqtt->write(data, length); +} + +void HAMqtt::writePayload(const __FlashStringHelper* src) +{ + _mqtt->print(src); +} + +bool HAMqtt::endPublish() +{ + return _mqtt->endPublish(); +} + +bool HAMqtt::subscribe(const char* topic) +{ + ARDUINOHA_DEBUG_PRINT(F("AHA: subscribing ")) + ARDUINOHA_DEBUG_PRINTLN(topic) + + return _mqtt->subscribe(topic); +} + +void HAMqtt::processMessage(const char* topic, const uint8_t* payload, uint16_t length) +{ + ARDUINOHA_DEBUG_PRINT(F("AHA: received call ")) + ARDUINOHA_DEBUG_PRINT(topic) + ARDUINOHA_DEBUG_PRINT(F(", len: ")) + ARDUINOHA_DEBUG_PRINTLN(length) + + if (_messageCallback) { + _messageCallback(topic, payload, length); + } + + for (uint8_t i = 0; i < _devicesTypesNb; i++) { + _devicesTypes[i]->onMqttMessage(topic, payload, length); + } +} + +void HAMqtt::connectToServer() +{ + if (_lastConnectionAttemptAt > 0 && + (millis() - _lastConnectionAttemptAt) < ReconnectInterval) { + return; + } + + _lastConnectionAttemptAt = millis(); + setState(StateConnecting); + + ARDUINOHA_DEBUG_PRINT(F("AHA: MQTT connecting, client ID: ")) + ARDUINOHA_DEBUG_PRINTLN(_device.getUniqueId()) + + _mqtt->connect( + _device.getUniqueId(), + _username, + _password, + _lastWillTopic, + 0, + _lastWillRetain, + _lastWillMessage, + true + ); + + if (isConnected()) { + setState(StateConnected); + } else { + ARDUINOHA_DEBUG_PRINTLN(F("AHA: failed to connect")) + } +} + +void HAMqtt::onConnectedLogic() +{ + if (_connectedCallback) { + _connectedCallback(); + } + + _device.publishAvailability(); + + for (uint8_t i = 0; i < _devicesTypesNb; i++) { + _devicesTypes[i]->onMqttConnected(); + } +} + +void HAMqtt::setState(ConnectionState state) +{ + ConnectionState previousState = _currentState; + _currentState = state; + + ARDUINOHA_DEBUG_PRINT(F("AHA: MQTT state changed to ")) + ARDUINOHA_DEBUG_PRINT(_currentState) + ARDUINOHA_DEBUG_PRINT(F(", previous state: ")) + ARDUINOHA_DEBUG_PRINTLN(previousState) + + if (_currentState == StateConnected) { + ARDUINOHA_DEBUG_PRINTLN(F("AHA: MQTT connected")) + onConnectedLogic(); + } else if (previousState == StateConnected && _currentState != StateConnected) { + ARDUINOHA_DEBUG_PRINTLN(F("AHA: MQTT disconnected")) + + if (_disconnectedCallback) { + _disconnectedCallback(); + } + } + + if (_stateChangedCallback) { + _stateChangedCallback(_currentState); + } +} \ No newline at end of file diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/HAMqtt.h b/firmware/uniclock/lib/arduino-home-assistant/src/HAMqtt.h new file mode 100644 index 0000000..915d846 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/HAMqtt.h @@ -0,0 +1,465 @@ +#ifndef AHA_HAMQTT_H +#define AHA_HAMQTT_H + +#include +#include +#include +#include "ArduinoHADefines.h" + +#define HAMQTT_CALLBACK(name) void (*name)() +#define HAMQTT_STATE_CALLBACK(name) void (*name)(ConnectionState state) +#define HAMQTT_MESSAGE_CALLBACK(name) void (*name)(const char* topic, const uint8_t* payload, uint16_t length) +#define HAMQTT_DEFAULT_PORT 1883 + +#ifdef ARDUINOHA_TEST +class PubSubClientMock; +#else +class PubSubClient; +#endif + +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) +#define HAMQTT_DEFAULT_DEVICES_LIMIT 6 +#else +#define HAMQTT_DEFAULT_DEVICES_LIMIT 24 +#endif + +class HADevice; +class HABaseDeviceType; + +#if defined(ARDUINO_API_VERSION) +using namespace arduino; +#endif + +/** + * This class is a wrapper for the PubSub API. + * It's a central point of the library where instances of all device types are stored. + */ +class HAMqtt +{ +public: + enum ConnectionState { + StateConnecting = -5, + StateConnectionTimeout = -4, + StateConnectionLost = -3, + StateConnectionFailed = -2, + StateDisconnected = -1, + StateConnected = 0, + StateBadProtocol = 1, + StateBadClientId = 2, + StateUnavailable = 3, + StateBadCredentials = 4, + StateUnauthorized = 5 + }; + + /** + * Returns existing instance (singleton) of the HAMqtt class. + * It may be a null pointer if the HAMqtt object was never constructed or it was destroyed. + */ + inline static HAMqtt* instance() + { return _instance; } + +#ifdef ARDUINOHA_TEST + explicit HAMqtt( + PubSubClientMock* pubSub, + HADevice& device, + const uint8_t maxDevicesTypesNb = HAMQTT_DEFAULT_DEVICES_LIMIT + ); +#else + /** + * Creates a new instance of the HAMqtt class. + * Please note that only one instance of the class can be initialized at the same time. + * + * @param netClient The EthernetClient or WiFiClient that's going to be used for the network communication. + * @param device An instance of the HADevice class representing your device. + * @param maxDevicesTypesNb The maximum number of device types (sensors, switches, etc.) that you're going to implement. + */ + explicit HAMqtt( + Client& netClient, + HADevice& device, + const uint8_t maxDevicesTypesNb = HAMQTT_DEFAULT_DEVICES_LIMIT + ); +#endif + + /** + * Removes singleton of the HAMqtt class. + */ + ~HAMqtt(); + + /** + * Sets the prefix of the Home Assistant discovery topics. + * It needs to match the prefix set in the HA admin panel. + * The default prefix is "homeassistant". + * + * @param prefix The discovery topics' prefix. + */ + inline void setDiscoveryPrefix(const char* prefix) + { _discoveryPrefix = prefix; } + + /** + * Returns the discovery topics' prefix. + */ + inline const char* getDiscoveryPrefix() const + { return _discoveryPrefix; } + + /** + * Sets prefix of the data topics. + * It may be useful if you want to pass MQTT traffic through a bridge. + * The default prefix is "aha". + * + * @param prefix The data topics' prefix. + */ + inline void setDataPrefix(const char* prefix) + { _dataPrefix = prefix; } + + /** + * Returns the data topics' prefix. + */ + inline const char* getDataPrefix() const + { return _dataPrefix; } + + /** + * Returns instance of the device assigned to the HAMqtt class. + * It's the same object (pointer) that was passed to the HAMqtt constructor. + */ + inline HADevice const* getDevice() const + { return &_device; } + + /** + * Registers a new callback method that will be called when the device receives an MQTT message. + * Please note that the callback is also fired by internal MQTT messages used by the library. + * You should always verify the topic of the received message. + * + * @param callback Callback method. + */ + inline void onMessage(HAMQTT_MESSAGE_CALLBACK(callback)) + { _messageCallback = callback; } + + /** + * Registers a new callback method that will be called each time a connection to the MQTT broker is acquired. + * The callback is also fired after reconnecting to the broker. + * You can use this method to register topics' subscriptions. + * + * @param callback Callback method. + */ + inline void onConnected(HAMQTT_CALLBACK(callback)) + { _connectedCallback = callback; } + + /** + * Registers a new callback method that will be called each time the connection to the MQTT broker is lost. + * + * @param callback Callback method. + */ + inline void onDisconnected(HAMQTT_CALLBACK(callback)) + { _disconnectedCallback = callback; } + + /** + * Registers a new callback method that will be called each time the connection state changes. + * + * @param callback Callback method. + */ + inline void onStateChanged(HAMQTT_STATE_CALLBACK(callback)) + { _stateChangedCallback = callback; } + + /** + * Returns the current state of the MQTT connection. + */ + inline ConnectionState getState() const + { return _currentState; } + + /** + * Sets parameters of the MQTT connection using the IP address and port. + * The library will try to connect to the broker in first loop cycle. + * Please note that the library automatically reconnects to the broker if connection is lost. + * + * @param serverIp IP address of the MQTT broker. + * @param serverPort Port of the MQTT broker. + * @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed. + * @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed. + */ + bool begin( + const IPAddress serverIp, + const uint16_t serverPort = HAMQTT_DEFAULT_PORT, + const char* username = nullptr, + const char* password = nullptr + ); + + /** + * Sets parameters of the MQTT connection using the IP address and the default port (1883). + * The library will try to connect to the broker in first loop cycle. + * Please note that the library automatically reconnects to the broker if connection is lost. + * + * @param serverIp IP address of the MQTT broker. + * @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed. + * @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed. + */ + bool begin( + const IPAddress serverIp, + const char* username, + const char* password + ); + + /** + * Sets parameters of the MQTT connection using the hostname and port. + * The library will try to connect to the broker in first loop cycle. + * Please note that the library automatically reconnects to the broker if connection is lost. + * + * @param serverHostname Hostname of the MQTT broker. + * @param serverPort Port of the MQTT broker. + * @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed. + * @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed. + */ + bool begin( + const char* serverHostname, + const uint16_t serverPort = HAMQTT_DEFAULT_PORT, + const char* username = nullptr, + const char* password = nullptr + ); + + /** + * Sets parameters of the MQTT connection using the hostname and the default port (1883). + * The library will try to connect to the broker in first loop cycle. + * Please note that the library automatically reconnects to the broker if connection is lost. + * + * @param serverHostname Hostname of the MQTT broker. + * @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed. + * @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed. + */ + bool begin( + const char* serverHostname, + const char* username, + const char* password + ); + + /** + * Closes the MQTT connection. + */ + bool disconnect(); + + /** + * This method should be called periodically inside the main loop of the firmware. + * It's safe to call this method in some interval (like 5ms). + */ + void loop(); + + /** + * Returns true if connection to the MQTT broker is established. + */ + bool isConnected() const; + + /** + * Sets keep alive for the MQTT connection. + * By default it's 15 seconds. + * + * @param keepAlive Number of seconds to keep connection alive. + */ + void setKeepAlive(uint16_t keepAlive); + + /** + * Sets the buffer size for the MQTT connection. + * By default it's 256 bytes. + * + * @param size Size of the buffer. + */ + bool setBufferSize(uint16_t size); + + /** + * Adds a new device's type to the MQTT. + * Each time the connection with MQTT broker is acquired, the HAMqtt class + * calls "onMqttConnected" method in all devices' types instances. + * + * @note The HAMqtt class doesn't take ownership of the given pointer. + * @param deviceType Instance of the device's type (HASwitch, HABinarySensor, etc.). + */ + void addDeviceType(HABaseDeviceType* deviceType); + + /** + * Publishes the MQTT message with given topic and payload. + * Message won't be published if the connection with the MQTT broker is not established. + * In this case method returns false. + * + * @param topic The topic to publish. + * @param payload The payload to publish (it may be empty const char). + * @param retained Specifies whether message should be retained. + */ + bool publish(const char* topic, const char* payload, bool retained = false); + + /** + * Begins publishing of a message with the given properties. + * When this method returns true the payload can be written using HAMqtt::writePayload method. + * + * @param topic Topic of the published message. + * @param payloadLength Length of the payload (bytes) that's going to be published. + * @param retained Specifies whether the published message should be retained. + */ + bool beginPublish(const char* topic, uint16_t payloadLength, bool retained = false); + + /** + * Writes given string to the TCP stream. + * Please note that before writing any data the HAMqtt::beginPublish method + * needs to be called. + * + * @param data The string to publish. + * @param length Length of the data (bytes). + */ + void writePayload(const char* data, const uint16_t length); + + /** + * Writes given data to the TCP stream. + * Please note that before writing any data the HAMqtt::beginPublish method + * needs to be called. + * + * @param data The data to publish. + * @param length Length of the data (bytes). + */ + void writePayload(const uint8_t* data, const uint16_t length); + + /** + * Writes given progmem data to the TCP stream. + * Please note that before writing any data the HAMqtt::beginPublish method + * needs to be called. + * + * @param data Progmem data to publish. + */ + void writePayload(const __FlashStringHelper* data); + + /** + * Finishes publishing of a message. + * After calling this method the message will be processed by the broker. + */ + bool endPublish(); + + /** + * Subscribes to the given topic. + * Whenever a new message is received the onMqttMessage callback in all + * devices types is called. + * + * Please note that you need to subscribe topic each time the connection + * with the broker is acquired. + * + * @param topic Topic to subscribe. + */ + bool subscribe(const char* topic); + + /** + * Enables the last will message that will be produced when the device disconnects from the broker. + * If you want to change availability of the device in Home Assistant panel + * please use enableLastWill() method from the HADevice class instead. + * + * @param lastWillTopic The topic to publish. + * @param lastWillMessage The message (payload) to publish. + * @param lastWillRetain Specifies whether the published message should be retained. + */ + inline void setLastWill( + const char* lastWillTopic, + const char* lastWillMessage, + bool lastWillRetain + ) { + _lastWillTopic = lastWillTopic; + _lastWillMessage = lastWillMessage; + _lastWillRetain = lastWillRetain; + } + + /** + * Processes MQTT message received from the broker (subscription). + * + * @note Do not use this method on your own. It's only for the internal purpose. + * @param topic Topic of the message. + * @param payload Content of the message. + * @param length Length of the message. + */ + void processMessage(const char* topic, const uint8_t* payload, uint16_t length); + +#ifdef ARDUINOHA_TEST + inline uint8_t getDevicesTypesNb() const + { return _devicesTypesNb; } + + inline HABaseDeviceType** getDevicesTypes() const + { return _devicesTypes; } +#endif + +private: + /// Interval between MQTT reconnects (milliseconds). + static const uint16_t ReconnectInterval = 10000; + + /// Living instance of the HAMqtt class. It can be nullptr. + static HAMqtt* _instance; + + /** + * Attempts to connect to the MQTT broker. + * The method uses properties passed to the "begin" method. + */ + void connectToServer(); + + /** + * This method is called each time the connection with MQTT broker is acquired. + */ + void onConnectedLogic(); + + /** + * Sets the state of the MQTT connection. + */ + void setState(ConnectionState state); + +#ifdef ARDUINOHA_TEST + PubSubClientMock* _mqtt; +#else + /// Instance of the PubSubClient class. It's initialized in the constructor. + PubSubClient* _mqtt; +#endif + + /// Instance of the HADevice passed to the constructor. + const HADevice& _device; + + /// The callback method that will be called when an MQTT message is received. + HAMQTT_MESSAGE_CALLBACK(_messageCallback); + + /// The callback method that will be called when the MQTT connection is acquired. + HAMQTT_CALLBACK(_connectedCallback); + + /// The callback method that will be called when the MQTT connection is lost. + HAMQTT_CALLBACK(_disconnectedCallback); + + /// The callback method that will be called when the MQTT connection state changes. + HAMQTT_STATE_CALLBACK(_stateChangedCallback); + + /// Specifies whether the HAMqtt::begin method was ever called. + bool _initialized; + + /// Teh discovery prefix that's used for the configuration messages. + const char* _discoveryPrefix; + + /// The data prefix that's used for publishing data messages. + const char* _dataPrefix; + + /// The username used for the authentication. It's set in the HAMqtt::begin method. + const char* _username; + + /// The password used for the authentication. It's set in the HAMqtt::begin method. + const char* _password; + + /// Time of the last connection attemps (milliseconds since boot). + uint32_t _lastConnectionAttemptAt; + + /// The amount of registered devices types. + uint8_t _devicesTypesNb; + + /// The maximum amount of devices types that can be registered. + uint8_t _maxDevicesTypesNb; + + /// Pointers of all registered devices types (array of pointers). + HABaseDeviceType** _devicesTypes; + + /// The last will topic set by HAMqtt::setLastWill + const char* _lastWillTopic; + + /// The last will message set by HAMqtt::setLastWill + const char* _lastWillMessage; + + /// The last will retain set by HAMqtt::setLastWill + bool _lastWillRetain; + + /// The last known state of the MQTT connection. + ConnectionState _currentState; +}; + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABaseDeviceType.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABaseDeviceType.cpp new file mode 100644 index 0000000..17583e3 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABaseDeviceType.cpp @@ -0,0 +1,206 @@ +#include "HABaseDeviceType.h" +#include "../HAMqtt.h" +#include "../HADevice.h" +#include "../utils/HAUtils.h" +#include "../utils/HASerializer.h" + +HABaseDeviceType::HABaseDeviceType( + const __FlashStringHelper* componentName, + const char* uniqueId +) : + _componentName(componentName), + _uniqueId(uniqueId), + _name(nullptr), + _objectId(nullptr), + _serializer(nullptr), + _availability(AvailabilityDefault) +{ + if (mqtt()) { + mqtt()->addDeviceType(this); + } +} + +void HABaseDeviceType::setAvailability(bool online) +{ + _availability = (online ? AvailabilityOnline : AvailabilityOffline); + publishAvailability(); +} + +HAMqtt* HABaseDeviceType::mqtt() +{ + return HAMqtt::instance(); +} + +void HABaseDeviceType::subscribeTopic( + const char* uniqueId, + const __FlashStringHelper* topic +) +{ + const uint16_t topicLength = HASerializer::calculateDataTopicLength( + uniqueId, + topic + ); + if (topicLength == 0) { + return; + } + + char fullTopic[topicLength]; + if (!HASerializer::generateDataTopic( + fullTopic, + uniqueId, + topic + )) { + return; + } + + HAMqtt::instance()->subscribe(fullTopic); +} + +void HABaseDeviceType::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + (void)topic; + (void)payload; + (void)length; +} + +void HABaseDeviceType::destroySerializer() +{ + if (_serializer) { + delete _serializer; + _serializer = nullptr; + } +} + +void HABaseDeviceType::publishConfig() +{ + buildSerializer(); + + if (_serializer == nullptr) { + return; + } + + const uint16_t topicLength = HASerializer::calculateConfigTopicLength( + componentName(), + uniqueId() + ); + const uint16_t dataLength = _serializer->calculateSize(); + + if (topicLength > 0 && dataLength > 0) { + char topic[topicLength]; + HASerializer::generateConfigTopic( + topic, + componentName(), + uniqueId() + ); + + if (mqtt()->beginPublish(topic, dataLength, true)) { + _serializer->flush(); + mqtt()->endPublish(); + } + } + + destroySerializer(); +} + +void HABaseDeviceType::publishAvailability() +{ + const HADevice* device = mqtt()->getDevice(); + if ( + !device || + device->isSharedAvailabilityEnabled() || + !isAvailabilityConfigured() + ) { + return; + } + + publishOnDataTopic( + AHATOFSTR(HAAvailabilityTopic), + _availability == AvailabilityOnline + ? AHATOFSTR(HAOnline) + : AHATOFSTR(HAOffline), + true + ); +} + +bool HABaseDeviceType::publishOnDataTopic( + const __FlashStringHelper* topic, + const __FlashStringHelper* payload, + bool retained +) +{ + if (!payload) { + return false; + } + + return publishOnDataTopic( + topic, + reinterpret_cast(payload), + strlen_P(AHAFROMFSTR(payload)), + retained, + true + ); +} + +bool HABaseDeviceType::publishOnDataTopic( + const __FlashStringHelper* topic, + const char* payload, + bool retained +) +{ + if (!payload) { + return false; + } + + return publishOnDataTopic( + topic, + reinterpret_cast(payload), + strlen(payload), + retained + ); +} + +bool HABaseDeviceType::publishOnDataTopic( + const __FlashStringHelper* topic, + const uint8_t* payload, + const uint16_t length, + bool retained, + bool isProgmemData +) +{ + if (!payload) { + return false; + } + + const uint16_t topicLength = HASerializer::calculateDataTopicLength( + uniqueId(), + topic + ); + if (topicLength == 0) { + return false; + } + + char fullTopic[topicLength]; + if (!HASerializer::generateDataTopic( + fullTopic, + uniqueId(), + topic + )) { + return false; + } + + if (mqtt()->beginPublish(fullTopic, length, retained)) { + if (isProgmemData) { + mqtt()->writePayload(AHATOFSTR(payload)); + } else { + mqtt()->writePayload(payload, length); + } + + return mqtt()->endPublish(); + } + + return false; +} \ No newline at end of file diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABaseDeviceType.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABaseDeviceType.h new file mode 100644 index 0000000..0d6fa32 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABaseDeviceType.h @@ -0,0 +1,246 @@ +#ifndef AHA_HABASEDEVICETYPE_H +#define AHA_HABASEDEVICETYPE_H + +#include +#include "../ArduinoHADefines.h" + +class HAMqtt; +class HASerializer; + +class HABaseDeviceType +{ +public: + enum NumberPrecision { + /// No digits after the decimal point. + PrecisionP0 = 0, + + /// One digit after the decimal point. + PrecisionP1, + + /// Two digits after the decimal point. + PrecisionP2, + + /// Three digits after the decimal point. + PrecisionP3 + }; + + /** + * Creates a new device type instance and registers it in the HAMqtt class. + * + * @param componentName The name of the Home Assistant component (e.g. `binary_sensor`). + * You can find all available component names in the Home Assistant documentation. + * The component name needs to be stored in the flash memory. + * @param uniqueId The unique ID of the device type. It needs to be unique in a scope of the HADevice. + */ + HABaseDeviceType( + const __FlashStringHelper* componentName, + const char* uniqueId + ); + + /** + * Returns unique ID of the device type. + */ + inline const char* uniqueId() const + { return _uniqueId; } + + /** + * Returns component name defined by the device type. + * It's used for the MQTT discovery topic. + */ + inline const __FlashStringHelper* componentName() const + { return _componentName; } + + /** + * Returns `true` if the availability was configured for this device type. + */ + inline bool isAvailabilityConfigured() const + { return (_availability != AvailabilityDefault); } + + /** + * Returns online state of the device type. + */ + inline bool isOnline() const + { return (_availability == AvailabilityOnline); } + + /** + * Sets name of the device type that will be used as a label in the HA panel. + * Keep the name short to save the resources. + * + * @param name The device type name. + */ + inline void setName(const char* name) + { _name = name; } + + /** + * Returns name of the deviced type that was assigned via setName method. + * It can be nullptr if there is no name assigned. + */ + inline const char* getName() const + { return _name; } + + /** + * Sets object ID that will be used by HA to generate entity ID. + * Keep the ID short to save the resources. + * + * @param objectId The object ID. + */ + inline void setObjectId(const char* objectId) + { _objectId = objectId; } + + /** + * Returns the object ID that was set by setObjectId method. + * It can be nullptr if there is no ID assigned. + */ + inline const char* getObjectId() const + { return _objectId; } + + /** + * Sets availability of the device type. + * Setting the initial availability enables availability reporting for this device type. + * Please note that not all device types support this feature. + * Follow HA documentation of a specific device type to get more information. + * + * @param online Specifies whether the device type is online. + */ + virtual void setAvailability(bool online); + +#ifdef ARDUINOHA_TEST + inline HASerializer* getSerializer() const + { return _serializer; } + + inline void buildSerializerTest() + { buildSerializer(); } +#endif + +protected: + /** + * Returns instance of the HAMqtt class. + */ + static HAMqtt* mqtt(); + + /** + * Subscribes to the given data topic. + * + * @param uniqueId THe unique ID of the device type assigned via the constructor. + * @param topic Topic to subscribe (progmem string). + */ + static void subscribeTopic( + const char* uniqueId, + const __FlashStringHelper* topic + ); + + /** + * This method should build serializer that will be used for publishing the configuration. + * The serializer is built each time the MQTT connection is acquired. + * Follow implementation of the existing device types to get better understanding of the logic. + */ + virtual void buildSerializer() { }; + + /** + * This method is called each time the MQTT connection is acquired. + * Each device type should publish its configuration and availability. + * It can be also used for subscribing to MQTT topics. + */ + virtual void onMqttConnected() = 0; + + /** + * This method is called each time the device receives a MQTT message. + * It can be any MQTT message so the method should always verify the topic. + * + * @param topic The topic on which the message was produced. + * @param payload The payload of the message. It can be nullptr. + * @param length The length of the payload. + */ + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ); + + /** + * Destroys the existing serializer. + */ + void destroySerializer(); + + /** + * Publishes configuration of this device type on the HA discovery topic. + */ + void publishConfig(); + + /** + * Publishes current availability of the device type. + * The message is only produced if the availability is configured for this device type. + */ + void publishAvailability(); + + /** + * Publishes the given flash string on the data topic. + * + * @param topic The topic to publish on (progmem string). + * @param payload The message's payload (progmem string). + * @param retained Specifies whether the message should be retained. + */ + bool publishOnDataTopic( + const __FlashStringHelper* topic, + const __FlashStringHelper* payload, + bool retained = false + ); + + /** + * Publishes the given string on the data topic. + * + * @param topic The topic to publish on (progmem string). + * @param payload The message's payload. + * @param retained Specifies whether the message should be retained. + */ + bool publishOnDataTopic( + const __FlashStringHelper* topic, + const char* payload, + bool retained = false + ); + + /** + * Publishes the given data on the data topic. + * + * @param topic The topic to publish on (progmem string). + * @param payload The message's payload. + * @param length The length of the payload. + * @param retained Specifies whether the message should be retained. + * @param isProgmemData Specifies whether the given data is stored in the flash memory. + */ + bool publishOnDataTopic( + const __FlashStringHelper* topic, + const uint8_t* payload, + const uint16_t length, + bool retained = false, + bool isProgmemData = false + ); + + /// The component name that was assigned via the constructor. + const __FlashStringHelper* const _componentName; + + /// The unique ID that was assigned via the constructor. + const char* _uniqueId; + + /// The name that was set using setName method. It can be nullptr. + const char* _name; + + /// The object ID that was set using setObjectId method. It can be nullptr. + const char* _objectId; + + /// HASerializer that belongs to this device type. It can be nullptr. + HASerializer* _serializer; + +private: + enum Availability { + AvailabilityDefault = 0, + AvailabilityOnline, + AvailabilityOffline + }; + + /// The current availability of this device type. AvailabilityDefault means that the initial availability was never set. + Availability _availability; + friend class HAMqtt; +}; + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABinarySensor.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABinarySensor.cpp new file mode 100644 index 0000000..20ab76f --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABinarySensor.cpp @@ -0,0 +1,85 @@ +#include "HABinarySensor.h" +#ifndef EX_ARDUINOHA_BINARY_SENSOR + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HABinarySensor::HABinarySensor(const char* uniqueId) : + HABaseDeviceType(AHATOFSTR(HAComponentBinarySensor), uniqueId), + _class(nullptr), + _icon(nullptr), + _currentState(false) +{ + +} + +bool HABinarySensor::setState(const bool state, const bool force) +{ + if (!force && state == _currentState) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +void HABinarySensor::setExpireAfter(uint16_t expireAfter) +{ + if (expireAfter > 0) { + _expireAfter.setBaseValue(expireAfter); + } else { + _expireAfter.reset(); + } +} + +void HABinarySensor::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 9); // 9 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_expireAfter.isSet()) { + _serializer->set( + AHATOFSTR(HAExpireAfterProperty), + &_expireAfter, + HASerializer::NumberPropertyType + ); + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); +} + +void HABinarySensor::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + publishState(_currentState); +} + +bool HABinarySensor::publishState(const bool state) +{ + return publishOnDataTopic( + AHATOFSTR(HAStateTopic), + AHATOFSTR(state ? HAStateOn : HAStateOff), + true + ); +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABinarySensor.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABinarySensor.h new file mode 100644 index 0000000..518e546 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HABinarySensor.h @@ -0,0 +1,103 @@ +#ifndef AHA_HABINARYSENSOR_H +#define AHA_HABINARYSENSOR_H + +#include "HABaseDeviceType.h" +#include "../utils/HANumeric.h" + +#ifndef EX_ARDUINOHA_BINARY_SENSOR + +/** + * HABinarySensor represents a binary sensor that allows publishing on/off state to the Home Assistant panel. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/binary_sensor.mqtt/ + */ +class HABinarySensor : public HABaseDeviceType +{ +public: + /** + * @param uniqueId The unique ID of the button. It needs to be unique in a scope of your device. + */ + HABinarySensor(const char* uniqueId); + + /** + * Changes state of the sensor and publish MQTT message. + * Please note that if a new value is the same as the previous one the MQTT message won't be published. + * + * @param state New state of the sensor (`true` - on, `false` - off). + * @param force Forces to update the state without comparing it to a previous known state. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool setState(const bool state, const bool force = false); + + /** + * Sets the number of seconds after the sensor’s state expires, if it’s not updated. + * By default the sensors state never expires. + * + * @param expireAfter The number of seconds. + */ + void setExpireAfter(uint16_t expireAfter); + + /** + * Sets the current state of the sensor without publishing it to Home Assistant. + * This method may be useful if you want to change the state before the connection with the MQTT broker is acquired. + * + * @param state New state of the sensor. + */ + inline void setCurrentState(const bool state) + { _currentState = state; } + + /** + * Returns the last known state of the sensor. + */ + inline bool getCurrentState() const + { return _currentState; } + + /** + * Sets class of the device. + * You can find list of available values here: https://www.home-assistant.io/integrations/binary_sensor/#device-class + * + * @param deviceClass The class name. + */ + inline void setDeviceClass(const char* deviceClass) + { _class = deviceClass; } + + /** + * Sets icon of the sensor. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + +private: + /** + * Publishes the MQTT message with the given state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishState(bool state); + + /// The device class. It can be nullptr. + const char* _class; + + /// The icon of the sensor. It can be nullptr. + const char* _icon; + + /// It defines the number of seconds after the sensor’s state expires, if it’s not updated. By default the sensors state never expires. + HANumeric _expireAfter; + + /// Current state of the sensor. By default it's false. + bool _currentState; + +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAButton.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAButton.cpp new file mode 100644 index 0000000..4a913c6 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAButton.cpp @@ -0,0 +1,73 @@ +#include "HAButton.h" +#ifndef EX_ARDUINOHA_BUTTON + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HAButton::HAButton(const char* uniqueId) : + HABaseDeviceType(AHATOFSTR(HAComponentButton), uniqueId), + _class(nullptr), + _icon(nullptr), + _retain(false), + _commandCallback(nullptr) +{ + +} + +void HAButton::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 9); // 9 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + + // optional property + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HACommandTopic)); +} + +void HAButton::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); +} + +void HAButton::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + (void)payload; + (void)length; + + if (_commandCallback && HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HACommandTopic) + )) { + _commandCallback(this); + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAButton.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAButton.h new file mode 100644 index 0000000..58c88fd --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAButton.h @@ -0,0 +1,86 @@ +#ifndef AHA_HABUTTON_H +#define AHA_HABUTTON_H + +#include "HABaseDeviceType.h" + +#ifndef EX_ARDUINOHA_BUTTON + +#define HABUTTON_CALLBACK(name) void (*name)(HAButton* sender) + +/** + * HAButton represents a button that's displayed in the Home Assistant panel and + * triggers some logic on your Arduino/ESP device once clicked. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/button.mqtt/ + */ +class HAButton : public HABaseDeviceType +{ +public: + /** + * @param uniqueId The unique ID of the button. It needs to be unique in a scope of your device. + */ + HAButton(const char* uniqueId); + + /** + * Sets class of the device. + * You can find list of available values here: https://www.home-assistant.io/integrations/button/#device-class + * + * @param deviceClass The class name. + */ + inline void setDeviceClass(const char* deviceClass) + { _class = deviceClass; } + + /** + * Sets icon of the button. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the button's command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Registers callback that will be called each time the press command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same button. + * + * @param callback + */ + inline void onCommand(HABUTTON_CALLBACK(callback)) + { _commandCallback = callback; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /// The device class. It can be nullptr. + const char* _class; + + /// The icon of the button. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The command callback that will be called once clicking the button in HA panel. + HABUTTON_CALLBACK(_commandCallback); +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACamera.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACamera.cpp new file mode 100644 index 0000000..8a9650d --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACamera.cpp @@ -0,0 +1,66 @@ +#include "HACamera.h" +#ifndef EX_ARDUINOHA_CAMERA + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HACamera::HACamera(const char* uniqueId) : + HABaseDeviceType(AHATOFSTR(HAComponentCamera), uniqueId), + _encoding(EncodingBinary), + _icon(nullptr) +{ + +} + +bool HACamera::publishImage(const uint8_t* data, const uint16_t length) +{ + if (!data) { + return false; + } + + return publishOnDataTopic(AHATOFSTR(HATopic), data, length, true); +} + +void HACamera::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 8); // 8 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + _serializer->set( + AHATOFSTR(HAEncodingProperty), + getEncodingProperty(), + HASerializer::ProgmemPropertyValue + ); + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HATopic)); +} + +void HACamera::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); +} + +const __FlashStringHelper* HACamera::getEncodingProperty() const +{ + switch (_encoding) { + case EncodingBase64: + return AHATOFSTR(HAEncodingBase64); + + default: + return nullptr; + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACamera.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACamera.h new file mode 100644 index 0000000..c2bcc2b --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACamera.h @@ -0,0 +1,77 @@ +#ifndef AHA_HACAMERA_H +#define AHA_HACAMERA_H + +#include "HABaseDeviceType.h" + +#ifndef EX_ARDUINOHA_CAMERA + +/** + * HACamera allows to display an image in the Home Assistant panel. + * It can be used for publishing an image from the ESP32-Cam module or any other + * module that's equipped with a camera. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/camera.mqtt/ + */ +class HACamera : public HABaseDeviceType +{ +public: + enum ImageEncoding { + EncodingBinary = 1, + EncodingBase64 + }; + + /** + * @param uniqueId The unique ID of the camera. It needs to be unique in a scope of your device. + */ + HACamera(const char* uniqueId); + + /** + * Publishes MQTT message with the given image data as a message content. + * It updates image displayed in the Home Assistant panel. + * + * @param data Image data (raw binary data or base64) + * @param length The length of the data. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool publishImage(const uint8_t* data, const uint16_t length); + + /** + * Sets encoding of the image content. + * Bu default Home Assistant expects raw binary data (e.g. JPEG binary data). + * + * @param encoding The image's data encoding. + */ + inline void setEncoding(const ImageEncoding encoding) + { _encoding = encoding; } + + /** + * Sets icon of the camera. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + +private: + /** + * Returns progmem string representing the encoding property. + */ + const __FlashStringHelper* getEncodingProperty() const; + + /// The encoding of the image's data. By default it's `HACamera::EncodingBinary`. + ImageEncoding _encoding; + + /// The icon of the camera. It can be nullptr. + const char* _icon; + +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACover.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACover.cpp new file mode 100644 index 0000000..78cd5cd --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACover.cpp @@ -0,0 +1,184 @@ +#include "HACover.h" +#ifndef EX_ARDUINOHA_COVER + +#include "../HAMqtt.h" +#include "../utils/HAUtils.h" +#include "../utils/HANumeric.h" +#include "../utils/HASerializer.h" + +HACover::HACover(const char* uniqueId, const Features features) : + HABaseDeviceType(AHATOFSTR(HAComponentCover), uniqueId), + _features(features), + _currentState(StateUnknown), + _currentPosition(DefaultPosition), + _class(nullptr), + _icon(nullptr), + _retain(false), + _optimistic(false), + _commandCallback(nullptr) +{ + +} + +bool HACover::setState(const CoverState state, const bool force) +{ + if (!force && _currentState == state) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +bool HACover::setPosition(const int16_t position, const bool force) +{ + if (!force && _currentPosition == position) { + return true; + } + + if (publishPosition(position)) { + _currentPosition = position; + return true; + } + + return false; +} + +void HACover::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 12); // 12 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + _serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); + _serializer->topic(AHATOFSTR(HACommandTopic)); + + if (_features & PositionFeature) { + _serializer->topic(AHATOFSTR(HAPositionTopic)); + } +} + +void HACover::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + + if (!_retain) { + publishState(_currentState); + publishPosition(_currentPosition); + } + + subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); +} + +void HACover::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HACommandTopic) + )) { + handleCommand(payload, length); + } +} + +bool HACover::publishState(CoverState state) +{ + if (state == StateUnknown) { + return false; + } + + const __FlashStringHelper *stateStr = nullptr; + switch (state) { + case StateClosed: + stateStr = AHATOFSTR(HAClosedState); + break; + + case StateClosing: + stateStr = AHATOFSTR(HAClosingState); + break; + + case StateOpen: + stateStr = AHATOFSTR(HAOpenState); + break; + + case StateOpening: + stateStr = AHATOFSTR(HAOpeningState); + break; + + case StateStopped: + stateStr = AHATOFSTR(HAStoppedState); + break; + + default: + return false; + } + + return publishOnDataTopic(AHATOFSTR(HAStateTopic), stateStr, true); +} + +bool HACover::publishPosition(int16_t position) +{ + if (position == DefaultPosition || !(_features & PositionFeature)) { + return false; + } + + char str[6 + 1] = {0}; // int16_t digits with null terminator + HANumeric(position, 0).toStr(str); + + return publishOnDataTopic(AHATOFSTR(HAPositionTopic), str, true); +} + +void HACover::handleCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_commandCallback) { + return; + } + + if (memcmp_P(cmd, HACloseCommand, length) == 0) { + _commandCallback(CommandClose, this); + } else if (memcmp_P(cmd, HAOpenCommand, length) == 0) { + _commandCallback(CommandOpen, this); + } else if (memcmp_P(cmd, HAStopCommand, length) == 0) { + _commandCallback(CommandStop, this); + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACover.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACover.h new file mode 100644 index 0000000..92a1ce5 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HACover.h @@ -0,0 +1,210 @@ +#ifndef AHA_HACOVER_H +#define AHA_HACOVER_H + +#include "HABaseDeviceType.h" + +#ifndef EX_ARDUINOHA_COVER + +#define HACOVER_CALLBACK(name) void (*name)(CoverCommand cmd, HACover* sender) + +/** + * HACover allows to control a cover (such as blinds, a roller shutter or a garage door). + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/cover.mqtt/ + */ +class HACover : public HABaseDeviceType +{ +public: + static const int16_t DefaultPosition = -32768; + + enum CoverState { + StateUnknown = 0, + StateClosed, + StateClosing, + StateOpen, + StateOpening, + StateStopped + }; + + enum CoverCommand { + CommandOpen, + CommandClose, + CommandStop + }; + + enum Features { + DefaultFeatures = 0, + PositionFeature = 1 + }; + + /** + * @param uniqueId The unique ID of the cover. It needs to be unique in a scope of your device. + * @param features Features that should be enabled for the fan. + */ + HACover(const char* uniqueId, const Features features = DefaultFeatures); + + /** + * Changes state of the cover and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state New state of the cover. + * @param force Forces to update state without comparing it to previous known state. + * @returns Returns true if MQTT message has been published successfully. + */ + bool setState(const CoverState state, const bool force = false); + + /** + * Changes the position of the cover and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param position The new position of the cover (0-100). + * @param force Forces to update the state without comparing it to a previous known state. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setPosition(const int16_t position, const bool force = false); + + /** + * Sets the current state of the cover without publishing it to Home Assistant. + * This method may be useful if you want to change the state before the connection + * with the MQTT broker is acquired. + * + * @param state The new state of the cover. + */ + inline void setCurrentState(const CoverState state) + { _currentState = state; } + + /** + * Returns last known state of the cover. + * By default the state is set to CoverState::StateUnknown + */ + inline CoverState getCurrentState() const + { return _currentState; } + + /** + * Sets the current position of the cover without pushing the value to Home Assistant. + * This method may be useful if you want to change the position before the connection + * with the MQTT broker is acquired. + * + * @param position The new position of the cover (0-100). + */ + inline void setCurrentPosition(const int16_t position) + { _currentPosition = position; } + + /** + * Returns the last known position of the cover. + * By default position is set to HACover::DefaultPosition + */ + inline int16_t getCurrentPosition() const + { return _currentPosition; } + + /** + * Sets class of the device. + * You can find list of available values here: https://www.home-assistant.io/integrations/cover/ + * + * @param deviceClass The class name. + */ + inline void setDeviceClass(const char* deviceClass) + { _class = deviceClass; } + + /** + * Sets icon of the cover. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the cover's command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Sets optimistic flag for the cover state. + * In this mode the cover state doesn't need to be reported back to the HA panel when a command is received. + * By default the optimistic mode is disabled. + * + * @param optimistic The optimistic mode (`true` - enabled, `false` - disabled). + */ + inline void setOptimistic(const bool optimistic) + { _optimistic = optimistic; } + + /** + * Registers callback that will be called each time the command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same cover. + * + * @param callback + */ + inline void onCommand(HACOVER_CALLBACK(callback)) + { _commandCallback = callback; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /** + * Publishes the MQTT message with the given state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishState(const CoverState state); + + /** + * Publishes the MQTT message with the given position. + * + * @param position The position to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishPosition(const int16_t position); + + /** + * Parses the given command and executes the cover's callback with proper enum's property. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleCommand(const uint8_t* cmd, const uint16_t length); + + /// Features enabled for the cover. + const uint8_t _features; + + /// The current state of the cover. By default it's `HACover::StateUnknown`. + CoverState _currentState; + + /// The current position of the cover. By default it's `HACover::DefaultPosition`. + int16_t _currentPosition; + + /// The device class. It can be nullptr. + const char* _class; + + /// The icon of the button. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The optimistic mode of the cover (`true` - enabled, `false` - disabled). + bool _optimistic; + + /// The command callback that will be called when clicking the cover's button in the HA panel. + HACOVER_CALLBACK(_commandCallback); +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTracker.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTracker.cpp new file mode 100644 index 0000000..81e6a06 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTracker.cpp @@ -0,0 +1,105 @@ +#include "HADeviceTracker.h" +#ifndef EX_ARDUINOHA_DEVICE_TRACKER + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HADeviceTracker::HADeviceTracker(const char* uniqueId) : + HABaseDeviceType(AHATOFSTR(HAComponentDeviceTracker), uniqueId), + _icon(nullptr), + _sourceType(SourceTypeUnknown), + _currentState(StateUnknown) +{ + +} + +bool HADeviceTracker::setState(const TrackerState state, const bool force) +{ + if (!force && state == _currentState) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +void HADeviceTracker::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 8); // 8 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + _serializer->set( + AHATOFSTR(HASourceTypeProperty), + getSourceTypeProperty(), + HASerializer::ProgmemPropertyValue + ); + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); +} + +void HADeviceTracker::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + publishState(_currentState); +} + +bool HADeviceTracker::publishState(const TrackerState state) +{ + const __FlashStringHelper *stateStr = nullptr; + switch (state) { + case StateHome: + stateStr = AHATOFSTR(HAHome); + break; + + case StateNotHome: + stateStr = AHATOFSTR(HANotHome); + break; + + case StateNotAvailable: + stateStr = AHATOFSTR(HAOffline); + break; + + default: + return false; + } + + return publishOnDataTopic(AHATOFSTR(HAStateTopic), stateStr, true); +} + +const __FlashStringHelper* HADeviceTracker::getSourceTypeProperty() const +{ + switch (_sourceType) { + case SourceTypeGPS: + return AHATOFSTR(HAGPSType); + + case SourceTypeRouter: + return AHATOFSTR(HARouterType); + + case SourceTypeBluetooth: + return AHATOFSTR(HABluetoothType); + + case SourceTypeBluetoothLE: + return AHATOFSTR(HABluetoothLEType); + + default: + return nullptr; + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTracker.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTracker.h new file mode 100644 index 0000000..9f5a693 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTracker.h @@ -0,0 +1,114 @@ +#ifndef AHA_HADEVICETRACKER_H +#define AHA_HADEVICETRACKER_H + +#include "HABaseDeviceType.h" + +#ifndef EX_ARDUINOHA_DEVICE_TRACKER + +/** + * HADeviceTracker allows to implement a custom device's tracker. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/device_tracker.mqtt/ + */ +class HADeviceTracker : public HABaseDeviceType +{ +public: + /// Available source types of the tracker. + enum SourceType { + SourceTypeUnknown = 0, + SourceTypeGPS, + SourceTypeRouter, + SourceTypeBluetooth, + SourceTypeBluetoothLE + }; + + /// Available states that can be reported to the HA panel. + enum TrackerState { + StateUnknown = 0, + StateHome, + StateNotHome, + StateNotAvailable + }; + + /** + * @param uniqueId The unique ID of the tracker. It needs to be unique in a scope of your device. + */ + HADeviceTracker(const char* uniqueId); + + /** + * Changes the state of the tracker and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state The new state of the tracker. + * @param force Forces to update the state without comparing it to a previous known state. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setState(const TrackerState state, const bool force = false); + + /** + * Sets the current state of the tracker without publishing it to Home Assistant. + * This method may be useful if you want to change the state before connection + * with MQTT broker is acquired. + * + * @param state The new state of the tracker. + */ + inline void setCurrentState(const TrackerState state) + { _currentState = state; } + + /** + * Returns the last known state of the tracker. + * If setState method wasn't called the initial value will be returned. + */ + inline TrackerState getState() const + { return _currentState; } + + /** + * Sets icon of the tracker. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets the source type of the tracker. + * + * @param type The source type (gps, router, bluetooth, bluetooth LE). + */ + inline void setSourceType(const SourceType type) + { _sourceType = type; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + +private: + /** + * Publishes the MQTT message with the given state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishState(TrackerState state); + + /** + * Returns progmem string representing source type of the tracker. + */ + const __FlashStringHelper* getSourceTypeProperty() const; + + /// The icon of the tracker. It can be nullptr. + const char* _icon; + + /// The source type of the tracker. By default it's `HADeviceTracker::SourceTypeUnknown`. + SourceType _sourceType; + + /// The current state of the device's tracker. By default its `HADeviceTracker::StateUnknown`. + TrackerState _currentState; +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTrigger.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTrigger.cpp new file mode 100644 index 0000000..fc83114 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTrigger.cpp @@ -0,0 +1,209 @@ +#include "HADeviceTrigger.h" +#ifndef EX_ARDUINOHA_DEVICE_TRIGGER + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HADeviceTrigger::HADeviceTrigger(const char* type, const char* subtype) : + HABaseDeviceType(AHATOFSTR(HAComponentDeviceAutomation), nullptr), + _type(type), + _subtype(subtype), + _isProgmemType(false), + _isProgmemSubtype(false) +{ + buildUniqueId(); +} + +HADeviceTrigger::HADeviceTrigger(TriggerType type, const char* subtype) : + HABaseDeviceType(AHATOFSTR(HAComponentDeviceAutomation), nullptr), + _type(determineProgmemType(type)), + _subtype(subtype), + _isProgmemType(true), + _isProgmemSubtype(false) +{ + buildUniqueId(); +} + +HADeviceTrigger::HADeviceTrigger(const char* type, TriggerSubtype subtype) : + HABaseDeviceType(AHATOFSTR(HAComponentDeviceAutomation), nullptr), + _type(type), + _subtype(determineProgmemSubtype(subtype)), + _isProgmemType(false), + _isProgmemSubtype(true) +{ + buildUniqueId(); +} + +HADeviceTrigger::HADeviceTrigger(TriggerType type, TriggerSubtype subtype) : + HABaseDeviceType(AHATOFSTR(HAComponentDeviceAutomation), nullptr), + _type(determineProgmemType(type)), + _subtype(determineProgmemSubtype(subtype)), + _isProgmemType(true), + _isProgmemSubtype(true) +{ + buildUniqueId(); +} + +HADeviceTrigger::~HADeviceTrigger() +{ + if (_uniqueId) { + delete _uniqueId; + } +} + +bool HADeviceTrigger::trigger() +{ + if (!_type || !_subtype) { + return false; + } + + return publishOnDataTopic(AHATOFSTR(HATopic), ""); +} + +void HADeviceTrigger::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 5); // 5 - max properties nb + _serializer->set( + AHATOFSTR(HAAutomationTypeProperty), + AHATOFSTR(HATrigger), + HASerializer::ProgmemPropertyValue + ); + _serializer->set( + AHATOFSTR(HATypeProperty), + _type, + _isProgmemType + ? HASerializer::ProgmemPropertyValue + : HASerializer::ConstCharPropertyValue + ); + _serializer->set( + AHATOFSTR(HASubtypeProperty), + _subtype, + _isProgmemSubtype + ? HASerializer::ProgmemPropertyValue + : HASerializer::ConstCharPropertyValue + ); + _serializer->set(HASerializer::WithDevice); + _serializer->topic(AHATOFSTR(HATopic)); +} + +void HADeviceTrigger::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); +} + +uint16_t HADeviceTrigger::calculateIdSize() const +{ + if (!_type || !_subtype) { + return 0; + } + + const uint16_t typeSize = _isProgmemType ? strlen_P(_type) : strlen(_type); + const uint16_t subtypeSize = _isProgmemSubtype + ? strlen_P(_subtype) + : strlen(_subtype); + + // plus underscore separator and null terminator + return typeSize + subtypeSize + 2; +} + +void HADeviceTrigger::buildUniqueId() +{ + const uint16_t idSize = calculateIdSize(); + if (idSize == 0) { + return; + } + + char* id = new char[idSize]; + + if (_isProgmemType) { + strcpy_P(id, _type); + } else { + strcpy(id, _type); + } + + strcat_P(id, HASerializerUnderscore); + + if (_isProgmemSubtype) { + strcat_P(id, _subtype); + } else { + strcat(id, _subtype); + } + + _uniqueId = id; +} + +const char* HADeviceTrigger::determineProgmemType(TriggerType type) const +{ + switch (type) { + case ButtonShortPressType: + return HAButtonShortPressType; + + case ButtonShortReleaseType: + return HAButtonShortReleaseType; + + case ButtonLongPressType: + return HAButtonLongPressType; + + case ButtonLongReleaseType: + return HAButtonLongReleaseType; + + case ButtonDoublePressType: + return HAButtonDoublePressType; + + case ButtonTriplePressType: + return HAButtonTriplePressType; + + case ButtonQuadruplePressType: + return HAButtonQuadruplePressType; + + case ButtonQuintuplePressType: + return HAButtonQuintuplePressType; + + default: + return nullptr; + } +} + +const char* HADeviceTrigger::determineProgmemSubtype( + TriggerSubtype subtype +) const +{ + switch (subtype) { + case TurnOnSubtype: + return HATurnOnSubtype; + + case TurnOffSubtype: + return HATurnOffSubtype; + + case Button1Subtype: + return HAButton1Subtype; + + case Button2Subtype: + return HAButton2Subtype; + + case Button3Subtype: + return HAButton3Subtype; + + case Button4Subtype: + return HAButton4Subtype; + + case Button5Subtype: + return HAButton5Subtype; + + case Button6Subtype: + return HAButton6Subtype; + + default: + return nullptr; + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTrigger.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTrigger.h new file mode 100644 index 0000000..91b4156 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HADeviceTrigger.h @@ -0,0 +1,176 @@ +#ifndef AHA_HADEVICETRIGGER_H +#define AHA_HADEVICETRIGGER_H + +#include "HABaseDeviceType.h" + +#ifndef EX_ARDUINOHA_DEVICE_TRIGGER + +/** + * HADeviceTrigger allows to a custom trigger that can be used in the Home Assistant automation. + * For example, it can be a wall switch that produces `press` and `long_press` actions. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/device_trigger.mqtt/ + */ +class HADeviceTrigger : public HABaseDeviceType +{ +public: + /// Built-in types of the trigger. + enum TriggerType { + ButtonShortPressType = 1, + ButtonShortReleaseType, + ButtonLongPressType, + ButtonLongReleaseType, + ButtonDoublePressType, + ButtonTriplePressType, + ButtonQuadruplePressType, + ButtonQuintuplePressType + }; + + /// Built-in subtypes of the trigger. + enum TriggerSubtype { + TurnOnSubtype = 1, + TurnOffSubtype, + Button1Subtype, + Button2Subtype, + Button3Subtype, + Button4Subtype, + Button5Subtype, + Button6Subtype + }; + + /** + * Creates the device trigger with a custom type and subtype. + * For example, it can be `click` as the type and `btn0` as the subtype. + * Please note that combination of the type and subtype needs to be unique in a scope of your device. + * + * @param type String representation of the type. + * @param subtype String representation of the subtype. + */ + HADeviceTrigger(const char* type, const char* subtype); + + /** + * Creates the device trigger with a built-in type and a custom subtype. + * For example, it can be `HADeviceTrigger::ButtonShortPressType` as the type and `btn0` as the subtype. + * Please note that combination of the type and subtype needs to be unique in a scope of your device. + * + * @param type Built-in type of the trigger. + * @param subtype String representation of the subtype. + */ + HADeviceTrigger(TriggerType type, const char* subtype); + + /** + * Creates the device trigger with a custom type and a built-in subtype. + * For example, it can be `click` as the type and `HADeviceTrigger::Button1Subtype` as the subtype. + * Please note that combination of the type and subtype needs to be unique in a scope of your device. + * + * @param type String representation of the subtype. + * @param subtype Built-in subtype of the trigger. + */ + HADeviceTrigger(const char* type, TriggerSubtype subtype); + + /** + * Creates the device trigger with a built-in type and built-in subtype. + * For example, it can be `HADeviceTrigger::ButtonShortPressType` as the type and `HADeviceTrigger::Button1Subtype` as the subtype. + * Please note that combination of the type and subtype needs to be unique in a scope of your device. + * + * @param type Built-in type of the trigger. + * @param subtype Built-in subtype of the trigger. + */ + HADeviceTrigger(TriggerType type, TriggerSubtype subtype); + + /** + * Frees memory allocated by the class. + */ + ~HADeviceTrigger(); + + /** + * Publishes MQTT message with the trigger event. + * The published message is not retained. + * + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool trigger(); + + /** + * Returns the type of the trigger. + * If the built-in type is used the returned value points to the flash memory. + * Use `HADeviceTrigger::isProgmemType` to verify if the returned value is the progmem pointer. + * + * @returns Pointer to the type. + */ + inline const char* getType() const + { return _type; } + + /** + * Returns `true` if the built-in type was assigned to the trigger. + */ + inline bool isProgmemType() const + { return _isProgmemType; } + + /** + * Returns the subtype of the trigger. + * If the built-in subtype is used the returned value points to the flash memory. + * Use `HADeviceTrigger::isProgmemSubtype` to verify if the returned value is the progmem pointer. + * + * @returns Pointer to the subtype. + */ + inline const char* getSubtype() const + { return _subtype; } + + /** + * Returns `true` if the built-in subtype was assigned to the trigger. + */ + inline bool isProgmemSubtype() const + { return _isProgmemSubtype; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + +private: + /** + * Calculates desired size of the unique ID based on the type and subtype that were passed to the constructor. + */ + uint16_t calculateIdSize() const; + + /** + * Builds the unique ID of the device's type based on the type and subtype that were passed to the constructor. + */ + void buildUniqueId(); + + /** + * Returns pointer to the flash memory that represents the given type. + * + * @param subtype Built-in type enum's value. + * @returns Pointer to the flash memory if the given type is supported. + * For the unsupported type the nullptr is returned. + */ + const char* determineProgmemType(TriggerType type) const; + + /** + * Returns pointer to the flash memory that represents the given subtype. + * + * @param subtype Built-in subtype enum's value. + * @returns Pointer to the flash memory if the given subtype is supported. + * For the unsupported subtype the nullptr is returned. + */ + const char* determineProgmemSubtype(TriggerSubtype subtype) const; + +private: + /// Pointer to the trigger's type. It can be pointer to the flash memory. + const char* _type; + + /// Pointer to the trigger's subtype. It can be pointer to the flash memory. + const char* _subtype; + + /// Specifies whether the type points to the flash memory. + bool _isProgmemType; + + /// Specifies whether the subtype points to the flash memory. + bool _isProgmemSubtype; +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAFan.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAFan.cpp new file mode 100644 index 0000000..ee404a2 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAFan.cpp @@ -0,0 +1,194 @@ +#include "HAFan.h" +#ifndef EX_ARDUINOHA_FAN + +#include "../HAMqtt.h" +#include "../utils/HAUtils.h" +#include "../utils/HASerializer.h" + +HAFan::HAFan(const char* uniqueId, const uint8_t features) : + HABaseDeviceType(AHATOFSTR(HAComponentFan), uniqueId), + _features(features), + _icon(nullptr), + _retain(false), + _optimistic(false), + _speedRangeMax(), + _speedRangeMin(), + _currentState(false), + _currentSpeed(0), + _stateCallback(nullptr), + _speedCallback(nullptr) +{ + +} + +bool HAFan::setState(const bool state, const bool force) +{ + if (!force && state == _currentState) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +bool HAFan::setSpeed(const uint16_t speed, const bool force) +{ + if (!force && speed == _currentSpeed) { + return true; + } + + if (publishSpeed(speed)) { + _currentSpeed = speed; + return true; + } + + return false; +} + +void HAFan::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 14); // 14 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + _serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + if (_features & SpeedsFeature) { + _serializer->topic(AHATOFSTR(HAPercentageStateTopic)); + _serializer->topic(AHATOFSTR(HAPercentageCommandTopic)); + + if (_speedRangeMax.isSet()) { + _serializer->set( + AHATOFSTR(HASpeedRangeMaxProperty), + &_speedRangeMax, + HASerializer::NumberPropertyType + ); + } + + if (_speedRangeMin.isSet()) { + _serializer->set( + AHATOFSTR(HASpeedRangeMinProperty), + &_speedRangeMin, + HASerializer::NumberPropertyType + ); + } + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); + _serializer->topic(AHATOFSTR(HACommandTopic)); +} + +void HAFan::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + + if (!_retain) { + publishState(_currentState); + publishSpeed(_currentSpeed); + } + + subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); + + if (_features & SpeedsFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HAPercentageCommandTopic)); + } +} + +void HAFan::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HACommandTopic) + )) { + handleStateCommand(payload, length); + } else if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HAPercentageCommandTopic) + )) { + handleSpeedCommand(payload, length); + } +} + +bool HAFan::publishState(const bool state) +{ + return publishOnDataTopic( + AHATOFSTR(HAStateTopic), + AHATOFSTR(state ? HAStateOn : HAStateOff), + true + ); +} + +bool HAFan::publishSpeed(const uint16_t speed) +{ + if (!(_features & SpeedsFeature)) { + return false; + } + + char str[5 + 1] = {0}; // uint16_t digits with null terminator + HANumeric(speed, 0).toStr(str); + + return publishOnDataTopic(AHATOFSTR(HAPercentageStateTopic), str, true); +} + +void HAFan::handleStateCommand(const uint8_t* cmd, const uint16_t length) +{ + (void)cmd; + + if (!_stateCallback) { + return; + } + + bool state = length == strlen_P(HAStateOn); + _stateCallback(state, this); +} + +void HAFan::handleSpeedCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_speedCallback) { + return; + } + + const HANumeric& number = HANumeric::fromStr(cmd, length); + if (number.isUInt16()) { + _speedCallback(number.toUInt16(), this); + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAFan.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAFan.h new file mode 100644 index 0000000..8ed750e --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAFan.h @@ -0,0 +1,245 @@ +#ifndef AHA_HAFAN_H +#define AHA_HAFAN_H + +#include "HABaseDeviceType.h" +#include "../utils/HANumeric.h" + +#ifndef EX_ARDUINOHA_FAN + +#define HAFAN_STATE_CALLBACK(name) void (*name)(bool state, HAFan* sender) +#define HAFAN_SPEED_CALLBACK(name) void (*name)(uint16_t speed, HAFan* sender) + +/** + * HAFan allows adding a controllable fan in the Home Assistant panel. + * The library supports only the state and speed of the fan. + * If you want more features please open a new GitHub issue. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/fan.mqtt/ + */ +class HAFan : public HABaseDeviceType +{ +public: + enum Features { + DefaultFeatures = 0, + SpeedsFeature = 1 + }; + + /** + * @param uniqueId The unique ID of the fan. It needs to be unique in a scope of your device. + * @param features Features that should be enabled for the fan. + */ + HAFan(const char* uniqueId, const uint8_t features = DefaultFeatures); + + /** + * Changes state of the fan and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state New state of the fan. + * @param force Forces to update state without comparing it to previous known state. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setState(const bool state, const bool force = false); + + /** + * Changes the speed of the fan and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param speed The new speed of the fan. It should be in range of min and max value. + * @param force Forces to update the value without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setSpeed(const uint16_t speed, const bool force = false); + + /** + * Alias for `setState(true)`. + */ + inline bool turnOn() + { return setState(true); } + + /** + * Alias for `setState(false)`. + */ + inline bool turnOff() + { return setState(false); } + + /** + * Sets current state of the fan without publishing it to Home Assistant. + * This method may be useful if you want to change state before connection + * with MQTT broker is acquired. + * + * @param state New state of the fan. + */ + inline void setCurrentState(const bool state) + { _currentState = state; } + + /** + * Returns last known state of the fan. + * By default it's `false`. + */ + inline bool getCurrentState() const + { return _currentState; } + + /** + * Sets the current speed of the fan without pushing the value to Home Assistant. + * This method may be useful if you want to change the speed before the connection + * with the MQTT broker is acquired. + * + * @param speed The new speed of the fan. It should be in range of min and max value. + */ + inline void setCurrentSpeed(const uint16_t speed) + { _currentSpeed = speed; } + + /** + * Returns the last known speed of the fan. + * By default speed is set to `0`. + */ + inline uint16_t getCurrentSpeed() const + { return _currentSpeed; } + + /** + * Sets icon of the fan. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the fan's command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Sets optimistic flag for the fan state. + * In this mode the fan state doesn't need to be reported back to the HA panel when a command is received. + * By default the optimistic mode is disabled. + * + * @param optimistic The optimistic mode (`true` - enabled, `false` - disabled). + */ + inline void setOptimistic(const bool optimistic) + { _optimistic = optimistic; } + + /** + * Sets the maximum of numeric output range (representing 100%). + * The number of speeds within the speed_range / 100 will determine the percentage step. + * By default the maximum range is `100`. + * + * @param max The maximum of numeric output range. + */ + inline void setSpeedRangeMax(const uint16_t max) + { _speedRangeMax.setBaseValue(max); } + + /** + * Sets the minimum of numeric output range (off is not included, so speed_range_min - 1 represents 0 %). + * The number of speeds within the speed_range / 100 will determine the percentage step. + * By default the minimum range is `1`. + * + * @param min The minimum of numeric output range. + */ + inline void setSpeedRangeMin(const uint16_t min) + { _speedRangeMin.setBaseValue(min); } + + /** + * Registers callback that will be called each time the state command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same fan. + * + * @param callback + * @note In non-optimistic mode, the state must be reported back to HA using the HAFan::setState method. + */ + inline void onStateCommand(HAFAN_STATE_CALLBACK(callback)) + { _stateCallback = callback; } + + /** + * Registers callback that will be called each time the speed command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same fan. + * + * @param callback + * @note In non-optimistic mode, the speed must be reported back to HA using the HAFan::setSpeed method. + */ + inline void onSpeedCommand(HAFAN_SPEED_CALLBACK(callback)) + { _speedCallback = callback; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /** + * Publishes the MQTT message with the given state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishState(const bool state); + + /** + * Publishes the MQTT message with the given speed. + * + * @param speed The speed to publish. It should be in range of min and max value. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishSpeed(const uint16_t speed); + + /** + * Parses the given state command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleStateCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Parses the given speed command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleSpeedCommand(const uint8_t* cmd, const uint16_t length); + + /// Features enabled for the fan. + const uint8_t _features; + + /// The icon of the button. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The optimistic mode of the fan (`true` - enabled, `false` - disabled). + bool _optimistic; + + /// The maximum of numeric output range. + HANumeric _speedRangeMax; + + /// The minimum of numeric output range. + HANumeric _speedRangeMin; + + /// The current state of the fan. By default it's `false`. + bool _currentState; + + /// The current speed of the fan. By default it's `0`. + uint16_t _currentSpeed; + + /// The callback that will be called when the state command is received from the HA. + HAFAN_STATE_CALLBACK(_stateCallback); + + /// The callback that will be called when the speed command is received from the HA. + HAFAN_SPEED_CALLBACK(_speedCallback); +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAHVAC.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAHVAC.cpp new file mode 100644 index 0000000..76eb422 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAHVAC.cpp @@ -0,0 +1,756 @@ +#include "HAHVAC.h" +#ifndef EX_ARDUINOHA_HVAC + +#include "../HAMqtt.h" +#include "../utils/HAUtils.h" +#include "../utils/HASerializer.h" + +const uint8_t HAHVAC::DefaultFanModes = AutoFanMode | LowFanMode | MediumFanMode | HighFanMode; +const uint8_t HAHVAC::DefaultSwingModes = OnSwingMode | OffSwingMode; +const uint8_t HAHVAC::DefaultModes = AutoMode | OffMode | CoolMode | HeatMode | DryMode | FanOnlyMode; + +HAHVAC::HAHVAC( + const char* uniqueId, + const uint16_t features, + const NumberPrecision precision +) : + HABaseDeviceType(AHATOFSTR(HAComponentClimate), uniqueId), + _features(features), + _precision(precision), + _icon(nullptr), + _retain(false), + _currentTemperature(), + _action(UnknownAction), + _temperatureUnit(DefaultUnit), + _minTemp(), + _maxTemp(), + _tempStep(), + _auxCallback(nullptr), + _auxState(false), + _powerCallback(nullptr), + _fanMode(UnknownFanMode), + _fanModes(DefaultFanModes), + _fanModesSerializer(nullptr), + _fanModeCallback(nullptr), + _swingMode(UnknownSwingMode), + _swingModes(DefaultSwingModes), + _swingModesSerializer(nullptr), + _swingModeCallback(nullptr), + _mode(UnknownMode), + _modes(DefaultModes), + _modesSerializer(nullptr), + _modeCallback(nullptr), + _targetTemperature(), + _targetTemperatureCallback(nullptr) +{ + if (_features & FanFeature) { + _fanModesSerializer = new HASerializerArray(4); + } + + if (_features & SwingFeature) { + _swingModesSerializer = new HASerializerArray(2); + } + + if (_features & ModesFeature) { + _modesSerializer = new HASerializerArray(6); + } +} + +HAHVAC::~HAHVAC() +{ + if (_fanModesSerializer) { + delete _fanModesSerializer; + } + + if (_swingModesSerializer) { + delete _swingModesSerializer; + } + + if (_modesSerializer) { + delete _modesSerializer; + } +} + +bool HAHVAC::setCurrentTemperature(const HANumeric& temperature, const bool force) +{ + if (temperature.getPrecision() != _precision) { + return false; + } + + if (!force && temperature == _currentTemperature) { + return true; + } + + if (publishCurrentTemperature(temperature)) { + _currentTemperature = temperature; + return true; + } + + return false; +} + +bool HAHVAC::setAction(const Action action, const bool force) +{ + if (!force && action == _action) { + return true; + } + + if (publishAction(action)) { + _action = action; + return true; + } + + return false; +} + +bool HAHVAC::setAuxState(const bool state, const bool force) +{ + if (!force && state == _auxState) { + return true; + } + + if (publishAuxState(state)) { + _auxState = state; + return true; + } + + return false; +} + +bool HAHVAC::setFanMode(const FanMode mode, const bool force) +{ + if (!force && mode == _fanMode) { + return true; + } + + if (publishFanMode(mode)) { + _fanMode = mode; + return true; + } + + return false; +} + +bool HAHVAC::setSwingMode(const SwingMode mode, const bool force) +{ + if (!force && mode == _swingMode) { + return true; + } + + if (publishSwingMode(mode)) { + _swingMode = mode; + return true; + } + + return false; +} + +bool HAHVAC::setMode(const Mode mode, const bool force) +{ + if (!force && mode == _mode) { + return true; + } + + if (publishMode(mode)) { + _mode = mode; + return true; + } + + return false; +} + +bool HAHVAC::setTargetTemperature(const HANumeric& temperature, const bool force) +{ + if (temperature.getPrecision() != _precision) { + return false; + } + + if (!force && temperature == _targetTemperature) { + return true; + } + + if (publishTargetTemperature(temperature)) { + _targetTemperature = temperature; + return true; + } + + return false; +} + +void HAHVAC::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 28); // 28 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_features & ActionFeature) { + _serializer->topic(AHATOFSTR(HAActionTopic)); + } + + if (_features & AuxHeatingFeature) { + _serializer->topic(AHATOFSTR(HAAuxCommandTopic)); + _serializer->topic(AHATOFSTR(HAAuxStateTopic)); + } + + if (_features & PowerFeature) { + _serializer->topic(AHATOFSTR(HAPowerCommandTopic)); + } + + if (_features & FanFeature) { + _serializer->topic(AHATOFSTR(HAFanModeCommandTopic)); + _serializer->topic(AHATOFSTR(HAFanModeStateTopic)); + + if (_fanModes != DefaultFanModes) { + _fanModesSerializer->clear(); + + if (_fanModes & AutoFanMode) { + _fanModesSerializer->add(HAFanModeAuto); + } + + if (_fanModes & LowFanMode) { + _fanModesSerializer->add(HAFanModeLow); + } + + if (_fanModes & MediumFanMode) { + _fanModesSerializer->add(HAFanModeMedium); + } + + if (_fanModes & HighFanMode) { + _fanModesSerializer->add(HAFanModeHigh); + } + + _serializer->set( + AHATOFSTR(HAFanModesProperty), + _fanModesSerializer, + HASerializer::ArrayPropertyType + ); + } + } + + if (_features & SwingFeature) { + _serializer->topic(AHATOFSTR(HASwingModeCommandTopic)); + _serializer->topic(AHATOFSTR(HASwingModeStateTopic)); + + if (_swingModes != DefaultSwingModes) { + _swingModesSerializer->clear(); + + if (_swingModes & OnSwingMode) { + _swingModesSerializer->add(HASwingModeOn); + } + + if (_swingModes & OffSwingMode) { + _swingModesSerializer->add(HASwingModeOff); + } + + _serializer->set( + AHATOFSTR(HASwingModesProperty), + _swingModesSerializer, + HASerializer::ArrayPropertyType + ); + } + } + + if (_features & ModesFeature) { + _serializer->topic(AHATOFSTR(HAModeCommandTopic)); + _serializer->topic(AHATOFSTR(HAModeStateTopic)); + + if (_modes != DefaultModes) { + _modesSerializer->clear(); + + if (_modes & AutoMode) { + _modesSerializer->add(HAModeAuto); + } + + if (_modes & OffMode) { + _modesSerializer->add(HAModeOff); + } + + if (_modes & CoolMode) { + _modesSerializer->add(HAModeCool); + } + + if (_modes & HeatMode) { + _modesSerializer->add(HAModeHeat); + } + + if (_modes & DryMode) { + _modesSerializer->add(HAModeDry); + } + + if (_modes & FanOnlyMode) { + _modesSerializer->add(HAModeFanOnly); + } + + _serializer->set( + AHATOFSTR(HAModesProperty), + _modesSerializer, + HASerializer::ArrayPropertyType + ); + } + } + + if (_features & TargetTemperatureFeature) { + _serializer->topic(AHATOFSTR(HATemperatureCommandTopic)); + _serializer->topic(AHATOFSTR(HATemperatureStateTopic)); + _serializer->set( + AHATOFSTR(HATemperatureCommandTemplateProperty), + getCommandWithFloatTemplate(), + HASerializer::ProgmemPropertyValue + ); + } + + if (_temperatureUnit != DefaultUnit) { + const __FlashStringHelper *unitStr = _temperatureUnit == CelsiusUnit + ? AHATOFSTR(HATemperatureUnitC) + : AHATOFSTR(HATemperatureUnitF); + + _serializer->set( + AHATOFSTR(HATemperatureUnitProperty), + unitStr, + HASerializer::ProgmemPropertyValue + ); + } + + if (_minTemp.isSet()) { + _serializer->set( + AHATOFSTR(HAMinTempProperty), + &_minTemp, + HASerializer::NumberPropertyType + ); + } + + if (_maxTemp.isSet()) { + _serializer->set( + AHATOFSTR(HAMaxTempProperty), + &_maxTemp, + HASerializer::NumberPropertyType + ); + } + + if (_tempStep.isSet()) { + _serializer->set( + AHATOFSTR(HATempStepProperty), + &_tempStep, + HASerializer::NumberPropertyType + ); + } + + _serializer->topic(AHATOFSTR(HACurrentTemperatureTopic)); + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); +} + +void HAHVAC::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + + if (!_retain) { + publishCurrentTemperature(_currentTemperature); + publishAction(_action); + publishAuxState(_auxState); + publishFanMode(_fanMode); + publishSwingMode(_swingMode); + publishMode(_mode); + publishTargetTemperature(_targetTemperature); + } + + if (_features & AuxHeatingFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HAAuxCommandTopic)); + } + + if (_features & PowerFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HAPowerCommandTopic)); + } + + if (_features & FanFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HAFanModeCommandTopic)); + } + + if (_features & SwingFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HASwingModeCommandTopic)); + } + + if (_features & ModesFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HAModeCommandTopic)); + } + + if (_features & TargetTemperatureFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HATemperatureCommandTopic)); + } +} + +void HAHVAC::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HAAuxCommandTopic) + )) { + handleAuxStateCommand(payload, length); + } else if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HAPowerCommandTopic) + )) { + handlePowerCommand(payload, length); + } else if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HAFanModeCommandTopic) + )) { + handleFanModeCommand(payload, length); + } else if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HASwingModeCommandTopic) + )) { + handleSwingModeCommand(payload, length); + } else if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HAModeCommandTopic) + )) { + handleModeCommand(payload, length); + } else if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HATemperatureCommandTopic) + )) { + handleTargetTemperatureCommand(payload, length); + } +} + +bool HAHVAC::publishCurrentTemperature(const HANumeric& temperature) +{ + if (!temperature.isSet()) { + return false; + } + + uint8_t size = temperature.calculateSize(); + if (size == 0) { + return false; + } + + char str[size + 1]; // with null terminator + str[size] = 0; + temperature.toStr(str); + + return publishOnDataTopic( + AHATOFSTR(HACurrentTemperatureTopic), + str, + true + ); +} + +bool HAHVAC::publishAction(const Action action) +{ + if (action == UnknownAction || !(_features & ActionFeature)) { + return false; + } + + const __FlashStringHelper *stateStr = nullptr; + switch (action) { + case OffAction: + stateStr = AHATOFSTR(HAActionOff); + break; + + case HeatingAction: + stateStr = AHATOFSTR(HAActionHeating); + break; + + case CoolingAction: + stateStr = AHATOFSTR(HAActionCooling); + break; + + case DryingAction: + stateStr = AHATOFSTR(HAActionDrying); + break; + + case IdleAction: + stateStr = AHATOFSTR(HAActionIdle); + break; + + case FanAction: + stateStr = AHATOFSTR(HAActionFan); + break; + + default: + return false; + } + + return publishOnDataTopic( + AHATOFSTR(HAActionTopic), + stateStr, + true + ); +} + +bool HAHVAC::publishAuxState(const bool state) +{ + if (!(_features & AuxHeatingFeature)) { + return false; + } + + return publishOnDataTopic( + AHATOFSTR(HAAuxStateTopic), + AHATOFSTR(state ? HAStateOn : HAStateOff), + true + ); +} + +bool HAHVAC::publishFanMode(const FanMode mode) +{ + if (mode == UnknownFanMode || !(_features & FanFeature)) { + return false; + } + + const __FlashStringHelper *stateStr = nullptr; + switch (mode) { + case AutoFanMode: + stateStr = AHATOFSTR(HAFanModeAuto); + break; + + case LowFanMode: + stateStr = AHATOFSTR(HAFanModeLow); + break; + + case MediumFanMode: + stateStr = AHATOFSTR(HAFanModeMedium); + break; + + case HighFanMode: + stateStr = AHATOFSTR(HAFanModeHigh); + break; + + default: + return false; + } + + return publishOnDataTopic( + AHATOFSTR(HAFanModeStateTopic), + stateStr, + true + ); +} + +bool HAHVAC::publishSwingMode(const SwingMode mode) +{ + if (mode == UnknownSwingMode || !(_features & SwingFeature)) { + return false; + } + + const __FlashStringHelper *stateStr = nullptr; + switch (mode) { + case OnSwingMode: + stateStr = AHATOFSTR(HASwingModeOn); + break; + + case OffSwingMode: + stateStr = AHATOFSTR(HASwingModeOff); + break; + + default: + return false; + } + + return publishOnDataTopic( + AHATOFSTR(HASwingModeStateTopic), + stateStr, + true + ); +} + +bool HAHVAC::publishMode(const Mode mode) +{ + if (mode == UnknownMode || !(_features & ModesFeature)) { + return false; + } + + const __FlashStringHelper *stateStr = nullptr; + switch (mode) { + case AutoMode: + stateStr = AHATOFSTR(HAModeAuto); + break; + + case OffMode: + stateStr = AHATOFSTR(HAModeOff); + break; + + case CoolMode: + stateStr = AHATOFSTR(HAModeCool); + break; + + case HeatMode: + stateStr = AHATOFSTR(HAModeHeat); + break; + + case DryMode: + stateStr = AHATOFSTR(HAModeDry); + break; + + case FanOnlyMode: + stateStr = AHATOFSTR(HAModeFanOnly); + break; + + default: + return false; + } + + return publishOnDataTopic( + AHATOFSTR(HAModeStateTopic), + stateStr, + true + ); +} + +bool HAHVAC::publishTargetTemperature(const HANumeric& temperature) +{ + if (!temperature.isSet()) { + return false; + } + + uint8_t size = temperature.calculateSize(); + if (size == 0) { + return false; + } + + char str[size + 1]; // with null terminator + str[size] = 0; + temperature.toStr(str); + + return publishOnDataTopic( + AHATOFSTR(HATemperatureStateTopic), + str, + true + ); +} + +void HAHVAC::handleAuxStateCommand(const uint8_t* cmd, const uint16_t length) +{ + (void)cmd; + + if (!_auxCallback) { + return; + } + + bool state = length == strlen_P(HAStateOn); + _auxCallback(state, this); +} + +void HAHVAC::handlePowerCommand(const uint8_t* cmd, const uint16_t length) +{ + (void)cmd; + + if (!_powerCallback) { + return; + } + + bool state = length == strlen_P(HAStateOn); + _powerCallback(state, this); +} + +void HAHVAC::handleFanModeCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_fanModeCallback) { + return; + } + + if (memcmp_P(cmd, HAFanModeAuto, length) == 0) { + _fanModeCallback(AutoFanMode, this); + } else if (memcmp_P(cmd, HAFanModeLow, length) == 0) { + _fanModeCallback(LowFanMode, this); + } else if (memcmp_P(cmd, HAFanModeMedium, length) == 0) { + _fanModeCallback(MediumFanMode, this); + } else if (memcmp_P(cmd, HAFanModeHigh, length) == 0) { + _fanModeCallback(HighFanMode, this); + } +} + +void HAHVAC::handleSwingModeCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_swingModeCallback) { + return; + } + + if (memcmp_P(cmd, HASwingModeOn, length) == 0) { + _swingModeCallback(OnSwingMode, this); + } else if (memcmp_P(cmd, HASwingModeOff, length) == 0) { + _swingModeCallback(OffSwingMode, this); + } +} + +void HAHVAC::handleModeCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_modeCallback) { + return; + } + + if (memcmp_P(cmd, HAModeAuto, length) == 0) { + _modeCallback(AutoMode, this); + } else if (memcmp_P(cmd, HAModeOff, length) == 0) { + _modeCallback(OffMode, this); + } else if (memcmp_P(cmd, HAModeCool, length) == 0) { + _modeCallback(CoolMode, this); + } else if (memcmp_P(cmd, HAModeHeat, length) == 0) { + _modeCallback(HeatMode, this); + } else if (memcmp_P(cmd, HAModeDry, length) == 0) { + _modeCallback(DryMode, this); + } else if (memcmp_P(cmd, HAModeFanOnly, length) == 0) { + _modeCallback(FanOnlyMode, this); + } +} + +void HAHVAC::handleTargetTemperatureCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_targetTemperatureCallback) { + return; + } + + HANumeric number = HANumeric::fromStr(cmd, length); + if (number.isSet()) { + number.setPrecision(_precision); + _targetTemperatureCallback(number, this); + } +} + +const __FlashStringHelper* HAHVAC::getCommandWithFloatTemplate() +{ + switch (_precision) { + case PrecisionP1: + return AHATOFSTR(HAValueTemplateFloatP1); + + case PrecisionP2: + return AHATOFSTR(HAValueTemplateFloatP2); + + case PrecisionP3: + return AHATOFSTR(HAValueTemplateFloatP3); + + default: + return nullptr; + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAHVAC.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAHVAC.h new file mode 100644 index 0000000..1957ac6 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAHVAC.h @@ -0,0 +1,704 @@ +#ifndef AHA_HAHVAC_H +#define AHA_HAHVAC_H + +#include "HABaseDeviceType.h" +#include "../utils/HANumeric.h" + +#ifndef EX_ARDUINOHA_HVAC + +#define _SET_CURRENT_TEMPERATURE_OVERLOAD(type) \ + /** @overload */ \ + inline bool setCurrentTemperature(const type temperature, const bool force = false) \ + { return setCurrentTemperature(HANumeric(temperature, _precision), force); } + +#define _SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(type) \ + /** @overload */ \ + inline void setCurrentCurrentTemperature(const type temperature) \ + { setCurrentCurrentTemperature(HANumeric(temperature, _precision)); } + +#define _SET_TARGET_TEMPERATURE_OVERLOAD(type) \ + /** @overload */ \ + inline bool setTargetTemperature(const type temperature, const bool force = false) \ + { return setTargetTemperature(HANumeric(temperature, _precision), force); } + +#define _SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(type) \ + /** @overload */ \ + inline void setCurrentTargetTemperature(const type temperature) \ + { setCurrentTargetTemperature(HANumeric(temperature, _precision)); } + +#define HAHVAC_CALLBACK_BOOL(name) void (*name)(bool state, HAHVAC* sender) +#define HAHVAC_CALLBACK_TARGET_TEMP(name) void (*name)(HANumeric temperature, HAHVAC* sender) +#define HAHVAC_CALLBACK_FAN_MODE(name) void (*name)(FanMode mode, HAHVAC* sender) +#define HAHVAC_CALLBACK_SWING_MODE(name) void (*name)(SwingMode mode, HAHVAC* sender) +#define HAHVAC_CALLBACK_MODE(name) void (*name)(Mode mode, HAHVAC* sender) + +class HASerializerArray; + +/** + * HAHVAC lets you control your HVAC devices. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/climate.mqtt/ + */ +class HAHVAC : public HABaseDeviceType +{ +public: + static const uint8_t DefaultFanModes; + static const uint8_t DefaultSwingModes; + static const uint8_t DefaultModes; + + /// The list of features available in the HVAC. They're used in the constructor. + enum Features { + DefaultFeatures = 0, + ActionFeature = 1, + AuxHeatingFeature = 2, + PowerFeature = 4, + FanFeature = 8, + SwingFeature = 16, + ModesFeature = 32, + TargetTemperatureFeature = 64 + }; + + /// The list of available actions of the HVAC. + enum Action { + UnknownAction = 0, + OffAction, + HeatingAction, + CoolingAction, + DryingAction, + IdleAction, + FanAction + }; + + /// The list of available fan modes. + enum FanMode { + UnknownFanMode = 0, + AutoFanMode = 1, + LowFanMode = 2, + MediumFanMode = 4, + HighFanMode = 8 + }; + + /// The list of available swing modes. + enum SwingMode { + UnknownSwingMode = 0, + OnSwingMode = 1, + OffSwingMode = 2 + }; + + /// The list of available HVAC's modes. + enum Mode { + UnknownMode = 0, + AutoMode = 1, + OffMode = 2, + CoolMode = 4, + HeatMode = 8, + DryMode = 16, + FanOnlyMode = 32 + }; + + /// Temperature units available in the HVAC. + enum TemperatureUnit { + DefaultUnit = 1, + CelsiusUnit, + FahrenheitUnit + }; + + /** + * @param uniqueId The unique ID of the HVAC. It needs to be unique in a scope of your device. + * @param features Features that should be enabled for the HVAC. + * @param precision The precision of temperatures reported by the HVAC. + */ + HAHVAC( + const char* uniqueId, + const uint16_t features = DefaultFeatures, + const NumberPrecision precision = PrecisionP1 + ); + + /** + * Frees memory allocated for the arrays serialization. + */ + ~HAHVAC(); + + /** + * Changes current temperature of the HVAC and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param temperature New current temperature. + * @param force Forces to update the temperature without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setCurrentTemperature(const HANumeric& temperature, const bool force = false); + + _SET_CURRENT_TEMPERATURE_OVERLOAD(int8_t) + _SET_CURRENT_TEMPERATURE_OVERLOAD(int16_t) + _SET_CURRENT_TEMPERATURE_OVERLOAD(int32_t) + _SET_CURRENT_TEMPERATURE_OVERLOAD(uint8_t) + _SET_CURRENT_TEMPERATURE_OVERLOAD(uint16_t) + _SET_CURRENT_TEMPERATURE_OVERLOAD(uint32_t) + _SET_CURRENT_TEMPERATURE_OVERLOAD(float) + +#ifdef ARDUINOHA_INT_OVERLOAD + _SET_CURRENT_TEMPERATURE_OVERLOAD(int) +#endif + + /** + * Changes action of the HVAC and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param action New action. + * @param force Forces to update the action without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setAction(const Action action, const bool force = false); + + /** + * Changes state of the aux heating and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state The new state. + * @param force Forces to update the state without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setAuxState(const bool state, const bool force = false); + + /** + * Changes mode of the fan of the HVAC and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param mode New fan's mode. + * @param force Forces to update the mode without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setFanMode(const FanMode mode, const bool force = false); + + /** + * Changes swing mode of the HVAC and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param mode New swing mode. + * @param force Forces to update the mode without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setSwingMode(const SwingMode mode, const bool force = false); + + /** + * Changes mode of the HVAC and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param mode New HVAC's mode. + * @param force Forces to update the mode without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setMode(const Mode mode, const bool force = false); + + /** + * Changes target temperature of the HVAC and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param temperature Target temperature to set. + * @param force Forces to update the mode without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setTargetTemperature(const HANumeric& temperature, const bool force = false); + + _SET_TARGET_TEMPERATURE_OVERLOAD(int8_t) + _SET_TARGET_TEMPERATURE_OVERLOAD(int16_t) + _SET_TARGET_TEMPERATURE_OVERLOAD(int32_t) + _SET_TARGET_TEMPERATURE_OVERLOAD(uint8_t) + _SET_TARGET_TEMPERATURE_OVERLOAD(uint16_t) + _SET_TARGET_TEMPERATURE_OVERLOAD(uint32_t) + _SET_TARGET_TEMPERATURE_OVERLOAD(float) + +#ifdef ARDUINOHA_INT_OVERLOAD + _SET_TARGET_TEMPERATURE_OVERLOAD(int) +#endif + + /** + * Sets current temperature of the HVAC without publishing it to Home Assistant. + * This method may be useful if you want to change temperature before connection + * with MQTT broker is acquired. + * + * @param temperature New current temperature. + */ + inline void setCurrentCurrentTemperature(const HANumeric& temperature) + { if (temperature.getPrecision() == _precision) { _currentTemperature = temperature; } } + + _SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(int8_t) + _SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(int16_t) + _SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(int32_t) + _SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(uint8_t) + _SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(uint16_t) + _SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(uint32_t) + _SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(float) + +#ifdef ARDUINOHA_INT_OVERLOAD + _SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(int) +#endif + + /** + * Returns last known current temperature of the HVAC. + * If setCurrentTemperature method wasn't called the initial value will be returned. + */ + inline const HANumeric& getCurrentTemperature() const + { return _currentTemperature; } + + /** + * Sets action of the HVAC without publishing it to Home Assistant. + * This method may be useful if you want to change the action before connection + * with MQTT broker is acquired. + * + * @param action New action. + */ + inline void setCurrentAction(const Action action) + { _action = action; } + + /** + * Returns last known action of the HVAC. + * If setAction method wasn't called the initial value will be returned. + */ + inline Action getCurrentAction() const + { return _action; } + + /** + * Sets aux heating state without publishing it to Home Assistant. + * This method may be useful if you want to change the state before connection + * with MQTT broker is acquired. + * + * @param state The new state. + */ + inline void setCurrentAuxState(const bool state) + { _auxState = state; } + + /** + * Returns last known state of the aux heating. + * If setAuxState method wasn't called the initial value will be returned. + */ + inline bool getCurrentAuxState() const + { return _auxState; } + + /** + * Sets fan's mode of the HVAC without publishing it to Home Assistant. + * This method may be useful if you want to change the mode before connection + * with MQTT broker is acquired. + * + * @param mode New fan's mode. + */ + inline void setCurrentFanMode(const FanMode mode) + { _fanMode = mode; } + + /** + * Returns last known fan's mode of the HVAC. + * If setFanMode method wasn't called the initial value will be returned. + */ + inline FanMode getCurrentFanMode() const + { return _fanMode; } + + /** + * Sets available fan modes. + * + * @param modes The modes to set (for example: `HAHVAC::AutoFanMode | HAHVAC::HighFanMode`). + */ + inline void setFanModes(const uint8_t modes) + { _fanModes = modes; } + + /** + * Sets swing mode of the HVAC without publishing it to Home Assistant. + * This method may be useful if you want to change the mode before connection + * with MQTT broker is acquired. + * + * @param mode New swing mode. + */ + inline void setCurrentSwingMode(const SwingMode mode) + { _swingMode = mode; } + + /** + * Returns last known swing mode of the HVAC. + * If setSwingMode method wasn't called the initial value will be returned. + */ + inline SwingMode getCurrentSwingMode() const + { return _swingMode; } + + /** + * Sets available swing modes. + * + * @param modes The modes to set (for example: `HAHVAC::OnSwingMode`). + */ + inline void setSwingModes(const uint8_t modes) + { _swingModes = modes; } + + /** + * Sets mode of the HVAC without publishing it to Home Assistant. + * This method may be useful if you want to change the mode before connection + * with MQTT broker is acquired. + * + * @param mode New HVAC's mode. + */ + inline void setCurrentMode(const Mode mode) + { _mode = mode; } + + /** + * Returns last known mode of the HVAC. + * If setMode method wasn't called the initial value will be returned. + */ + inline Mode getCurrentMode() const + { return _mode; } + + /** + * Sets available HVAC's modes. + * + * @param modes The modes to set (for example: `HAHVAC::CoolMode | HAHVAC::HeatMode`). + */ + inline void setModes(const uint8_t modes) + { _modes = modes; } + + /** + * Sets target temperature of the HVAC without publishing it to Home Assistant. + * This method may be useful if you want to change the target before connection + * with MQTT broker is acquired. + * + * @param temperature Target temperature to set. + */ + inline void setCurrentTargetTemperature(const HANumeric& temperature) + { if (temperature.getPrecision() == _precision) { _targetTemperature = temperature; } } + + _SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(int8_t) + _SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(int16_t) + _SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(int32_t) + _SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(uint8_t) + _SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(uint16_t) + _SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(uint32_t) + _SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(float) + +#ifdef ARDUINOHA_INT_OVERLOAD + _SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(int) +#endif + + /** + * Returns last known target temperature of the HVAC. + * If setTargetTemperature method wasn't called the initial value will be returned. + */ + inline const HANumeric& getCurrentTargetTemperature() const + { return _targetTemperature; } + + /** + * Sets icon of the HVAC. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the HVAC's command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Changes the temperature unit. + * + * @param unit See the TemperatureUnit enum above. + */ + inline void setTemperatureUnit(TemperatureUnit unit) + { _temperatureUnit = unit; } + + /** + * Sets the minimum temperature that can be set from the Home Assistant panel. + * + * @param min The minimum value. + */ + inline void setMinTemp(const float min) + { _minTemp = HANumeric(min, _precision); } + + /** + * Sets the maximum temperature that can be set from the Home Assistant panel. + * + * @param min The maximum value. + */ + inline void setMaxTemp(const float max) + { _maxTemp = HANumeric(max, _precision); } + + /** + * Sets the step of the temperature that can be set from the Home Assistant panel. + * + * @param step The setp value. By default it's `1`. + */ + inline void setTempStep(const float step) + { _tempStep = HANumeric(step, _precision); } + + /** + * Registers callback that will be called each time the aux state command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same HVAC. + * + * @param callback + * @note The aux state must be reported back to HA using the HAHVAC::setAuxState method. + */ + inline void onAuxStateCommand(HAHVAC_CALLBACK_BOOL(callback)) + { _auxCallback = callback; } + + /** + * Registers callback that will be called each time the power command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same HVAC. + * + * @param callback + */ + inline void onPowerCommand(HAHVAC_CALLBACK_BOOL(callback)) + { _powerCallback = callback; } + + /** + * Registers callback that will be called each time the fan mode command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same HVAC. + * + * @param callback + * @note The fan mode must be reported back to HA using the HAHVAC::setFanMode method. + */ + inline void onFanModeCommand(HAHVAC_CALLBACK_FAN_MODE(callback)) + { _fanModeCallback = callback; } + + /** + * Registers callback that will be called each time the swing mode command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same HVAC. + * + * @param callback + * @note The swing mode must be reported back to HA using the HAHVAC::setSwingMode method. + */ + inline void onSwingModeCommand(HAHVAC_CALLBACK_SWING_MODE(callback)) + { _swingModeCallback = callback; } + + /** + * Registers callback that will be called each time the HVAC mode command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same HVAC. + * + * @param callback + * @note The mode must be reported back to HA using the HAHVAC::setMode method. + */ + inline void onModeCommand(HAHVAC_CALLBACK_MODE(callback)) + { _modeCallback = callback; } + + /** + * Registers callback that will be called each time the target temperature is set via HA panel. + * Please note that it's not possible to register multiple callbacks for the same HVAC. + * + * @param callback + * @note The target temperature must be reported back to HA using the HAHVAC::setTargetTemperature method. + */ + inline void onTargetTemperatureCommand(HAHVAC_CALLBACK_TARGET_TEMP(callback)) + { _targetTemperatureCallback = callback; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /** + * Publishes the MQTT message with the given current temperature. + * + * @param temperature The temperature to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishCurrentTemperature(const HANumeric& temperature); + + /** + * Publishes the MQTT message with the given action. + * + * @param action The action to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishAction(const Action action); + + /** + * Publishes the MQTT message with the given aux heating state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishAuxState(const bool state); + + /** + * Publishes the MQTT message with the given fan mode. + * + * @param mode The mode to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishFanMode(const FanMode mode); + + /** + * Publishes the MQTT message with the given swing mode. + * + * @param mode The mode to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishSwingMode(const SwingMode mode); + + /** + * Publishes the MQTT message with the given mode. + * + * @param mode The mode to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishMode(const Mode mode); + + /** + * Publishes the MQTT message with the given target temperature. + * + * @param temperature The temperature to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishTargetTemperature(const HANumeric& temperature); + + /** + * Parses the given aux state command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleAuxStateCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Parses the given power command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handlePowerCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Parses the given fan mode command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleFanModeCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Parses the given swing mode command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleSwingModeCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Parses the given HVAC's mode command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleModeCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Parses the given HVAC's target temperature command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleTargetTemperatureCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Returns progmem string representing value template for the command + * that contains floating point numbers. + */ + const __FlashStringHelper* getCommandWithFloatTemplate(); + + /// Features enabled for the HVAC. + const uint16_t _features; + + /// The precision of temperatures. By default it's `HANumber::PrecisionP1`. + const NumberPrecision _precision; + + /// The icon of the button. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The current temperature of the HVAC. By default it's not set. + HANumeric _currentTemperature; + + /// The current action of the HVAC. By default it's `HAHVAC::UnknownAction`. + Action _action; + + /// The temperature unit for the HVAC. By default it's `HAHVAC::DefaultUnit`. + TemperatureUnit _temperatureUnit; + + /// The minimum temperature that can be set. + HANumeric _minTemp; + + /// The maximum temperature that can be set. + HANumeric _maxTemp; + + /// The step of the temperature that can be set. + HANumeric _tempStep; + + /// Callback that will be called when the aux state command is received from the HA. + HAHVAC_CALLBACK_BOOL(_auxCallback); + + /// The state of the aux heating. By default it's `false`. + bool _auxState; + + /// Callback that will be called when the power command is received from the HA. + HAHVAC_CALLBACK_BOOL(_powerCallback); + + /// The current mode of the fan. By default it's `HAHVAC::UnknownFanMode`. + FanMode _fanMode; + + /// The supported fan modes. By default it's `HAHVAC::DefaultFanModes`. + uint8_t _fanModes; + + /// The serializer for the fan modes. It's `nullptr` if the fan feature is disabled. + HASerializerArray* _fanModesSerializer; + + /// Callback that will be called when the fan mode command is received from the HA. + HAHVAC_CALLBACK_FAN_MODE(_fanModeCallback); + + /// The current swing mode. By default it's `HAHVAC::UnknownSwingMode`. + SwingMode _swingMode; + + /// The supported swing modes. By default it's `HAHVAC::DefaultSwingModes`. + uint8_t _swingModes; + + /// The serializer for the swing modes. It's `nullptr` if the swing feature is disabled. + HASerializerArray* _swingModesSerializer; + + /// Callback that will be called when the swing mode command is received from the HA. + HAHVAC_CALLBACK_SWING_MODE(_swingModeCallback); + + /// The current mode. By default it's `HAHVAC::UnknownMode`. + Mode _mode; + + /// The supported modes. By default it's `HAHVAC::DefaultModes`. + uint8_t _modes; + + /// The serializer for the modes. It's `nullptr` if the modes feature is disabled. + HASerializerArray* _modesSerializer; + + /// Callback that will be called when the mode command is received from the HA. + HAHVAC_CALLBACK_MODE(_modeCallback); + + /// The target temperature of the HVAC. By default it's not set. + HANumeric _targetTemperature; + + /// Callback that will be called when the target temperature is changed via the HA panel. + HAHVAC_CALLBACK_TARGET_TEMP(_targetTemperatureCallback); +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALight.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALight.cpp new file mode 100644 index 0000000..03f6dfe --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALight.cpp @@ -0,0 +1,373 @@ +#include "HALight.h" +#ifndef EX_ARDUINOHA_LIGHT + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +const uint8_t HALight::RGBStringMaxLength = 3*4; // 4 characters per color + +void HALight::RGBColor::fromBuffer(const uint8_t* data, const uint16_t length) +{ + if (length > RGBStringMaxLength) { + return; + } + + uint8_t firstCommaPos = 0; + uint8_t secondCommaPos = 0; + + for (uint8_t i = 0; i < length; i++) { + if (data[i] == ',') { + if (firstCommaPos == 0) { + firstCommaPos = i; + } else if (secondCommaPos == 0) { + secondCommaPos = i; + } + } + } + + if (firstCommaPos == 0 || secondCommaPos == 0) { + return; + } + + const uint8_t redLen = firstCommaPos; + const uint8_t greenLen = secondCommaPos - firstCommaPos - 1; // minus comma + const uint8_t blueLen = length - redLen - greenLen - 2; // minus two commas + + const HANumeric& r = HANumeric::fromStr(data, redLen); + const HANumeric& g = HANumeric::fromStr(&data[redLen + 1], greenLen); + const HANumeric& b = HANumeric::fromStr(&data[redLen + greenLen + 2], blueLen); + + if (r.isUInt8() && g.isUInt8() && b.isUInt8()) { + red = r.toUInt8(); + green = g.toUInt8(); + blue = b.toUInt8(); + isSet = true; + } +} + +HALight::HALight(const char* uniqueId, const uint8_t features) : + HABaseDeviceType(AHATOFSTR(HAComponentLight), uniqueId), + _features(features), + _icon(nullptr), + _retain(false), + _optimistic(false), + _brightnessScale(), + _currentState(false), + _currentBrightness(0), + _minMireds(), + _maxMireds(), + _currentColorTemperature(0), + _currentRGBColor(), + _stateCallback(nullptr), + _brightnessCallback(nullptr), + _colorTemperatureCallback(nullptr), + _rgbColorCallback(nullptr) +{ + +} + +bool HALight::setState(const bool state, const bool force) +{ + if (!force && state == _currentState) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +bool HALight::setBrightness(const uint8_t brightness, const bool force) +{ + if (!force && brightness == _currentBrightness) { + return true; + } + + if (publishBrightness(brightness)) { + _currentBrightness = brightness; + return true; + } + + return false; +} + +bool HALight::setColorTemperature(const uint16_t temperature, const bool force) +{ + if (!force && temperature == _currentColorTemperature) { + return true; + } + + if (publishColorTemperature(temperature)) { + _currentColorTemperature = temperature; + return true; + } + + return false; +} + +bool HALight::setRGBColor(const RGBColor& color, const bool force) +{ + if (!force && color == _currentRGBColor) { + return true; + } + + if (publishRGBColor(color)) { + _currentRGBColor = color; + return true; + } + + return false; +} + +void HALight::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 19); // 19 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + _serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + if (_features & BrightnessFeature) { + _serializer->topic(AHATOFSTR(HABrightnessStateTopic)); + _serializer->topic(AHATOFSTR(HABrightnessCommandTopic)); + + if (_brightnessScale.isSet()) { + _serializer->set( + AHATOFSTR(HABrightnessScaleProperty), + &_brightnessScale, + HASerializer::NumberPropertyType + ); + } + } + + if (_features & ColorTemperatureFeature) { + _serializer->topic(AHATOFSTR(HAColorTemperatureStateTopic)); + _serializer->topic(AHATOFSTR(HAColorTemperatureCommandTopic)); + + if (_minMireds.isSet()) { + _serializer->set( + AHATOFSTR(HAMinMiredsProperty), + &_minMireds, + HASerializer::NumberPropertyType + ); + } + + if (_maxMireds.isSet()) { + _serializer->set( + AHATOFSTR(HAMaxMiredsProperty), + &_maxMireds, + HASerializer::NumberPropertyType + ); + } + } + + if (_features & RGBFeature) { + _serializer->topic(AHATOFSTR(HARGBCommandTopic)); + _serializer->topic(AHATOFSTR(HARGBStateTopic)); + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); + _serializer->topic(AHATOFSTR(HACommandTopic)); +} + +void HALight::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + + if (!_retain) { + publishState(_currentState); + publishBrightness(_currentBrightness); + publishColorTemperature(_currentColorTemperature); + publishRGBColor(_currentRGBColor); + } + + subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); + + if (_features & BrightnessFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HABrightnessCommandTopic)); + } + + if (_features & ColorTemperatureFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HAColorTemperatureCommandTopic)); + } + + if (_features & RGBFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HARGBCommandTopic)); + } +} + +void HALight::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HACommandTopic) + )) { + handleStateCommand(payload, length); + } else if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HABrightnessCommandTopic) + )) { + handleBrightnessCommand(payload, length); + } else if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HAColorTemperatureCommandTopic) + )) { + handleColorTemperatureCommand(payload, length); + } else if ( + HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HARGBCommandTopic) + ) + ) { + handleRGBCommand(payload, length); + } +} + +bool HALight::publishState(const bool state) +{ + return publishOnDataTopic( + AHATOFSTR(HAStateTopic), + AHATOFSTR(state ? HAStateOn : HAStateOff), + true + ); +} + +bool HALight::publishBrightness(const uint8_t brightness) +{ + if (!(_features & BrightnessFeature)) { + return false; + } + + char str[3 + 1] = {0}; // uint8_t digits with null terminator + HANumeric(brightness, 0).toStr(str); + + return publishOnDataTopic(AHATOFSTR(HABrightnessStateTopic), str, true); +} + +bool HALight::publishColorTemperature(const uint16_t temperature) +{ + if (!(_features & ColorTemperatureFeature)) { + return false; + } + + char str[5 + 1] = {0}; // uint16_t digits with null terminator + HANumeric(temperature, 0).toStr(str); + + return publishOnDataTopic(AHATOFSTR(HAColorTemperatureStateTopic), str, true); +} + +bool HALight::publishRGBColor(const RGBColor& color) +{ + if (!(_features & RGBFeature) || !color.isSet) { + return false; + } + + char str[RGBStringMaxLength] = {0}; + uint16_t len = 0; + + // append red color with comma + len += HANumeric(color.red, 0).toStr(&str[0]); + str[len++] = ','; + + // append green color with comma + len += HANumeric(color.green, 0).toStr(&str[len]); + str[len++] = ','; + + // append blue color + HANumeric(color.blue, 0).toStr(&str[len]); + + return publishOnDataTopic(AHATOFSTR(HARGBStateTopic), str, true); +} + +void HALight::handleStateCommand(const uint8_t* cmd, const uint16_t length) +{ + (void)cmd; + + if (!_stateCallback) { + return; + } + + bool state = length == strlen_P(HAStateOn); + _stateCallback(state, this); +} + +void HALight::handleBrightnessCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_brightnessCallback) { + return; + } + + const HANumeric& number = HANumeric::fromStr(cmd, length); + if (number.isUInt8()) { + _brightnessCallback(number.toUInt8(), this); + } +} + +void HALight::handleColorTemperatureCommand( + const uint8_t* cmd, + const uint16_t length +) +{ + if (!_colorTemperatureCallback) { + return; + } + + const HANumeric& number = HANumeric::fromStr(cmd, length); + if (number.isUInt16()) { + _colorTemperatureCallback(number.toUInt16(), this); + } +} + +void HALight::handleRGBCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_rgbColorCallback) { + return; + } + + RGBColor color; + color.fromBuffer(cmd, length); + + if (color.isSet) { + _rgbColorCallback(color, this); + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALight.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALight.h new file mode 100644 index 0000000..021d32f --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALight.h @@ -0,0 +1,421 @@ +#ifndef AHA_HALIGHT_H +#define AHA_HALIGHT_H + +#include "HABaseDeviceType.h" +#include "../utils/HANumeric.h" + +#ifndef EX_ARDUINOHA_LIGHT + +#define HALIGHT_STATE_CALLBACK(name) void (*name)(bool state, HALight* sender) +#define HALIGHT_BRIGHTNESS_CALLBACK(name) void (*name)(uint8_t brightness, HALight* sender) +#define HALIGHT_COLOR_TEMP_CALLBACK(name) void (*name)(uint16_t temperature, HALight* sender) +#define HALIGHT_RGB_COLOR_CALLBACK(name) void (*name)(HALight::RGBColor color, HALight* sender) + +/** + * HALight allows adding a controllable light in the Home Assistant panel. + * The library supports only the state, brightness, color temperature and RGB color. + * If you need more features please open a new GitHub issue. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/light.mqtt/ + */ +class HALight : public HABaseDeviceType +{ +public: + static const uint8_t RGBStringMaxLength; + + enum Features { + DefaultFeatures = 0, + BrightnessFeature = 1, + ColorTemperatureFeature = 2, + RGBFeature = 4 + }; + + struct RGBColor { + uint8_t red; + uint8_t green; + uint8_t blue; + bool isSet; + + RGBColor() : + red(0), green(0), blue(0), isSet(false) { } + + RGBColor(uint8_t r, uint8_t g, uint8_t b) : + red(r), green(g), blue(b), isSet(true) { } + + void operator= (const RGBColor& a) { + red = a.red; + green = a.green; + blue = a.blue; + isSet = a.isSet; + } + + bool operator== (const RGBColor& a) const { + return ( + red == a.red && + green == a.green && + blue == a.blue + ); + } + + bool operator!= (const RGBColor& a) const { + return ( + red != a.red || + green != a.green || + blue != a.blue + ); + } + + void fromBuffer(const uint8_t* data, const uint16_t length); + }; + + /** + * @param uniqueId The unique ID of the light. It needs to be unique in a scope of your device. + * @param features Features that should be enabled for the light. + * You can enable multiple features by using OR bitwise operator, for example: + * `HALight::BrightnessFeature | HALight::ColorTemperatureFeature` + */ + HALight(const char* uniqueId, const uint8_t features = DefaultFeatures); + + /** + * Changes state of the light and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state New state of the light. + * @param force Forces to update state without comparing it to previous known state. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setState(const bool state, const bool force = false); + + /** + * Changes the brightness of the light and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param brightness The new brightness of the light. + * @param force Forces to update the value without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setBrightness(const uint8_t brightness, const bool force = false); + + /** + * Changes the color temperature of the light and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param temperature The new color temperature of the light. + * @param force Forces to update the value without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setColorTemperature(const uint16_t temperature, const bool force = false); + + /** + * Changes the RGB color of the light and publishes MQTT message. + * Please note that if a new color is the same as previous one, + * the MQTT message won't be published. + * + * @param color The new RGB color of the light. + * @param force Forces to update the value without comparing it to a previous known value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setRGBColor(const RGBColor& color, const bool force = false); + + /** + * Alias for `setState(true)`. + */ + inline bool turnOn() + { return setState(true); } + + /** + * Alias for `setState(false)`. + */ + inline bool turnOff() + { return setState(false); } + + /** + * Sets current state of the light without publishing it to Home Assistant. + * This method may be useful if you want to change state before connection + * with MQTT broker is acquired. + * + * @param state New state of the light. + */ + inline void setCurrentState(const bool state) + { _currentState = state; } + + /** + * Returns last known state of the light. + * By default it's `false`. + */ + inline bool getCurrentState() const + { return _currentState; } + + /** + * Sets the current brightness of the light without pushing the value to Home Assistant. + * This method may be useful if you want to change the brightness before the connection + * with the MQTT broker is acquired. + * + * @param brightness The new brightness of the light. + */ + inline void setCurrentBrightness(const uint8_t brightness) + { _currentBrightness = brightness; } + + /** + * Returns the last known brightness of the light. + * By default brightness is set to `0`. + */ + inline uint8_t getCurrentBrightness() const + { return _currentBrightness; } + + /** + * Sets the current color temperature of the light without pushing the value to Home Assistant. + * This method may be useful if you want to change the color temperature before the connection + * with the MQTT broker is acquired. + * + * @param colorTemp The new color temperature (mireds). + */ + inline void setCurrentColorTemperature(const uint16_t temperature) + { _currentColorTemperature = temperature; } + + /** + * Returns the last known color temperature of the light. + * By default temperature is set to `0`. + */ + inline uint16_t getCurrentColorTemperature() const + { return _currentColorTemperature; } + + /** + * Sets the current RGB color of the light without pushing the value to Home Assistant. + * This method may be useful if you want to change the color before the connection + * with the MQTT broker is acquired. + * + * @param color The new RGB color. + */ + inline void setCurrentRGBColor(const RGBColor& color) + { _currentRGBColor = color; } + + /** + * Returns the last known RGB color of the light. + * By default the RGB color is set to `0,0,0`. + */ + inline const RGBColor& getCurrentRGBColor() const + { return _currentRGBColor; } + + /** + * Sets icon of the light. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the light's command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Sets optimistic flag for the light state. + * In this mode the light state doesn't need to be reported back to the HA panel when a command is received. + * By default the optimistic mode is disabled. + * + * @param optimistic The optimistic mode (`true` - enabled, `false` - disabled). + */ + inline void setOptimistic(const bool optimistic) + { _optimistic = optimistic; } + + /** + * Sets the maximum brightness value that can be set via HA panel. + * By default it's `255`. + * + * @param scale The maximum value of the brightness. + */ + inline void setBrightnessScale(const uint8_t scale) + { _brightnessScale.setBaseValue(scale); } + + /** + * Sets the minimum color temperature (mireds) value that can be set via HA panel. + * By default it's `153`. + * + * @param mireds The minimum value of the brightness. + */ + inline void setMinMireds(const uint16_t mireds) + { _minMireds.setBaseValue(mireds); } + + /** + * Sets the maximum color temperature (mireds) value that can be set via HA panel. + * By default it's `500`. + * + * @param mireds The maximum value of the brightness. + */ + inline void setMaxMireds(const uint16_t mireds) + { _maxMireds.setBaseValue(mireds); } + + /** + * Registers callback that will be called each time the state command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same light. + * + * @param callback + * @note In non-optimistic mode, the state must be reported back to HA using the HALight::setState method. + */ + inline void onStateCommand(HALIGHT_STATE_CALLBACK(callback)) + { _stateCallback = callback; } + + /** + * Registers callback that will be called each time the brightness command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same light. + * + * @param callback + * @note In non-optimistic mode, the brightness must be reported back to HA using the HALight::setBrightness method. + */ + inline void onBrightnessCommand(HALIGHT_BRIGHTNESS_CALLBACK(callback)) + { _brightnessCallback = callback; } + + /** + * Registers callback that will be called each time the color temperature command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same light. + * + * @param callback + * @note In non-optimistic mode, the color temperature must be reported back to HA using the HALight::setColorTemperature method. + */ + inline void onColorTemperatureCommand(HALIGHT_COLOR_TEMP_CALLBACK(callback)) + { _colorTemperatureCallback = callback; } + + /** + * Registers callback that will be called each time the RGB color command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same light. + * + * @param callback + * @note In non-optimistic mode, the color must be reported back to HA using the HALight::setRGBColor method. + */ + inline void onRGBColorCommand(HALIGHT_RGB_COLOR_CALLBACK(callback)) + { _rgbColorCallback = callback; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /** + * Publishes the MQTT message with the given state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishState(const bool state); + + /** + * Publishes the MQTT message with the given brightness. + * + * @param brightness The brightness to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishBrightness(const uint8_t brightness); + + /** + * Publishes the MQTT message with the given color temperature (mireds). + * + * @param temperature The color temperature to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishColorTemperature(const uint16_t temperature); + + /** + * Publishes the MQTT message with the given RGB color. + * + * @param color The color to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishRGBColor(const RGBColor& color); + + /** + * Parses the given state command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleStateCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Parses the given brightness command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleBrightnessCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Parses the given color temperature command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleColorTemperatureCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Parses the given RGB color command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleRGBCommand(const uint8_t* cmd, const uint16_t length); + + /// Features enabled for the light. + const uint8_t _features; + + /// The icon of the button. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The optimistic mode of the light (`true` - enabled, `false` - disabled). + bool _optimistic; + + /// The maximum value of the brightness. By default it's 255. + HANumeric _brightnessScale; + + /// The current state of the light. By default it's `false`. + bool _currentState; + + /// The current brightness of the light. By default it's `0`. + uint8_t _currentBrightness; + + /// The minimum color temperature (mireds). By default the value is not set. + HANumeric _minMireds; + + /// The maximum color temperature (mireds). By default the value is not set. + HANumeric _maxMireds; + + /// The current color temperature (mireds). By default the value is not set. + uint16_t _currentColorTemperature; + + /// The current RBB color. By default the value is not set. + RGBColor _currentRGBColor; + + /// The callback that will be called when the state command is received from the HA. + HALIGHT_STATE_CALLBACK(_stateCallback); + + /// The callback that will be called when the brightness command is received from the HA. + HALIGHT_BRIGHTNESS_CALLBACK(_brightnessCallback); + + /// The callback that will be called when the color temperature command is received from the HA. + HALIGHT_COLOR_TEMP_CALLBACK(_colorTemperatureCallback); + + /// The callback that will be called when the RGB command is received from the HA. + HALIGHT_RGB_COLOR_CALLBACK(_rgbColorCallback); +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALock.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALock.cpp new file mode 100644 index 0000000..d107458 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALock.cpp @@ -0,0 +1,125 @@ +#include "HALock.h" +#ifndef EX_ARDUINOHA_LOCK + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HALock::HALock(const char* uniqueId) : + HABaseDeviceType(AHATOFSTR(HAComponentLock), uniqueId), + _icon(nullptr), + _retain(false), + _optimistic(false), + _currentState(StateUnknown), + _commandCallback(nullptr) +{ + +} + +bool HALock::setState(const LockState state, const bool force) +{ + if (!force && state == _currentState) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +void HALock::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 10); // 10 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + _serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); + _serializer->topic(AHATOFSTR(HACommandTopic)); +} + +void HALock::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + + if (!_retain) { + publishState(_currentState); + } + + subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); +} + +void HALock::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HACommandTopic) + )) { + handleCommand(payload, length); + } +} + +bool HALock::publishState(const LockState state) +{ + if (state == StateUnknown) { + return false; + } + + return publishOnDataTopic( + AHATOFSTR(HAStateTopic), + AHATOFSTR(state == StateLocked ? HAStateLocked : HAStateUnlocked), + true + ); +} + +void HALock::handleCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_commandCallback) { + return; + } + + if (memcmp_P(cmd, HALockCommand, length) == 0) { + _commandCallback(CommandLock, this); + } else if (memcmp_P(cmd, HAUnlockCommand, length) == 0) { + _commandCallback(CommandUnlock, this); + } else if (memcmp_P(cmd, HAOpenCommand, length) == 0) { + _commandCallback(CommandOpen, this); + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALock.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALock.h new file mode 100644 index 0000000..d8d036f --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HALock.h @@ -0,0 +1,148 @@ +#ifndef AHA_HALOCK_H +#define AHA_HALOCK_H + +#include "HABaseDeviceType.h" + +#ifndef EX_ARDUINOHA_LOCK + +#define HALOCK_CALLBACK(name) void (*name)(LockCommand command, HALock* sender) + +/** + * HALock allows to implement a custom lock (for example: door lock) + * that can be controlled from the Home Assistant panel. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/lock.mqtt/ + */ +class HALock : public HABaseDeviceType +{ +public: + /// Available states of the lock that can be reported to the HA panel. + enum LockState { + StateUnknown = 0, + StateLocked, + StateUnlocked + }; + + /// Commands that will be produced by the HA panel. + enum LockCommand { + CommandLock = 1, + CommandUnlock, + CommandOpen + }; + + /** + * @param uniqueId The unique ID of the lock. It needs to be unique in a scope of your device. + */ + HALock(const char* uniqueId); + + /** + * Changes state of the lock and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state New state of the lock. + * @param force Forces to update state without comparing it to a previous known state. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setState(const LockState state, const bool force = false); + + /** + * Sets current state of the lock without publishing it to Home Assistant. + * This method may be useful if you want to change state before connection + * with MQTT broker is acquired. + * + * @param state New state of the lock. + */ + inline void setCurrentState(const LockState state) + { _currentState = state; } + + /** + * Returns last known state of the lock. + * If setState method wasn't called the initial value will be returned. + */ + inline LockState getCurrentState() const + { return _currentState; } + + /** + * Sets icon of the lock. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the lock's command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Sets optimistic flag for the lock state. + * In this mode the lock state doesn't need to be reported back to the HA panel when a command is received. + * By default the optimistic mode is disabled. + * + * @param optimistic The optimistic mode (`true` - enabled, `false` - disabled). + */ + inline void setOptimistic(const bool optimistic) + { _optimistic = optimistic; } + + /** + * Registers callback that will be called each time the lock/unlock/open command from the HA is received. + * Please note that it's not possible to register multiple callbacks for the same lock. + * + * @param callback + */ + inline void onCommand(HALOCK_CALLBACK(callback)) + { _commandCallback = callback; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /** + * Publishes the MQTT message with the given state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishState(const LockState state); + + /** + * Parses the given command and executes the lock's callback with proper enum's property. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleCommand(const uint8_t* cmd, const uint16_t length); + + /// The icon of the lock. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The optimistic mode of the lock (`true` - enabled, `false` - disabled). + bool _optimistic; + + /// The current state of the lock. By default it's `HALock::StateUnknown`. + LockState _currentState; + + /// The callback that will be called when lock/unlock/open command is received from the HA. + HALOCK_CALLBACK(_commandCallback); +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HANumber.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HANumber.cpp new file mode 100644 index 0000000..2c41345 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HANumber.cpp @@ -0,0 +1,214 @@ +#include "HANumber.h" +#ifndef EX_ARDUINOHA_NUMBER + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HANumber::HANumber(const char* uniqueId, const NumberPrecision precision) : + HABaseDeviceType(AHATOFSTR(HAComponentNumber), uniqueId), + _precision(precision), + _class(nullptr), + _icon(nullptr), + _retain(false), + _optimistic(false), + _mode(ModeAuto), + _unitOfMeasurement(nullptr), + _minValue(), + _maxValue(), + _step(), + _currentState(), + _commandCallback(nullptr) +{ + +} + +bool HANumber::setState(const HANumeric& state, const bool force) +{ + if (!force && state == _currentState) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +void HANumber::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 16); // 16 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + _serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), _unitOfMeasurement); + _serializer->set( + AHATOFSTR(HAModeProperty), + getModeProperty(), + HASerializer::ProgmemPropertyValue + ); + _serializer->set( + AHATOFSTR(HACommandTemplateProperty), + getCommandTemplate(), + HASerializer::ProgmemPropertyValue + ); + + if (_minValue.isSet()) { + _serializer->set( + AHATOFSTR(HAMinProperty), + &_minValue, + HASerializer::NumberPropertyType + ); + } + + if (_maxValue.isSet()) { + _serializer->set( + AHATOFSTR(HAMaxProperty), + &_maxValue, + HASerializer::NumberPropertyType + ); + } + + if (_step.isSet()) { + _serializer->set( + AHATOFSTR(HAStepProperty), + &_step, + HASerializer::NumberPropertyType + ); + } + + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + _serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); + _serializer->topic(AHATOFSTR(HACommandTopic)); +} + +void HANumber::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + + if (!_retain) { + publishState(_currentState); + } + + subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); +} + +void HANumber::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + if (HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HACommandTopic) + )) { + handleCommand(payload, length); + } +} + +bool HANumber::publishState(const HANumeric& state) +{ + if (!state.isSet()) { + return publishOnDataTopic( + AHATOFSTR(HAStateTopic), + AHATOFSTR(HAStateNone), + true + ); + } + + const uint8_t size = state.calculateSize(); + if (size == 0) { + return false; + } + + char str[size + 1]; // with null terminator + str[size] = 0; + state.toStr(str); + + return publishOnDataTopic( + AHATOFSTR(HAStateTopic), + str, + true + ); +} + +void HANumber::handleCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_commandCallback) { + return; + } + + if (memcmp_P(cmd, HAStateNone, length) == 0) { + _commandCallback(HANumeric(), this); + } else { + HANumeric number = HANumeric::fromStr(cmd, length); + if (number.isSet()) { + number.setPrecision(_precision); + _commandCallback(number, this); + } + } +} + +const __FlashStringHelper* HANumber::getModeProperty() const +{ + switch (_mode) { + case ModeBox: + return AHATOFSTR(HAModeBox); + + case ModeSlider: + return AHATOFSTR(HAModeSlider); + + default: + return nullptr; + } +} + +const __FlashStringHelper* HANumber::getCommandTemplate() +{ + switch (_precision) { + case PrecisionP1: + return AHATOFSTR(HAValueTemplateFloatP1); + + case PrecisionP2: + return AHATOFSTR(HAValueTemplateFloatP2); + + case PrecisionP3: + return AHATOFSTR(HAValueTemplateFloatP3); + + default: + return nullptr; + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HANumber.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HANumber.h new file mode 100644 index 0000000..e20d6fe --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HANumber.h @@ -0,0 +1,263 @@ +#ifndef AHA_HANUMBER_H +#define AHA_HANUMBER_H + +#include "HABaseDeviceType.h" +#include "../utils/HANumeric.h" + +#ifndef EX_ARDUINOHA_NUMBER + +#define _SET_STATE_OVERLOAD(type) \ + /** @overload */ \ + inline bool setState(const type state, const bool force = false) \ + { return setState(HANumeric(state, _precision), force); } + +#define _SET_CURRENT_STATE_OVERLOAD(type) \ + /** @overload */ \ + inline void setCurrentState(const type state) \ + { setCurrentState(HANumeric(state, _precision)); } + +#define HANUMBER_CALLBACK(name) void (*name)(HANumeric number, HANumber* sender) + +/** + * HANumber adds a slider or a box in the Home Assistant panel + * that controls the numeric value stored on your device. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/number.mqtt/ + */ +class HANumber : public HABaseDeviceType +{ +public: + /// Represents mode of the number. + enum Mode { + ModeAuto = 0, + ModeBox, + ModeSlider + }; + + /** + * Creates instance of the HANumber entity with the given numbers precision. + * The given precision applies to the state, min, max and step values. + * + * @param uniqueId The unique ID of the number. It needs to be unique in a scope of your device. + * @param precision Precision of the floating point number that will be displayed in the HA panel. + */ + HANumber(const char* uniqueId, const NumberPrecision precision = PrecisionP0); + + /** + * Changes state of the number and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state New state of the number. + * @param force Forces to update state without comparing it to a previous known state. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setState(const HANumeric& state, const bool force = false); + + _SET_STATE_OVERLOAD(int8_t) + _SET_STATE_OVERLOAD(int16_t) + _SET_STATE_OVERLOAD(int32_t) + _SET_STATE_OVERLOAD(uint8_t) + _SET_STATE_OVERLOAD(uint16_t) + _SET_STATE_OVERLOAD(uint32_t) + _SET_STATE_OVERLOAD(float) + +#ifdef ARDUINOHA_INT_OVERLOAD + _SET_STATE_OVERLOAD(int) +#endif + + /** + * Sets current state of the number without publishing it to Home Assistant. + * This method may be useful if you want to change state before connection + * with MQTT broker is acquired. + * + * @param state New state of the number. + */ + inline void setCurrentState(const HANumeric& state) + { if (state.getPrecision() == _precision) { _currentState = state; } } + + _SET_CURRENT_STATE_OVERLOAD(int8_t) + _SET_CURRENT_STATE_OVERLOAD(int16_t) + _SET_CURRENT_STATE_OVERLOAD(int32_t) + _SET_CURRENT_STATE_OVERLOAD(uint8_t) + _SET_CURRENT_STATE_OVERLOAD(uint16_t) + _SET_CURRENT_STATE_OVERLOAD(uint32_t) + _SET_CURRENT_STATE_OVERLOAD(float) + +#ifdef ARDUINOHA_INT_OVERLOAD + _SET_CURRENT_STATE_OVERLOAD(int) +#endif + + /** + * Returns last known state of the number. + * If setState method wasn't called the initial value will be returned. + */ + inline const HANumeric& getCurrentState() const + { return _currentState; } + + /** + * Sets class of the device. + * You can find list of available values here: https://www.home-assistant.io/integrations/number/#device-class + * + * @param deviceClass The class name. + */ + inline void setDeviceClass(const char* deviceClass) + { _class = deviceClass; } + + /** + * Sets icon of the number. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the number's command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Sets optimistic flag for the number state. + * In this mode the number state doesn't need to be reported back to the HA panel when a command is received. + * By default the optimistic mode is disabled. + * + * @param optimistic The optimistic mode (`true` - enabled, `false` - disabled). + */ + inline void setOptimistic(const bool optimistic) + { _optimistic = optimistic; } + + /** + * Sets mode of the number. + * It controls how the number should be displayed in the UI. + * By default it's `HANumber::ModeAuto`. + * + * @param mode Mode to set. + */ + inline void setMode(const Mode mode) + { _mode = mode; } + + /** + * Defines the units of measurement of the number, if any. + * + * @param units For example: °C, % + */ + inline void setUnitOfMeasurement(const char* unitOfMeasurement) + { _unitOfMeasurement = unitOfMeasurement; } + + /** + * Sets the minimum value that can be set from the Home Assistant panel. + * + * @param min The minimal value. By default the value is not set. + */ + inline void setMin(const float min) + { _minValue = HANumeric(min, _precision); } + + /** + * Sets the maximum value that can be set from the Home Assistant panel. + * + * @param min The maximum value. By default the value is not set. + */ + inline void setMax(const float max) + { _maxValue = HANumeric(max, _precision); } + + /** + * Sets step of the slider's movement in the Home Assistant panel. + * + * @param step The step value. Smallest value `0.001`. By default the value is not set. + */ + inline void setStep(const float step) + { _step = HANumeric(step, _precision); } + + /** + * Registers callback that will be called each time the number is changed in the HA panel. + * Please note that it's not possible to register multiple callbacks for the same number. + * + * @param callback + * @note In non-optimistic mode, the number must be reported back to HA using the HANumber::setState method. + */ + inline void onCommand(HANUMBER_CALLBACK(callback)) + { _commandCallback = callback; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /** + * Publishes the MQTT message with the given state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishState(const HANumeric& state); + + /** + * Parses the given command and executes the number's callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleCommand(const uint8_t* cmd, const uint16_t length); + + /** + * Returns progmem string representing mode of the number + */ + const __FlashStringHelper* getModeProperty() const; + + /** + * Returns progmem string representing value template for the command. + */ + const __FlashStringHelper* getCommandTemplate(); + + /// The precision of the number. By default it's `HANumber::PrecisionP0`. + const NumberPrecision _precision; + + /// The device class. It can be nullptr. + const char* _class; + + /// The icon of the number. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The optimistic mode of the number (`true` - enabled, `false` - disabled). + bool _optimistic; + + /// Controls how the number should be displayed in the UI. By default it's `HANumber::ModeAuto`. + Mode _mode; + + /// The unit of measurement for the sensor. It can be nullptr. + const char* _unitOfMeasurement; + + /// The minimal value that can be set from the HA panel. + HANumeric _minValue; + + /// The maximum value that can be set from the HA panel. + HANumeric _maxValue; + + /// The step of the slider's movement. + HANumeric _step; + + /// The current state of the number. By default the value is not set. + HANumeric _currentState; + + /// The callback that will be called when the command is received from the HA. + HANUMBER_CALLBACK(_commandCallback); +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAScene.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAScene.cpp new file mode 100644 index 0000000..c579c68 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAScene.cpp @@ -0,0 +1,77 @@ +#include "HAScene.h" +#ifndef EX_ARDUINOHA_SCENE + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HAScene::HAScene(const char* uniqueId) : + HABaseDeviceType(AHATOFSTR(HAComponentScene), uniqueId), + _icon(nullptr), + _retain(false), + _commandCallback(nullptr) +{ + +} + +void HAScene::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 8); // 8 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + + // optional property + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + // HA 2022.10 throws an exception if this property is not set + _serializer->set( + AHATOFSTR(HAPayloadOnProperty), + AHATOFSTR(HAStateOn), + HASerializer::ProgmemPropertyValue + ); + + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HACommandTopic)); +} + +void HAScene::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); +} + +void HAScene::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + (void)payload; + (void)length; + + if (_commandCallback && HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HACommandTopic) + )) { + _commandCallback(this); + } +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAScene.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAScene.h new file mode 100644 index 0000000..b5105a1 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HAScene.h @@ -0,0 +1,73 @@ +#ifndef AHA_HASCENE_H +#define AHA_HASCENE_H + +#include "HABaseDeviceType.h" + +#ifndef EX_ARDUINOHA_SCENE + +#define HASCENE_CALLBACK(name) void (*name)(HAScene* sender) + +/** + * HAScene adds a new scene to the Home Assistant that triggers your callback once activated. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/scene.mqtt/ + */ +class HAScene : public HABaseDeviceType +{ +public: + /** + * @param uniqueId The unique ID of the scene. It needs to be unique in a scope of your device. + */ + HAScene(const char* uniqueId); + + /** + * Sets icon of the scene. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the scene's command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Registers callback that will be called when the scene is activated in the HA panel. + * Please note that it's not possible to register multiple callbacks for the same scene. + * + * @param callback + */ + inline void onCommand(HASCENE_CALLBACK(callback)) + { _commandCallback = callback; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /// The icon of the scene. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The command callback that will be called when scene is activated from the HA panel. + HASCENE_CALLBACK(_commandCallback); +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASelect.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASelect.cpp new file mode 100644 index 0000000..b4bff01 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASelect.cpp @@ -0,0 +1,208 @@ +#include "HASelect.h" +#ifndef EX_ARDUINOHA_SELECT + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HASelect::HASelect(const char* uniqueId) : + HABaseDeviceType(AHATOFSTR(HAComponentSelect), uniqueId), + _options(nullptr), + _currentState(-1), + _icon(nullptr), + _retain(false), + _optimistic(false), + _commandCallback(nullptr) +{ + +} + +HASelect::~HASelect() +{ + if (_options) { + const uint8_t optionsNb = _options->getItemsNb(); + const HASerializerArray::ItemType* options = _options->getItems(); + + if (optionsNb > 1) { + for (uint8_t i = 0; i < optionsNb; i++) { + delete options[i]; + } + } + + delete _options; + } +} + +void HASelect::setOptions(const char* options) +{ + if (!options || _options) { // options can be set only once + return; + } + + const uint16_t optionsNb = countOptionsInString(options); + if (optionsNb == 0) { + return; + } + + const uint16_t optionsLen = strlen(options) + 1; // include null terminator + _options = new HASerializerArray(optionsNb, false); + + if (optionsNb == 1) { + _options->add(options); + return; + } + + uint8_t optionLen = 0; + for (uint16_t i = 0; i < optionsLen; i++) { + if (options[i] == ';' || options[i] == 0) { + if (optionLen == 0) { + break; + } + + char* option = new char[optionLen + 1]; // including null terminator + option[optionLen] = 0; + memcpy(option, &options[i - optionLen], optionLen); + + _options->add(option); + optionLen = 0; + continue; + } + + optionLen++; + } +} + +bool HASelect::setState(const int8_t state, const bool force) +{ + if (!force && _currentState == state) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +const char* HASelect::getCurrentOption() const +{ + return _options->getItem(getCurrentState()); +} + +void HASelect::buildSerializer() +{ + if (_serializer || !uniqueId() || !_options) { + return; + } + + _serializer = new HASerializer(this, 11); // 11 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + _serializer->set( + AHATOFSTR(HAOptionsProperty), + _options, + HASerializer::ArrayPropertyType + ); + + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + _serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); + _serializer->topic(AHATOFSTR(HACommandTopic)); +} + +void HASelect::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + + if (!_retain) { + publishState(_currentState); + } + + subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); +} + +void HASelect::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + if (_commandCallback && HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HACommandTopic) + )) { + const uint8_t optionsNb = _options->getItemsNb(); + const HASerializerArray::ItemType* options = _options->getItems(); + + for (uint8_t i = 0; i < optionsNb; i++) { + if (memcmp(payload, options[i], length) == 0) { + _commandCallback(i, this); + return; + } + } + } +} + +bool HASelect::publishState(const int8_t state) +{ + if (!_options || state >= _options->getItemsNb()) { + return false; + } + + if (state < 0) { + return publishOnDataTopic(AHATOFSTR(HAStateTopic), AHATOFSTR(HAStateNone), true); + } + + const char* item = _options->getItems()[state]; + if (!item) { + return false; + } + + return publishOnDataTopic(AHATOFSTR(HAStateTopic), item, true); +} + +uint8_t HASelect::countOptionsInString(const char* options) const +{ + // the given string is treated as a single option if there are no semicolons + uint8_t optionsNb = 1; + const uint16_t optionsLen = strlen(options); + + if (optionsLen == 0) { + return 0; + } + + for (uint8_t i = 0; i < optionsLen; i++) { + if (options[i] == ';') { + optionsNb++; + } + } + + return optionsNb; +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASelect.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASelect.h new file mode 100644 index 0000000..a6c0f99 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASelect.h @@ -0,0 +1,162 @@ +#ifndef AHA_HASELECT_H +#define AHA_HASELECT_H + +#include "HABaseDeviceType.h" + +#ifndef EX_ARDUINOHA_SELECT + +class HASerializerArray; + +#define HASELECT_CALLBACK(name) void (*name)(int8_t index, HASelect* sender) + +/** + * HASelect adds a dropdown with options in the Home Assistant panel. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/button.mqtt/ + */ +class HASelect : public HABaseDeviceType +{ +public: + /** + * @param uniqueId The unique ID of the select. It needs to be unique in a scope of your device. + */ + HASelect(const char* uniqueId); + ~HASelect(); + + /** + * Sets the list of available options that will be listed in the dropdown. + * The input string should contain options separated using semicolons. + * For example: `setOptions("Option A;Option B;Option C"); + * + * @param options The list of options that are separated by semicolons. + * @note The options list can be set only once. + */ + void setOptions(const char* options); + + /** + * Changes state of the select and publishes MQTT message. + * State represents the index of the option that was set using the setOptions method. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state New state of the select. You can set `-1` to reset the select. + * @param force Forces to update state without comparing it to previous known state. + * @returns Returns true if MQTT message has been published successfully. + */ + bool setState(const int8_t state, const bool force = false); + + /** + * Returns the selected option based on the most recent state of the select. + * You can utilize this method to get the string representation of the option (e.g. for printing). + * If no option is selected, null is returned. + */ + const char* getCurrentOption() const; + + /** + * Sets the current state of the select without publishing it to Home Assistant. + * State represents the index of the option that was set using the setOptions method. + * This method may be useful if you want to change the state before the connection + * with the MQTT broker is acquired. + * + * @param state The new state of the cover. + */ + inline void setCurrentState(const int8_t state) + { _currentState = state; } + + /** + * Returns last known state of the select. + * State represents the index of the option that was set using the setOptions method. + * By default the state is set to `-1`. + */ + inline int8_t getCurrentState() const + { return _currentState; } + + /** + * Sets icon of the select. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the select's command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Sets optimistic flag for the select state. + * In this mode the select state doesn't need to be reported back to the HA panel when a command is received. + * By default the optimistic mode is disabled. + * + * @param optimistic The optimistic mode (`true` - enabled, `false` - disabled). + */ + inline void setOptimistic(const bool optimistic) + { _optimistic = optimistic; } + + /** + * Registers callback that will be called each time the option is changed from the HA panel. + * Please note that it's not possible to register multiple callbacks for the same select. + * + * @param callback + * @note In non-optimistic mode, the selected option must be reported back to HA using the HASelect::setState method. + */ + inline void onCommand(HASELECT_CALLBACK(callback)) + { _commandCallback = callback; } + +#ifdef ARDUINOHA_TEST + inline HASerializerArray* getOptions() const + { return _options; } +#endif + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /** + * Publishes the MQTT message with the given state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishState(const int8_t state); + + /** + * Counts the amount of options in the given string. + */ + uint8_t countOptionsInString(const char* options) const; + + /// Array of options for the serializer. + HASerializerArray* _options; + + /// Stores the current state (the current option's index). By default it's `-1`. + int8_t _currentState; + + /// The icon of the select. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The optimistic mode of the select (`true` - enabled, `false` - disabled). + bool _optimistic; + + /// The command callback that will be called when option is changed via the HA panel. + HASELECT_CALLBACK(_commandCallback); +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensor.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensor.cpp new file mode 100644 index 0000000..0d2efe9 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensor.cpp @@ -0,0 +1,93 @@ +#include "HASensor.h" +#ifndef EX_ARDUINOHA_SENSOR + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HASensor::HASensor(const char* uniqueId, const uint16_t features) : + HABaseDeviceType(AHATOFSTR(HAComponentSensor), uniqueId), + _features(features), + _deviceClass(nullptr), + _stateClass(nullptr), + _forceUpdate(false), + _icon(nullptr), + _unitOfMeasurement(nullptr), + _expireAfter() +{ + +} + +bool HASensor::setValue(const char* value) +{ + if (!value) { + return publishOnDataTopic(AHATOFSTR(HAStateTopic), AHATOFSTR(HAStateNone), true); + } + + return publishOnDataTopic(AHATOFSTR(HAStateTopic), value, true); +} + +bool HASensor::setJsonAttributes(const char* json) +{ + return publishOnDataTopic(AHATOFSTR(HAJsonAttributesTopic), json, true); +} + +void HASensor::setExpireAfter(uint16_t expireAfter) +{ + if (expireAfter > 0) { + _expireAfter.setBaseValue(expireAfter); + } else { + _expireAfter.reset(); + } +} + +void HASensor::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 13); // 13 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HADeviceClassProperty), _deviceClass); + _serializer->set(AHATOFSTR(HAStateClassProperty), _stateClass); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + _serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), _unitOfMeasurement); + + if (_forceUpdate) { + _serializer->set( + AHATOFSTR(HAForceUpdateProperty), + &_forceUpdate, + HASerializer::BoolPropertyType + ); + } + + if (_expireAfter.isSet()) { + _serializer->set( + AHATOFSTR(HAExpireAfterProperty), + &_expireAfter, + HASerializer::NumberPropertyType + ); + } + + if (_features & JsonAttributesFeature) { + _serializer->topic(AHATOFSTR(HAJsonAttributesTopic)); + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); +} + +void HASensor::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensor.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensor.h new file mode 100644 index 0000000..6dbd281 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensor.h @@ -0,0 +1,130 @@ +#ifndef AHA_HASENSOR_H +#define AHA_HASENSOR_H + +#include "HABaseDeviceType.h" +#include "../utils/HANumeric.h" + +#ifndef EX_ARDUINOHA_SENSOR + +/** + * HASensor allows to publish textual sensor values that will be displayed in the HA panel. + * If you need to publish numbers then HASensorNumber is what you're looking for. + * + * @note It's not possible to define a sensor that publishes mixed values (e.g. string + integer values). + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/sensor.mqtt/ + */ +class HASensor : public HABaseDeviceType +{ +public: + enum Features { + DefaultFeatures = 0, + JsonAttributesFeature = 1 + }; + + /** + * @param uniqueId The unique ID of the sensor. It needs to be unique in a scope of your device. + * @param features Features that should be enabled for the sensor. + */ + HASensor(const char* uniqueId, const uint16_t features = DefaultFeatures); + + /** + * Publishes the MQTT message with the given value. + * Unlike the other device types, the HASensor doesn't store the previous value that was set. + * It means that the MQTT message is produced each time the setValue method is called. + * + * @param value String representation of the sensor's value. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setValue(const char* value); + + /** + * Publishes the given JSON data on the JSON attributes topic. + * The `JsonAttributesFeature` has to be enabled prior to setting the value. + * + * @param json JSON attributes. + */ + bool setJsonAttributes(const char* json); + + /** + * Sets the number of seconds after the sensor’s state expires, if it’s not updated. + * By default the sensors state never expires. + * + * @param expireAfter The number of seconds. + */ + void setExpireAfter(uint16_t expireAfter); + + /** + * Sets class of the device. + * You can find list of available values here: https://www.home-assistant.io/integrations/sensor/#device-class + * + * @param deviceClass The class name. + */ + inline void setDeviceClass(const char* deviceClass) + { _deviceClass = deviceClass; } + + /** + * Sets class of the state for the long term stats. + * See: https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics + * + * @param stateClass The class name. + */ + inline void setStateClass(const char* stateClass) + { _stateClass = stateClass; } + + /** + * Forces HA panel to process each incoming value (MQTT message). + * It's useful if you want to have meaningful value graphs in history. + * + * @param forceUpdate + */ + inline void setForceUpdate(bool forceUpdate) + { _forceUpdate = forceUpdate; } + + /** + * Sets icon of the sensor. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param class The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Defines the units of measurement of the sensor, if any. + * + * @param units For example: °C, % + */ + inline void setUnitOfMeasurement(const char* unitOfMeasurement) + { _unitOfMeasurement = unitOfMeasurement; } + +protected: + virtual void buildSerializer() override final; + virtual void onMqttConnected() override; + +private: + /// Features enabled for the sensor. + const uint16_t _features; + + /// The device class. It can be nullptr. + const char* _deviceClass; + + /// The state class for the long term stats. It can be nullptr. See: https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics + const char* _stateClass; + + /// The force update flag for the HA panel. + bool _forceUpdate; + + /// The icon of the sensor. It can be nullptr. + const char* _icon; + + /// The unit of measurement for the sensor. It can be nullptr. + const char* _unitOfMeasurement; + + /// It defines the number of seconds after the sensor’s state expires, if it’s not updated. By default the sensors state never expires. + HANumeric _expireAfter; +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensorNumber.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensorNumber.cpp new file mode 100644 index 0000000..1f34627 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensorNumber.cpp @@ -0,0 +1,68 @@ +#include "HASensorNumber.h" +#ifndef EX_ARDUINOHA_SENSOR + +#include "../utils/HASerializer.h" + +HASensorNumber::HASensorNumber( + const char* uniqueId, + const NumberPrecision precision, + const uint16_t features +) : + HASensor(uniqueId, features), + _precision(precision), + _currentValue() +{ + +} + +bool HASensorNumber::setValue(const HANumeric& value, const bool force) +{ + if (value.getPrecision() != _precision) { + return false; + } + + if (!force && value == _currentValue) { + return true; + } + + if (publishValue(value)) { + _currentValue = value; + return true; + } + + return false; +} + +void HASensorNumber::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + HASensor::onMqttConnected(); + publishValue(_currentValue); +} + +bool HASensorNumber::publishValue(const HANumeric& value) +{ + if (!value.isSet()) { + return false; + } + + uint8_t size = value.calculateSize(); + if (size == 0) { + return false; + } + + char str[size + 1]; // with null terminator + str[size] = 0; + value.toStr(str); + + return publishOnDataTopic( + AHATOFSTR(HAStateTopic), + str, + true + ); +} + +#endif \ No newline at end of file diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensorNumber.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensorNumber.h new file mode 100644 index 0000000..d530737 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASensorNumber.h @@ -0,0 +1,107 @@ +#ifndef AHA_HASENSORNUMBER_H +#define AHA_HASENSORNUMBER_H + +#include "HASensor.h" + +#ifndef EX_ARDUINOHA_SENSOR + +#define _SET_VALUE_OVERLOAD(type) \ + /** @overload */ \ + inline bool setValue(const type value, const bool force = false) \ + { return setValue(HANumeric(value, _precision), force); } + +#define _SET_CURRENT_VALUE_OVERLOAD(type) \ + /** @overload */ \ + inline void setCurrentValue(const type value) \ + { setCurrentValue(HANumeric(value, _precision)); } + +/** + * HASensorInteger allows to publish numeric values of a sensor that will be displayed in the HA panel. + * + * @note You can find more information about this class in HASensor documentation. + */ +class HASensorNumber : public HASensor +{ +public: + /** + * @param uniqueId The unique ID of the sensor. It needs to be unique in a scope of your device. + * @param precision Precision of the floating point number that will be displayed in the HA panel. + * @param features Features that should be enabled for the sensor. + */ + HASensorNumber( + const char* uniqueId, + const NumberPrecision precision = PrecisionP0, + const uint16_t features = DefaultFeatures + ); + + /** + * Changes value of the sensor and publish MQTT message. + * Please note that if a new value is the same as the previous one the MQTT message won't be published. + * + * @param value New value of the sensor. THe precision of the value needs to match precision of the sensor. + * @param force Forces to update the value without comparing it to a previous known value. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool setValue(const HANumeric& value, const bool force = false); + + _SET_VALUE_OVERLOAD(int8_t) + _SET_VALUE_OVERLOAD(int16_t) + _SET_VALUE_OVERLOAD(int32_t) + _SET_VALUE_OVERLOAD(uint8_t) + _SET_VALUE_OVERLOAD(uint16_t) + _SET_VALUE_OVERLOAD(uint32_t) + _SET_VALUE_OVERLOAD(float) + +#ifdef ARDUINOHA_INT_OVERLOAD + _SET_VALUE_OVERLOAD(int) +#endif + + /** + * Sets the current value of the sensor without publishing it to Home Assistant. + * This method may be useful if you want to change the value before the connection with the MQTT broker is acquired. + * + * @param value New value of the sensor. + */ + inline void setCurrentValue(const HANumeric& value) + { if (value.getPrecision() == _precision) { _currentValue = value; } } + + _SET_CURRENT_VALUE_OVERLOAD(int8_t) + _SET_CURRENT_VALUE_OVERLOAD(int16_t) + _SET_CURRENT_VALUE_OVERLOAD(int32_t) + _SET_CURRENT_VALUE_OVERLOAD(uint8_t) + _SET_CURRENT_VALUE_OVERLOAD(uint16_t) + _SET_CURRENT_VALUE_OVERLOAD(uint32_t) + _SET_CURRENT_VALUE_OVERLOAD(float) + +#ifdef ARDUINOHA_INT_OVERLOAD + _SET_CURRENT_VALUE_OVERLOAD(int) +#endif + + /** + * Returns the last known value of the sensor. + * By default the value is not set. + */ + inline const HANumeric& getCurrentValue() const + { return _currentValue; } + +protected: + virtual void onMqttConnected() override; + +private: + /** + * Publishes the MQTT message with the given value. + * + * @param state The value to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishValue(const HANumeric& value); + + /// The precision of the sensor. By default it's `HASensorNumber::PrecisionP0`. + const NumberPrecision _precision; + + /// The current value of the sensor. By default the value is not set. + HANumeric _currentValue; +}; + +#endif +#endif \ No newline at end of file diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASwitch.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASwitch.cpp new file mode 100644 index 0000000..85da4f3 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASwitch.cpp @@ -0,0 +1,112 @@ +#include "HASwitch.h" +#ifndef EX_ARDUINOHA_SWITCH + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HASwitch::HASwitch(const char* uniqueId) : + HABaseDeviceType(AHATOFSTR(HAComponentSwitch), uniqueId), + _class(nullptr), + _icon(nullptr), + _retain(false), + _optimistic(false), + _currentState(false), + _commandCallback(nullptr) +{ + +} + +bool HASwitch::setState(const bool state, const bool force) +{ + if (!force && state == _currentState) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +void HASwitch::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 11); // 11 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + + // optional property + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + _serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); + _serializer->topic(AHATOFSTR(HACommandTopic)); +} + +void HASwitch::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + + if (!_retain) { + publishState(_currentState); + } + + subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); +} + +void HASwitch::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + (void)payload; + + if (_commandCallback && HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HACommandTopic) + )) { + bool state = length == strlen_P(HAStateOn); + _commandCallback(state, this); + } +} + +bool HASwitch::publishState(const bool state) +{ + return publishOnDataTopic( + AHATOFSTR(HAStateTopic), + AHATOFSTR(state ? HAStateOn : HAStateOff), + true + ); +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASwitch.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASwitch.h new file mode 100644 index 0000000..7179b28 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HASwitch.h @@ -0,0 +1,150 @@ +#ifndef AHA_HASWITCH_H +#define AHA_HASWITCH_H + +#include "HABaseDeviceType.h" + +#ifndef EX_ARDUINOHA_SWITCH + +#define HASWITCH_CALLBACK(name) void (*name)(bool state, HASwitch* sender) + +/** + * HASwitch allows to display on/off switch in the HA panel and receive commands on your device. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/switch.mqtt/ + */ +class HASwitch : public HABaseDeviceType +{ +public: + /** + * @param uniqueId The unique ID of the sensor. It needs to be unique in a scope of your device. + */ + HASwitch(const char* uniqueId); + + /** + * Changes state of the switch and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state New state of the switch. + * @param force Forces to update state without comparing it to previous known state. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setState(const bool state, const bool force = false); + + /** + * Alias for `setState(true)`. + */ + inline bool turnOn() + { return setState(true); } + + /** + * Alias for `setState(false)`. + */ + inline bool turnOff() + { return setState(false); } + + /** + * Sets current state of the switch without publishing it to Home Assistant. + * This method may be useful if you want to change state before connection + * with MQTT broker is acquired. + * + * @param state New state of the switch. + */ + inline void setCurrentState(const bool state) + { _currentState = state; } + + /** + * Returns last known state of the switch. + * By default it's `false`. + */ + inline bool getCurrentState() const + { return _currentState; } + + /** + * Sets class of the device. + * You can find list of available values here: https://www.home-assistant.io/integrations/switch/#device-class + * + * @param deviceClass The class name. + */ + inline void setDeviceClass(const char* deviceClass) + { _class = deviceClass; } + + /** + * Sets icon of the sensor. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the switch command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Sets optimistic flag for the switch state. + * In this mode the switch state doesn't need to be reported back to the HA panel when a command is received. + * By default the optimistic mode is disabled. + * + * @param optimistic The optimistic mode (`true` - enabled, `false` - disabled). + */ + inline void setOptimistic(const bool optimistic) + { _optimistic = optimistic; } + + /** + * Registers callback that will be called each time the on/off command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same switch. + * + * @param callback + * @note In non-optimistic mode, the state must be reported back to HA using the HASwitch::setState method. + */ + inline void onCommand(HASWITCH_CALLBACK(callback)) + { _commandCallback = callback; } + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /** + * Publishes the MQTT message with the given state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishState(const bool state); + + /// The device class. It can be nullptr. + const char* _class; + + /// The icon of the button. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The optimistic mode of the switch (`true` - enabled, `false` - disabled). + bool _optimistic; + + /// The current state of the switch. By default it's `false`. + bool _currentState; + + /// The callback that will be called when switch command is received from the HA. + HASWITCH_CALLBACK(_commandCallback); +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HATagScanner.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HATagScanner.cpp new file mode 100644 index 0000000..efac4fe --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HATagScanner.cpp @@ -0,0 +1,42 @@ +#include "HATagScanner.h" +#ifndef EX_ARDUINOHA_TAG_SCANNER + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HATagScanner::HATagScanner(const char* uniqueId) : + HABaseDeviceType(AHATOFSTR(HAComponentTag), uniqueId) +{ + +} + +bool HATagScanner::tagScanned(const char* tag) +{ + if (!tag || strlen(tag) == 0) { + return false; + } + + return publishOnDataTopic(AHATOFSTR(HATopic), tag); +} + +void HATagScanner::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 2); // 2 - max properties nb + _serializer->set(HASerializer::WithDevice); + _serializer->topic(AHATOFSTR(HATopic)); +} + +void HATagScanner::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HATagScanner.h b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HATagScanner.h new file mode 100644 index 0000000..6f0c576 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/device-types/HATagScanner.h @@ -0,0 +1,38 @@ +#ifndef AHA_HATAGSCANNER_H +#define AHA_HATAGSCANNER_H + +#include "HABaseDeviceType.h" + +#ifndef EX_ARDUINOHA_TAG_SCANNER + +/** + * HATagScanner allow to produce scan events that can be used in the HA automation. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/tag.mqtt/ + */ +class HATagScanner : public HABaseDeviceType +{ +public: + /** + * @param uniqueId The unique ID of the scanner. It needs to be unique in a scope of your device. + */ + HATagScanner(const char* uniqueId); + + /** + * Sends "tag scanned" event to the MQTT (Home Assistant). + * Based on this event HA may perform user-defined automation. + * + * @param tag Value of the scanned tag. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool tagScanned(const char* tag); + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/mocks/AUnitHelpers.h b/firmware/uniclock/lib/arduino-home-assistant/src/mocks/AUnitHelpers.h new file mode 100644 index 0000000..9b22424 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/mocks/AUnitHelpers.h @@ -0,0 +1,102 @@ +#ifndef AHA_AUNITHELPERS_H +#define AHA_AUNITHELPERS_H + +#ifdef ARDUINOHA_TEST + +#ifdef AUNITER +#include + +#if defined(__AVR__) +#include +#endif +#endif + +#define initMqttTest(testDeviceId) \ + PubSubClientMock* mock = new PubSubClientMock(); \ + HADevice device(testDeviceId); \ + HAMqtt mqtt(mock, device); \ + mqtt.setDataPrefix("testData"); \ + mqtt.begin("testHost", "testUser", "testPass"); + +#define assertNoMqttMessage() \ + assertTrue(mock->getFlushedMessagesNb() == 0); + +#define assertMqttMessage(index, eTopic, eMessage, eRetained) { \ + const __FlashStringHelper* messageP = F(eMessage); \ + size_t messageLen = strlen_P(reinterpret_cast(messageP)); \ + assertTrue(mock->getFlushedMessagesNb() > 0); \ + assertTrue(mock->getFlushedMessagesNb() > index); \ + MqttMessage* publishedMessage = mock->getFlushedMessages()[index]; \ + assertEqual(eTopic, publishedMessage->topic); \ + assertEqual(messageP, publishedMessage->buffer); \ + assertEqual(messageLen, publishedMessage->bufferSize - 1); \ + assertEqual(static_cast(eRetained), publishedMessage->retained); \ +} + +#define assertSingleMqttMessage(eTopic, eMessage, eRetained) { \ + assertEqual(1, mock->getFlushedMessagesNb()); \ + assertMqttMessage(0, eTopic, eMessage, eRetained) \ +} + +#define assertEntityConfig(mock, entity, expectedJson) \ +{ \ + mqtt.loop(); \ + assertMqttMessage(0, AHATOFSTR(ConfigTopic), expectedJson, true) \ + assertTrue(entity.getSerializer() == nullptr); \ +} + +#define assertEntityConfigOnTopic(mock, entity, topic, expectedJson) \ +{ \ + mqtt.loop(); \ + assertMqttMessage(0, topic, expectedJson, true) \ + assertTrue(entity.getSerializer() == nullptr); \ +} + +#ifdef AUNITER + +#if defined(__AVR__) +#define AHA_FREERAM mu_freeRam() +#elif defined(ESP8266) || defined(ESP32) +#define AHA_FREERAM ESP.getFreeHeap() +#else +#define AHA_FREERAM 0 +#endif + +#define AHA_LEAKTRACKSTART \ + int freeRam = AHA_FREERAM; + +#define AHA_LEAKTRACKEND \ + int diff = freeRam - AHA_FREERAM; \ + if (diff < 0) { diff *= -1; } \ + if (diff != 0) { \ + Serial.print(Test::getName().getFString()); \ + Serial.print(F(" memory leak: ")); \ + Serial.print(diff); \ + Serial.println(F("b")); \ + Test::fail(); \ + } +#else +// EpoxyDuino doesn't support memory tracking +#define AHA_LEAKTRACKSTART +#define AHA_LEAKTRACKEND +#endif + +#define AHA_TEST(suiteName, name) \ +class suiteName##_##name : public aunit::TestOnce { \ +public: \ + suiteName##_##name(); \ + void once() override; \ + void loop() override { \ + AHA_LEAKTRACKSTART \ + once(); \ + if (isNotDone()) { pass(); } \ + AHA_LEAKTRACKEND \ + } \ +} suiteName##_##name##_instance; \ +suiteName##_##name :: suiteName##_##name() { \ + init(AUNIT_F(#suiteName "_" #name)); \ +} \ +void suiteName##_##name :: once() + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/mocks/PubSubClientMock.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/mocks/PubSubClientMock.cpp new file mode 100644 index 0000000..4562eea --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/mocks/PubSubClientMock.cpp @@ -0,0 +1,271 @@ +#include "PubSubClientMock.h" +#ifdef ARDUINOHA_TEST + +#include "../ArduinoHADefines.h" + +PubSubClientMock::PubSubClientMock() : + _pendingMessage(nullptr), + _flushedMessages(nullptr), + _keepAlive(15), + _bufferSize(256), + _state(-1), + _flushedMessagesNb(0), + _subscriptions(nullptr), + _subscriptionsNb(0), + callback(nullptr) +{ + +} + +PubSubClientMock::~PubSubClientMock() +{ + if (_pendingMessage) { + delete _pendingMessage; + } + + clearFlushedMessages(); + clearSubscriptions(); +} + +bool PubSubClientMock::loop() +{ + return connected(); +} + +void PubSubClientMock::disconnect() +{ + _connection.connected = false; +} + +bool PubSubClientMock::connected() +{ + return _connection.connected; +} + +bool PubSubClientMock::connect( + const char *id, + const char *user, + const char *pass, + const char* willTopic, + uint8_t willQos, + bool willRetain, + const char* willMessage, + bool cleanSession +) +{ + (void)willQos; + (void)cleanSession; + + _connection.connected = true; + _connection.id = id; + _connection.user = user; + _connection.pass = pass; + + _lastWill.topic = willTopic; + _lastWill.message = willMessage; + _lastWill.retain = willRetain; + + return true; +} + +bool PubSubClientMock::connectDummy() +{ + _connection.connected = true; + _connection.id = "dummyId"; + _connection.user = nullptr; + _connection.pass = nullptr; + + _lastWill.topic = nullptr; + _lastWill.message = nullptr; + _lastWill.retain = false; + + return true; +} + +PubSubClientMock& PubSubClientMock::setServer(IPAddress ip, uint16_t port) +{ + _connection.ip = ip; + _connection.port = port; + + return *this; +} + +PubSubClientMock& PubSubClientMock::setServer( + const char * domain, + uint16_t port +) +{ + _connection.domain = domain; + _connection.port = port; + + return *this; +} + +PubSubClientMock& PubSubClientMock::setCallback(MQTT_CALLBACK_SIGNATURE) +{ + this->callback = callback; + return *this; +} + +bool PubSubClientMock::beginPublish( + const char* topic, + unsigned int plength, + bool retained +) +{ + if (!connected()) { + return false; + } + + if (_pendingMessage) { + delete _pendingMessage; + } + + _pendingMessage = new MqttMessage(); + _pendingMessage->retained = retained; + + { + size_t size = strlen(topic) + 1; + _pendingMessage->topic = new char[size]; + _pendingMessage->topicSize = size; + + memset(_pendingMessage->topic, 0, size); + memcpy(_pendingMessage->topic, topic, size); + } + + { + size_t size = plength + 1; + _pendingMessage->buffer = new char[size]; + _pendingMessage->bufferSize = size; + + memset(_pendingMessage->buffer, 0, size); + } + + return true; +} + +size_t PubSubClientMock::write(const uint8_t *buffer, size_t size) +{ + if (!_pendingMessage || !_pendingMessage->buffer) { + return 0; + } + + strncat(_pendingMessage->buffer, (const char*)buffer, size); + return size; +} + +size_t PubSubClientMock::print(const __FlashStringHelper* buffer) +{ + const size_t len = strlen_P(reinterpret_cast(buffer)); + char data[len + 1]; // including null terminator + strcpy_P(data, reinterpret_cast(buffer)); + + return write((const uint8_t*)(data), len); +} + +int PubSubClientMock::endPublish() +{ + if (!_pendingMessage) { + return 0; + } + + size_t messageSize = _pendingMessage->bufferSize; + uint8_t index = _flushedMessagesNb; + + _flushedMessagesNb++; + _flushedMessages = static_cast( + realloc(_flushedMessages, _flushedMessagesNb * sizeof(MqttMessage*)) + ); + + _flushedMessages[index] = _pendingMessage; // handover memory responsibility + _pendingMessage = nullptr; // do not call destructor + + return messageSize; +} + +bool PubSubClientMock::subscribe(const char* topic) +{ + uint8_t index = _subscriptionsNb; + + _subscriptionsNb++; + _subscriptions = static_cast( + realloc(_subscriptions, _subscriptionsNb * sizeof(MqttSubscription*)) + ); + + size_t topicSize = strlen(topic) + 1; + MqttSubscription* subscription = new MqttSubscription(); + subscription->topic = new char[topicSize]; + memcpy(subscription->topic, topic, topicSize); + + _subscriptions[index] = subscription; + return true; +} + +void PubSubClientMock::clearFlushedMessages() +{ + if (_flushedMessages) { + for (uint8_t i = 0; i < _flushedMessagesNb; i++) { + delete _flushedMessages[i]; + } + + delete _flushedMessages; + } + + _flushedMessagesNb = 0; +} + +void PubSubClientMock::clearSubscriptions() +{ + if (_subscriptions) { + for (uint8_t i = 0; i < _subscriptionsNb; i++) { + delete _subscriptions[i]; + } + + delete _subscriptions; + } + + _subscriptionsNb = 0; +} + +void PubSubClientMock::fakeMessage(const char* topic, const char* message) +{ + if (!callback) { + return; + } + + uint16_t len = strlen(message); + uint8_t data[len]; + memcpy(data, message, len); + + callback(const_cast(topic), data, len); +} + +void PubSubClientMock::fakeMessage( + const __FlashStringHelper* topic, + const char* message +) +{ + char topicStr[strlen_P(AHAFROMFSTR(topic)) + 1]; + topicStr[0] = 0; + strcpy_P(topicStr, AHAFROMFSTR(topic)); + + fakeMessage(topicStr, message); +} + +void PubSubClientMock::fakeMessage( + const __FlashStringHelper* topic, + const __FlashStringHelper* message +) +{ + char topicStr[strlen_P(AHAFROMFSTR(topic)) + 1]; + topicStr[0] = 0; + strcpy_P(topicStr, AHAFROMFSTR(topic)); + + char messageStr[strlen_P(AHAFROMFSTR(message)) + 1]; + messageStr[0] = 0; + strcpy_P(messageStr, AHAFROMFSTR(message)); + + fakeMessage(topicStr, messageStr); +} + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/mocks/PubSubClientMock.h b/firmware/uniclock/lib/arduino-home-assistant/src/mocks/PubSubClientMock.h new file mode 100644 index 0000000..0781ff1 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/mocks/PubSubClientMock.h @@ -0,0 +1,187 @@ +#ifndef AHA_PUBSUBCLIENTMOCK_H +#define AHA_PUBSUBCLIENTMOCK_H + +#ifdef ARDUINOHA_TEST + +#include +#include + +#if defined(ESP8266) || defined(ESP32) +#include +#define MQTT_CALLBACK_SIGNATURE std::function callback +#else +#define MQTT_CALLBACK_SIGNATURE void (*callback)(char*, uint8_t*, unsigned int) +#endif + +struct MqttMessage +{ + char* topic; + size_t topicSize; + char* buffer; + size_t bufferSize; + bool retained; + + MqttMessage() : + topic(nullptr), + topicSize(0), + buffer(nullptr), + bufferSize(0), + retained(false) + { + + } + + ~MqttMessage() + { + if (topic) { + delete topic; + } + + if (buffer) { + delete buffer; + } + } +}; + +struct MqttSubscription { + char* topic; + + MqttSubscription() : + topic(nullptr) + { + + } + + ~MqttSubscription() + { + if (topic) { + delete topic; + } + } +}; + +struct MqttConnection +{ + bool connected; + const char* domain; + IPAddress ip; + uint16_t port; + const char* id; + const char* user; + const char* pass; + + MqttConnection() : + connected(false), + domain(nullptr), + port(0), + id(nullptr), + user(nullptr), + pass(nullptr) + { + + } +}; + +struct MqttWill +{ + const char* topic; + const char* message; + bool retain; + + MqttWill() : + topic(nullptr), + message(nullptr), + retain(false) + { + + } +}; + +class PubSubClientMock +{ +public: + PubSubClientMock(); + ~PubSubClientMock(); + + bool loop(); + void disconnect(); + bool connected(); + bool connect( + const char *id, + const char *user, + const char *pass, + const char* willTopic, + uint8_t willQos, + bool willRetain, + const char* willMessage, + bool cleanSession + ); + bool connectDummy(); + PubSubClientMock& setServer(IPAddress ip, uint16_t port); + PubSubClientMock& setServer(const char* domain, uint16_t port); + PubSubClientMock& setCallback(MQTT_CALLBACK_SIGNATURE); + + bool beginPublish(const char* topic, unsigned int plength, bool retained); + size_t write(const uint8_t *buffer, size_t size); + size_t print(const __FlashStringHelper* buffer); + int endPublish(); + bool subscribe(const char* topic); + + inline void setKeepAlive(uint16_t keepAlive) + { _keepAlive = keepAlive; } + + inline uint16_t getKeepAlive() const + { return _keepAlive; } + + inline bool setBufferSize(uint16_t bufferSize) + { _bufferSize = bufferSize; return true; } + + inline uint16_t getBufferSize() const + { return _bufferSize; } + + inline void setState(int16_t state) + { _state = state; } + + inline int16_t state() const + { return _state; } + + inline uint8_t getFlushedMessagesNb() const + { return _flushedMessagesNb; } + + inline MqttMessage** getFlushedMessages() const + { return _flushedMessages; } + + inline uint8_t getSubscriptionsNb() const + { return _subscriptionsNb; } + + inline MqttSubscription** getSubscriptions() const + { return _subscriptions; } + + inline const MqttConnection& getConnection() const + { return _connection; } + + inline const MqttWill& getLastWill() const + { return _lastWill; } + + void clearFlushedMessages(); + void clearSubscriptions(); + void fakeMessage(const char* topic, const char* message); + void fakeMessage(const __FlashStringHelper* topic, const char* message); + void fakeMessage(const __FlashStringHelper* topic, const __FlashStringHelper* message); + +private: + MqttMessage* _pendingMessage; + MqttMessage** _flushedMessages; + uint16_t _keepAlive; + uint16_t _bufferSize; + int16_t _state; + uint8_t _flushedMessagesNb; + MqttSubscription** _subscriptions; + uint8_t _subscriptionsNb; + MqttConnection _connection; + MqttWill _lastWill; + MQTT_CALLBACK_SIGNATURE; +}; + +#endif +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/utils/HADictionary.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HADictionary.cpp new file mode 100644 index 0000000..266d21d --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HADictionary.cpp @@ -0,0 +1,200 @@ +#include +#include "HADictionary.h" + +// components +const char HAComponentBinarySensor[] PROGMEM = {"binary_sensor"}; +const char HAComponentButton[] PROGMEM = {"button"}; +const char HAComponentCamera[] PROGMEM = {"camera"}; +const char HAComponentCover[] PROGMEM = {"cover"}; +const char HAComponentDeviceTracker[] PROGMEM = {"device_tracker"}; +const char HAComponentDeviceAutomation[] PROGMEM = {"device_automation"}; +const char HAComponentLock[] PROGMEM = {"lock"}; +const char HAComponentNumber[] PROGMEM = {"number"}; +const char HAComponentSelect[] PROGMEM = {"select"}; +const char HAComponentSensor[] PROGMEM = {"sensor"}; +const char HAComponentSwitch[] PROGMEM = {"switch"}; +const char HAComponentTag[] PROGMEM = {"tag"}; +const char HAComponentScene[] PROGMEM = {"scene"}; +const char HAComponentFan[] PROGMEM = {"fan"}; +const char HAComponentLight[] PROGMEM = {"light"}; +const char HAComponentClimate[] PROGMEM = {"climate"}; + +// decorators +const char HASerializerSlash[] PROGMEM = {"/"}; +const char HASerializerJsonDataPrefix[] PROGMEM = {"{"}; +const char HASerializerJsonDataSuffix[] PROGMEM = {"}"}; +const char HASerializerJsonPropertyPrefix[] PROGMEM = {"\""}; +const char HASerializerJsonPropertySuffix[] PROGMEM = {"\":"}; +const char HASerializerJsonEscapeChar[] PROGMEM = {"\""}; +const char HASerializerJsonPropertiesSeparator[] PROGMEM = {","}; +const char HASerializerJsonArrayPrefix[] PROGMEM = {"["}; +const char HASerializerJsonArraySuffix[] PROGMEM = {"]"}; +const char HASerializerUnderscore[] PROGMEM = {"_"}; + +// properties +const char HADeviceIdentifiersProperty[] PROGMEM = {"ids"}; +const char HADeviceManufacturerProperty[] PROGMEM = {"mf"}; +const char HADeviceModelProperty[] PROGMEM = {"mdl"}; +const char HADeviceSoftwareVersionProperty[] PROGMEM = {"sw"}; +const char HADeviceConfigurationUrlProperty[] PROGMEM = {"cu"}; +const char HANameProperty[] PROGMEM = {"name"}; +const char HAUniqueIdProperty[] PROGMEM = {"uniq_id"}; +const char HAObjectIdProperty[] PROGMEM = {"obj_id"}; +const char HADeviceProperty[] PROGMEM = {"dev"}; +const char HADeviceClassProperty[] PROGMEM = {"dev_cla"}; +const char HAStateClassProperty[] PROGMEM = {"stat_cla"}; +const char HAIconProperty[] PROGMEM = {"ic"}; +const char HARetainProperty[] PROGMEM = {"ret"}; +const char HASourceTypeProperty[] PROGMEM = {"src_type"}; +const char HAEncodingProperty[] PROGMEM = {"e"}; +const char HAOptimisticProperty[] PROGMEM = {"opt"}; +const char HAAutomationTypeProperty[] PROGMEM = {"atype"}; +const char HATypeProperty[] PROGMEM = {"type"}; +const char HASubtypeProperty[] PROGMEM = {"stype"}; +const char HAForceUpdateProperty[] PROGMEM = {"frc_upd"}; +const char HAUnitOfMeasurementProperty[] PROGMEM = {"unit_of_meas"}; +const char HAValueTemplateProperty[] PROGMEM = {"val_tpl"}; +const char HAOptionsProperty[] PROGMEM = {"options"}; +const char HAMinProperty[] PROGMEM = {"min"}; +const char HAMaxProperty[] PROGMEM = {"max"}; +const char HAStepProperty[] PROGMEM = {"step"}; +const char HAModeProperty[] PROGMEM = {"mode"}; +const char HACommandTemplateProperty[] PROGMEM = {"cmd_tpl"}; +const char HASpeedRangeMaxProperty[] PROGMEM = {"spd_rng_max"}; +const char HASpeedRangeMinProperty[] PROGMEM = {"spd_rng_min"}; +const char HABrightnessScaleProperty[] PROGMEM = {"bri_scl"}; +const char HAMinMiredsProperty[] PROGMEM = {"min_mirs"}; +const char HAMaxMiredsProperty[] PROGMEM = {"max_mirs"}; +const char HATemperatureUnitProperty[] PROGMEM = {"temp_unit"}; +const char HAMinTempProperty[] PROGMEM = {"min_temp"}; +const char HAMaxTempProperty[] PROGMEM = {"max_temp"}; +const char HATempStepProperty[] PROGMEM = {"temp_step"}; +const char HAFanModesProperty[] PROGMEM = {"fan_modes"}; +const char HASwingModesProperty[] PROGMEM = {"swing_modes"}; +const char HAModesProperty[] PROGMEM = {"modes"}; +const char HATemperatureCommandTemplateProperty[] PROGMEM = {"temp_cmd_tpl"}; +const char HAPayloadOnProperty[] PROGMEM = {"pl_on"}; +const char HAExpireAfterProperty[] PROGMEM = {"exp_aft"}; + +// topics +const char HAConfigTopic[] PROGMEM = {"config"}; +const char HAAvailabilityTopic[] PROGMEM = {"avty_t"}; +const char HATopic[] PROGMEM = {"t"}; +const char HAStateTopic[] PROGMEM = {"stat_t"}; +const char HACommandTopic[] PROGMEM = {"cmd_t"}; +const char HAPositionTopic[] PROGMEM = {"pos_t"}; +const char HAPercentageStateTopic[] PROGMEM = {"pct_stat_t"}; +const char HAPercentageCommandTopic[] PROGMEM = {"pct_cmd_t"}; +const char HABrightnessCommandTopic[] PROGMEM = {"bri_cmd_t"}; +const char HABrightnessStateTopic[] PROGMEM = {"bri_stat_t"}; +const char HAColorTemperatureCommandTopic[] PROGMEM = {"clr_temp_cmd_t"}; +const char HAColorTemperatureStateTopic[] PROGMEM = {"clr_temp_stat_t"}; +const char HACurrentTemperatureTopic[] PROGMEM = {"curr_temp_t"}; +const char HAActionTopic[] PROGMEM = {"act_t"}; +const char HAAuxCommandTopic[] PROGMEM = {"aux_cmd_t"}; +const char HAAuxStateTopic[] PROGMEM = {"aux_stat_t"}; +const char HAPowerCommandTopic[] PROGMEM = {"pow_cmd_t"}; +const char HAFanModeCommandTopic[] PROGMEM = {"fan_mode_cmd_t"}; +const char HAFanModeStateTopic[] PROGMEM = {"fan_mode_stat_t"}; +const char HASwingModeCommandTopic[] PROGMEM = {"swing_mode_cmd_t"}; +const char HASwingModeStateTopic[] PROGMEM = {"swing_mode_stat_t"}; +const char HAModeCommandTopic[] PROGMEM = {"mode_cmd_t"}; +const char HAModeStateTopic[] PROGMEM = {"mode_stat_t"}; +const char HATemperatureCommandTopic[] PROGMEM = {"temp_cmd_t"}; +const char HATemperatureStateTopic[] PROGMEM = {"temp_stat_t"}; +const char HARGBCommandTopic[] PROGMEM = {"rgb_cmd_t"}; +const char HARGBStateTopic[] PROGMEM = {"rgb_stat_t"}; +const char HAJsonAttributesTopic[] PROGMEM = {"json_attr_t"}; + +// misc +const char HAOnline[] PROGMEM = {"online"}; +const char HAOffline[] PROGMEM = {"offline"}; +const char HAStateOn[] PROGMEM = {"ON"}; +const char HAStateOff[] PROGMEM = {"OFF"}; +const char HAStateLocked[] PROGMEM = {"LOCKED"}; +const char HAStateUnlocked[] PROGMEM = {"UNLOCKED"}; +const char HAStateNone[] PROGMEM = {"None"}; +const char HATrue[] PROGMEM = {"true"}; +const char HAFalse[] PROGMEM = {"false"}; +const char HAHome[] PROGMEM = {"home"}; +const char HANotHome[] PROGMEM = {"not_home"}; +const char HATrigger[] PROGMEM = {"trigger"}; +const char HAModeBox[] PROGMEM = {"box"}; +const char HAModeSlider[] PROGMEM = {"slider"}; + +// covers +const char HAClosedState[] PROGMEM = {"closed"}; +const char HAClosingState[] PROGMEM = {"closing"}; +const char HAOpenState[] PROGMEM = {"open"}; +const char HAOpeningState[] PROGMEM = {"opening"}; +const char HAStoppedState[] PROGMEM = {"stopped"}; + +// commands +const char HAOpenCommand[] PROGMEM = {"OPEN"}; +const char HACloseCommand[] PROGMEM = {"CLOSE"}; +const char HAStopCommand[] PROGMEM = {"STOP"}; +const char HALockCommand[] PROGMEM = {"LOCK"}; +const char HAUnlockCommand[] PROGMEM = {"UNLOCK"}; + +// device tracker +const char HAGPSType[] PROGMEM = {"gps"}; +const char HARouterType[] PROGMEM = {"router"}; +const char HABluetoothType[] PROGMEM = {"bluetooth"}; +const char HABluetoothLEType[] PROGMEM = {"bluetooth_le"}; + +// camera +const char HAEncodingBase64[] PROGMEM = {"b64"}; + +// trigger +const char HAButtonShortPressType[] PROGMEM = {"button_short_press"}; +const char HAButtonShortReleaseType[] PROGMEM = {"button_short_release"}; +const char HAButtonLongPressType[] PROGMEM = {"button_long_press"}; +const char HAButtonLongReleaseType[] PROGMEM = {"button_long_release"}; +const char HAButtonDoublePressType[] PROGMEM = {"button_double_press"}; +const char HAButtonTriplePressType[] PROGMEM = {"button_triple_press"}; +const char HAButtonQuadruplePressType[] PROGMEM = {"button_quadruple_press"}; +const char HAButtonQuintuplePressType[] PROGMEM = {"button_quintuple_press"}; +const char HATurnOnSubtype[] PROGMEM = {"turn_on"}; +const char HATurnOffSubtype[] PROGMEM = {"turn_off"}; +const char HAButton1Subtype[] PROGMEM = {"button_1"}; +const char HAButton2Subtype[] PROGMEM = {"button_2"}; +const char HAButton3Subtype[] PROGMEM = {"button_3"}; +const char HAButton4Subtype[] PROGMEM = {"button_4"}; +const char HAButton5Subtype[] PROGMEM = {"button_5"}; +const char HAButton6Subtype[] PROGMEM = {"button_6"}; + +// actions +const char HAActionOff[] PROGMEM = {"off"}; +const char HAActionHeating[] PROGMEM = {"heating"}; +const char HAActionCooling[] PROGMEM = {"cooling"}; +const char HAActionDrying[] PROGMEM = {"drying"}; +const char HAActionIdle[] PROGMEM = {"idle"}; +const char HAActionFan[] PROGMEM = {"fan"}; + +// fan modes +const char HAFanModeAuto[] PROGMEM = {"auto"}; +const char HAFanModeLow[] PROGMEM = {"low"}; +const char HAFanModeMedium[] PROGMEM = {"medium"}; +const char HAFanModeHigh[] PROGMEM = {"high"}; + +// swing modes +const char HASwingModeOn[] PROGMEM = {"on"}; +const char HASwingModeOff[] PROGMEM = {"off"}; + +// HVAC modes +const char HAModeAuto[] PROGMEM = {"auto"}; +const char HAModeOff[] PROGMEM = {"off"}; +const char HAModeCool[] PROGMEM = {"cool"}; +const char HAModeHeat[] PROGMEM = {"heat"}; +const char HAModeDry[] PROGMEM = {"dry"}; +const char HAModeFanOnly[] PROGMEM = {"fan_only"}; + +// other +const char HAHexMap[] PROGMEM = {"0123456789abcdef"}; + +// value templates +const char HAValueTemplateFloatP1[] PROGMEM = {"{{int(float(value)*10**1)}}"}; +const char HAValueTemplateFloatP2[] PROGMEM = {"{{int(float(value)*10**2)}}"}; +const char HAValueTemplateFloatP3[] PROGMEM = {"{{int(float(value)*10**3)}}"}; +const char HATemperatureUnitC[] PROGMEM = {"C"}; +const char HATemperatureUnitF[] PROGMEM = {"F"}; diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/utils/HADictionary.h b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HADictionary.h new file mode 100644 index 0000000..609f38d --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HADictionary.h @@ -0,0 +1,202 @@ +#ifndef AHA_HADICTIONARY_H +#define AHA_HADICTIONARY_H + +// components +extern const char HAComponentBinarySensor[]; +extern const char HAComponentButton[]; +extern const char HAComponentCamera[]; +extern const char HAComponentCover[]; +extern const char HAComponentDeviceTracker[]; +extern const char HAComponentDeviceAutomation[]; +extern const char HAComponentLock[]; +extern const char HAComponentNumber[]; +extern const char HAComponentSelect[]; +extern const char HAComponentSensor[]; +extern const char HAComponentSwitch[]; +extern const char HAComponentTag[]; +extern const char HAComponentScene[]; +extern const char HAComponentFan[]; +extern const char HAComponentLight[]; +extern const char HAComponentClimate[]; + +// decorators +extern const char HASerializerSlash[]; +extern const char HASerializerJsonDataPrefix[]; +extern const char HASerializerJsonDataSuffix[]; +extern const char HASerializerJsonPropertyPrefix[]; +extern const char HASerializerJsonPropertySuffix[]; +extern const char HASerializerJsonEscapeChar[]; +extern const char HASerializerJsonPropertiesSeparator[]; +extern const char HASerializerJsonArrayPrefix[]; +extern const char HASerializerJsonArraySuffix[]; +extern const char HASerializerUnderscore[]; + +// properties +extern const char HADeviceIdentifiersProperty[]; +extern const char HADeviceManufacturerProperty[]; +extern const char HADeviceModelProperty[]; +extern const char HADeviceSoftwareVersionProperty[]; +extern const char HADeviceConfigurationUrlProperty[]; +extern const char HANameProperty[]; +extern const char HAUniqueIdProperty[]; +extern const char HAObjectIdProperty[]; +extern const char HADeviceProperty[]; +extern const char HADeviceClassProperty[]; +extern const char HAStateClassProperty[]; +extern const char HAIconProperty[]; +extern const char HARetainProperty[]; +extern const char HASourceTypeProperty[]; +extern const char HAEncodingProperty[]; +extern const char HAOptimisticProperty[]; +extern const char HAAutomationTypeProperty[]; +extern const char HATypeProperty[]; +extern const char HASubtypeProperty[]; +extern const char HAForceUpdateProperty[]; +extern const char HAUnitOfMeasurementProperty[]; +extern const char HAValueTemplateProperty[]; +extern const char HAOptionsProperty[]; +extern const char HAMinProperty[]; +extern const char HAMaxProperty[]; +extern const char HAStepProperty[]; +extern const char HAModeProperty[]; +extern const char HACommandTemplateProperty[]; +extern const char HASpeedRangeMaxProperty[]; +extern const char HASpeedRangeMinProperty[]; +extern const char HABrightnessScaleProperty[]; +extern const char HAMinMiredsProperty[]; +extern const char HAMaxMiredsProperty[]; +extern const char HATemperatureUnitProperty[]; +extern const char HAMinTempProperty[]; +extern const char HAMaxTempProperty[]; +extern const char HATempStepProperty[]; +extern const char HAFanModesProperty[]; +extern const char HASwingModesProperty[]; +extern const char HAModesProperty[]; +extern const char HATemperatureCommandTemplateProperty[]; +extern const char HAPayloadOnProperty[]; +extern const char HAExpireAfterProperty[]; + +// topics +extern const char HAConfigTopic[]; +extern const char HAAvailabilityTopic[]; +extern const char HATopic[]; +extern const char HAStateTopic[]; +extern const char HACommandTopic[]; +extern const char HAPositionTopic[]; +extern const char HAPercentageStateTopic[]; +extern const char HAPercentageCommandTopic[]; +extern const char HABrightnessCommandTopic[]; +extern const char HABrightnessStateTopic[]; +extern const char HAColorTemperatureCommandTopic[]; +extern const char HAColorTemperatureStateTopic[]; +extern const char HACurrentTemperatureTopic[]; +extern const char HAActionTopic[]; +extern const char HAAuxCommandTopic[]; +extern const char HAAuxStateTopic[]; +extern const char HAPowerCommandTopic[]; +extern const char HAFanModeCommandTopic[]; +extern const char HAFanModeStateTopic[]; +extern const char HASwingModeCommandTopic[]; +extern const char HASwingModeStateTopic[]; +extern const char HAModeCommandTopic[]; +extern const char HAModeStateTopic[]; +extern const char HATemperatureCommandTopic[]; +extern const char HATemperatureStateTopic[]; +extern const char HARGBCommandTopic[]; +extern const char HARGBStateTopic[]; +extern const char HAJsonAttributesTopic[]; + +// misc +extern const char HAOnline[]; +extern const char HAOffline[]; +extern const char HAStateOn[]; +extern const char HAStateOff[]; +extern const char HAStateLocked[]; +extern const char HAStateUnlocked[]; +extern const char HAStateNone[]; +extern const char HATrue[]; +extern const char HAFalse[]; +extern const char HAHome[]; +extern const char HANotHome[]; +extern const char HATrigger[]; +extern const char HAModeBox[]; +extern const char HAModeSlider[]; + +// covers +extern const char HAClosedState[]; +extern const char HAClosingState[]; +extern const char HAOpenState[]; +extern const char HAOpeningState[]; +extern const char HAStoppedState[]; + +// commands +extern const char HAOpenCommand[]; +extern const char HACloseCommand[]; +extern const char HAStopCommand[]; +extern const char HALockCommand[]; +extern const char HAUnlockCommand[]; + +// device tracker +extern const char HAGPSType[]; +extern const char HARouterType[]; +extern const char HABluetoothType[]; +extern const char HABluetoothLEType[]; + +// camera +extern const char HAEncodingBase64[]; + +// trigger +extern const char HAButtonShortPressType[]; +extern const char HAButtonShortReleaseType[]; +extern const char HAButtonLongPressType[]; +extern const char HAButtonLongReleaseType[]; +extern const char HAButtonDoublePressType[]; +extern const char HAButtonTriplePressType[]; +extern const char HAButtonQuadruplePressType[]; +extern const char HAButtonQuintuplePressType[]; +extern const char HATurnOnSubtype[]; +extern const char HATurnOffSubtype[]; +extern const char HAButton1Subtype[]; +extern const char HAButton2Subtype[]; +extern const char HAButton3Subtype[]; +extern const char HAButton4Subtype[]; +extern const char HAButton5Subtype[]; +extern const char HAButton6Subtype[]; + +// actions +extern const char HAActionOff[]; +extern const char HAActionHeating[]; +extern const char HAActionCooling[]; +extern const char HAActionDrying[]; +extern const char HAActionIdle[]; +extern const char HAActionFan[]; + +// fan modes +extern const char HAFanModeAuto[]; +extern const char HAFanModeLow[]; +extern const char HAFanModeMedium[]; +extern const char HAFanModeHigh[]; + +// swing modes +extern const char HASwingModeOn[]; +extern const char HASwingModeOff[]; + +// HVAC modes +extern const char HAModeAuto[]; +extern const char HAModeOff[]; +extern const char HAModeCool[]; +extern const char HAModeHeat[]; +extern const char HAModeDry[]; +extern const char HAModeFanOnly[]; + +// other +extern const char HAHexMap[]; + +// value templates +extern const char HAValueTemplateFloatP1[]; +extern const char HAValueTemplateFloatP2[]; +extern const char HAValueTemplateFloatP3[]; +extern const char HATemperatureUnitC[]; +extern const char HATemperatureUnitF[]; + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/utils/HANumeric.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HANumeric.cpp new file mode 100644 index 0000000..4314663 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HANumeric.cpp @@ -0,0 +1,214 @@ +#include "HANumeric.h" + +const uint8_t HANumeric::MaxDigitsNb = 19; + +HANumeric HANumeric::fromStr(const uint8_t* buffer, const uint16_t length) +{ + if (length == 0) { + return HANumeric(); + } + + const uint8_t* firstCh = &buffer[0]; + int64_t out = 0; + bool isSigned = false; + + if (*firstCh == '-') { + isSigned = true; + firstCh++; + } + + uint8_t digitsNb = isSigned ? length - 1 : length; + if (digitsNb > MaxDigitsNb) { + return HANumeric(); + } + + uint64_t base = 1; + const uint8_t* ptr = &buffer[length - 1]; + + while (ptr >= firstCh) { + uint8_t digit = *ptr - '0'; + if (digit > 9) { + return HANumeric(); + } + + out += digit * base; + ptr--; + base *= 10; + } + + return HANumeric(isSigned ? out * -1 : out); +} + +HANumeric::HANumeric(): + _isSet(false), + _value(0), + _precision(0) +{ + +} + +HANumeric::HANumeric(const float value, const uint8_t precision): + _isSet(true), + _precision(precision) +{ + _value = value * static_cast(getPrecisionBase()); +} + +HANumeric::HANumeric(const int8_t value, const uint8_t precision): + _isSet(true), + _precision(precision) +{ + _value = value * static_cast(getPrecisionBase()); +} + +HANumeric::HANumeric(const int16_t value, const uint8_t precision): + _isSet(true), + _precision(precision) +{ + _value = value * static_cast(getPrecisionBase()); +} + +HANumeric::HANumeric(const int32_t value, const uint8_t precision): + _isSet(true), + _precision(precision) +{ + _value = value * static_cast(getPrecisionBase()); +} + +HANumeric::HANumeric(const uint8_t value, const uint8_t precision): + _isSet(true), + _precision(precision) +{ + _value = value * getPrecisionBase(); +} + +HANumeric::HANumeric(const uint16_t value, const uint8_t precision): + _isSet(true), + _precision(precision) +{ + _value = value * getPrecisionBase(); +} + +HANumeric::HANumeric(const uint32_t value, const uint8_t precision): + _isSet(true), + _precision(precision) +{ + _value = value * getPrecisionBase(); +} + +#ifdef ARDUINOHA_INT_OVERLOAD +HANumeric::HANumeric(const int value, const uint8_t precision): + _isSet(true), + _precision(precision) +{ + _value = value * static_cast(getPrecisionBase()); +} +#endif + +HANumeric::HANumeric(const int64_t value): + _isSet(true), + _value(value), + _precision(0) +{ + +} + +uint32_t HANumeric::getPrecisionBase() const +{ + // using pow() increases the flash size by ~2KB + switch (_precision) { + case 1: + return 10; + + case 2: + return 100; + + case 3: + return 1000; + + default: + return 1; + } +} + +uint8_t HANumeric::calculateSize() const +{ + if (!_isSet) { + return 0; + } + + int64_t value = _value; + const bool isSigned = value < 0; + + if (isSigned) { + value *= -1; + } + + uint8_t digitsNb = 1; + while (value > 9) { + value /= 10; + digitsNb++; + } + + if (isSigned) { + digitsNb++; // sign + } + + if (_precision > 0) { + if (value == 0) { + return 1; + } + + // one digit + dot + decimal digits (+ sign) + const uint8_t minValue = isSigned ? _precision + 3 : _precision + 2; + return digitsNb >= minValue ? digitsNb + 1 : minValue; + } + + return digitsNb; +} + +uint16_t HANumeric::toStr(char* dst) const +{ + char* prefixCh = &dst[0]; + if (!_isSet || _value == 0) { + *prefixCh = '0'; + return 1; + } + + int64_t value = _value; + const uint8_t numberLength = calculateSize(); + if (value < 0) { + value *= -1; + *prefixCh = '-'; + prefixCh++; + } + + if (_precision > 0) { + uint8_t i = _precision; + char* dotPtr = prefixCh + 1; + do { + *prefixCh = '0'; + prefixCh++; + } while(i-- > 0); + + *dotPtr = '.'; + } + + char* ch = &dst[numberLength - 1]; + char* lastCh = ch; + char* dotPos = _precision > 0 ? &dst[numberLength - 1 - _precision] : nullptr; + + while (value != 0) { + if (ch == dotPos) { + *dotPos = '.'; + ch--; + continue; + } + + *ch = (value % 10) + '0'; + value /= 10; + ch--; + } + + return lastCh - &dst[0] + 1; +} diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/utils/HANumeric.h b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HANumeric.h new file mode 100644 index 0000000..9debe2b --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HANumeric.h @@ -0,0 +1,234 @@ + +#ifndef AHA_NUMERIC_H +#define AHA_NUMERIC_H + +#include +#include "../ArduinoHADefines.h" + +/** + * This class represents a numeric value that simplifies use of different types of numbers across the library. + */ +class HANumeric +{ +public: + /// The maximum number of digits that the base value can have (int64_t). + static const uint8_t MaxDigitsNb; + + /** + * Deserializes number from the given buffer. + * Please note that the class expected buffer to contain the base number. + * For example, deserializing `1234` number and setting precision to `1` + * results in representation of `123.4` float. + * + * @param buffer The buffer that contains the number. + * @param length The length of the buffer. + */ + static HANumeric fromStr(const uint8_t* buffer, const uint16_t length); + + /** + * Creates an empty number representation. + */ + HANumeric(); + + /** + * Converts the given float into number representation of the given precision. + * If the precision is set to zero the given float will be converted into integer. + * + * @param value The value that should be used as a base. + * @param precision The number of digits in the decimal part. + */ + HANumeric(const float value, const uint8_t precision); + + /** + * Converts the given int8_t into number representation of the given precision. + * If the precision is greater than zero the given value will be converted to float. + * + * @param value The value that should be used as a base. + * @param precision The number of digits in the decimal part. + */ + HANumeric(const int8_t value, const uint8_t precision); + + /** + * Converts the given int16_t into number representation of the given precision. + * If the precision is greater than zero the given value will be converted to float. + * + * @param value The value that should be used as a base. + * @param precision The number of digits in the decimal part. + */ + HANumeric(const int16_t value, const uint8_t precision); + + /** + * Converts the given int32_t into number representation of the given precision. + * If the precision is greater than zero the given value will be converted to float. + * + * @param value The value that should be used as a base. + * @param precision The number of digits in the decimal part. + */ + HANumeric(const int32_t value, const uint8_t precision); + + /** + * Converts the given uint8_t into number representation of the given precision. + * If the precision is greater than zero the given value will be converted to float. + * + * @param value The value that should be used as a base. + * @param precision The number of digits in the decimal part. + */ + HANumeric(const uint8_t value, const uint8_t precision); + + /** + * Converts the given uint16_t into number representation of the given precision. + * If the precision is greater than zero the given value will be converted to float. + * + * @param value The value that should be used as a base. + * @param precision The number of digits in the decimal part. + */ + HANumeric(const uint16_t value, const uint8_t precision); + + /** + * Converts the given uint32_t into number representation of the given precision. + * If the precision is greater than zero the given value will be converted to float. + * + * @param value The value that should be used as a base. + * @param precision The number of digits in the decimal part. + */ + HANumeric(const uint32_t value, const uint8_t precision); + +#ifdef ARDUINOHA_INT_OVERLOAD + /** + * Converts the given int into number representation of the given precision. + * If the precision is greater than zero the given value will be converted to float. + * + * @param value The value that should be used as a base. + * @param precision The number of digits in the decimal part. + */ + HANumeric(const int value, const uint8_t precision); +#endif + + void operator= (const HANumeric& a) { + if (!a.isSet()) { + reset(); + } else { + _isSet = a.isSet(); + _value = a.getBaseValue(); + _precision = a.getPrecision(); + } + } + + bool operator== (const HANumeric& a) const { + return ( + isSet() == a.isSet() && + getBaseValue() == a.getBaseValue() && + getPrecision() == a.getPrecision() + ); + } + + /** + * Returns multiplier that used to generate base value based on the precision. + * The multiplier is generated using the formula: `pow(precision, 10)`. + */ + uint32_t getPrecisionBase() const; + + /** + * Returns size of the number + */ + uint8_t calculateSize() const; + + /** + * Converts the number to the string. + * + * @param dst Destination where the number will be saved. + * The null terminator is not added at the end. + * @return The number of written characters. + * @note The `dst` size should be calculated using HANumeric::calculateSize() method plus 1 extra byte for the null terminator. + */ + uint16_t toStr(char* dst) const; + + /** + * Returns true if the base value is set. + */ + inline bool isSet() const + { return _isSet; } + + /** + * Sets the base value without converting it to the proper precision. + */ + inline void setBaseValue(int64_t value) + { _isSet = true; _value = value; } + + /** + * Returns the base value of the number. + */ + inline int64_t getBaseValue() const + { return _value; } + + /** + * Sets the precision of the number (number of digits in the decimal part). + * + * @param precision The precision to use. + */ + inline void setPrecision(const uint8_t precision) + { _precision = precision; } + + /** + * Returns the precision of the number. + */ + inline uint8_t getPrecision() const + { return _precision; } + + /** + * Resets the number to the defaults. + */ + inline void reset() + { _isSet = false; _value = 0; _precision = 0; } + + inline bool isUInt8() const + { return _isSet && _precision == 0 && _value >= 0 && _value <= UINT8_MAX; } + + inline bool isUInt16() const + { return _isSet && _precision == 0 && _value >= 0 && _value <= UINT16_MAX; } + + inline bool isUInt32() const + { return _isSet && _precision == 0 && _value >= 0 && _value <= UINT32_MAX; } + + inline bool isInt8() const + { return _isSet && _precision == 0 && _value >= INT8_MIN && _value <= INT8_MAX; } + + inline bool isInt16() const + { return _isSet && _precision == 0 && _value >= INT16_MIN && _value <= INT16_MAX; } + + inline bool isInt32() const + { return _isSet && _precision == 0 && _value >= INT32_MIN && _value <= INT32_MAX; } + + inline bool isFloat() const + { return _isSet && _precision > 0; } + + inline uint8_t toUInt8() const + { return static_cast(_value); } + + inline uint16_t toUInt16() const + { return static_cast(_value); } + + inline uint32_t toUInt32() const + { return static_cast(_value); } + + inline int8_t toInt8() const + { return static_cast(_value); } + + inline int16_t toInt16() const + { return static_cast(_value); } + + inline int32_t toInt32() const + { return static_cast(_value); } + + inline float toFloat() const + { return _value / (float)getPrecisionBase(); } + +private: + bool _isSet; + int64_t _value; + uint8_t _precision; + + explicit HANumeric(const int64_t value); +}; + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializer.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializer.cpp new file mode 100644 index 0000000..832426d --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializer.cpp @@ -0,0 +1,563 @@ + +#include + +#ifdef ARDUINO_ARCH_SAMD +#include +#endif + +#include "HASerializer.h" +#include "../ArduinoHADefines.h" +#include "../HADevice.h" +#include "../HAMqtt.h" +#include "../utils/HAUtils.h" +#include "../utils/HANumeric.h" +#include "../device-types/HABaseDeviceType.h" + +uint16_t HASerializer::calculateConfigTopicLength( + const __FlashStringHelper* componentName, + const char* objectId +) +{ + const HAMqtt* mqtt = HAMqtt::instance(); + if ( + !componentName || + !objectId || + !mqtt || + !mqtt->getDiscoveryPrefix() || + !mqtt->getDevice() + ) { + return 0; + } + + return + strlen(mqtt->getDiscoveryPrefix()) + 1 + // prefix with slash + strlen_P(AHAFROMFSTR(componentName)) + 1 + // component name with slash + strlen(mqtt->getDevice()->getUniqueId()) + 1 + // device ID with slash + strlen(objectId) + 1 + // object ID with slash + strlen_P(HAConfigTopic) + 1; // including null terminator +} + +bool HASerializer::generateConfigTopic( + char* output, + const __FlashStringHelper* componentName, + const char* objectId +) +{ + const HAMqtt* mqtt = HAMqtt::instance(); + if ( + !output || + !componentName || + !objectId || + !mqtt || + !mqtt->getDiscoveryPrefix() || + !mqtt->getDevice() + ) { + return false; + } + + strcpy(output, mqtt->getDiscoveryPrefix()); + strcat_P(output, HASerializerSlash); + + strcat_P(output, AHAFROMFSTR(componentName)); + strcat_P(output, HASerializerSlash); + + strcat(output, mqtt->getDevice()->getUniqueId()); + strcat_P(output, HASerializerSlash); + + strcat(output, objectId); + strcat_P(output, HASerializerSlash); + + strcat_P(output, HAConfigTopic); + return true; +} + +uint16_t HASerializer::calculateDataTopicLength( + const char* objectId, + const __FlashStringHelper* topic +) +{ + const HAMqtt* mqtt = HAMqtt::instance(); + if ( + !topic || + !mqtt || + !mqtt->getDataPrefix() || + !mqtt->getDevice() + ) { + return 0; + } + + uint16_t size = + strlen(mqtt->getDataPrefix()) + 1 + // prefix with slash + strlen(mqtt->getDevice()->getUniqueId()) + 1 + // device ID with slash + strlen_P(AHAFROMFSTR(topic)); + + if (objectId) { + size += strlen(objectId) + 1; // object ID with slash; + } + + return size + 1; // including null terminator +} + +bool HASerializer::generateDataTopic( + char* output, + const char* objectId, + const __FlashStringHelper* topic +) +{ + const HAMqtt* mqtt = HAMqtt::instance(); + if ( + !output || + !topic || + !mqtt || + !mqtt->getDataPrefix() || + !mqtt->getDevice() + ) { + return false; + } + + strcpy(output, mqtt->getDataPrefix()); + strcat_P(output, HASerializerSlash); + + strcat(output, mqtt->getDevice()->getUniqueId()); + strcat_P(output, HASerializerSlash); + + if (objectId) { + strcat(output, objectId); + strcat_P(output, HASerializerSlash); + } + + strcat_P(output, AHAFROMFSTR(topic)); + return true; +} + +bool HASerializer::compareDataTopics( + const char* actualTopic, + const char* objectId, + const __FlashStringHelper* topic +) +{ + if (!actualTopic) { + return false; + } + + const uint16_t topicLength = calculateDataTopicLength(objectId, topic); + if (topicLength == 0) { + return false; + } + + char expectedTopic[topicLength]; + if (!generateDataTopic(expectedTopic, objectId, topic)) { + return false; + } + + return memcmp(actualTopic, expectedTopic, topicLength) == 0; +} + +HASerializer::HASerializer( + HABaseDeviceType* deviceType, + const uint8_t maxEntriesNb +) : + _deviceType(deviceType), + _entriesNb(0), + _maxEntriesNb(maxEntriesNb), + _entries(new SerializerEntry[maxEntriesNb]) +{ + +} + +HASerializer::~HASerializer() +{ + delete[] _entries; +} + +void HASerializer::set( + const __FlashStringHelper* property, + const void* value, + PropertyValueType valueType +) +{ + if (!property || !value) { + return; + } + + SerializerEntry* entry = addEntry(); + entry->type = PropertyEntryType; + entry->subtype = static_cast(valueType); + entry->property = property; + entry->value = value; +} + +void HASerializer::set(const FlagType flag) +{ + if (flag == WithDevice || flag == WithUniqueId) { + SerializerEntry* entry = addEntry(); + entry->type = FlagEntryType; + entry->subtype = static_cast(flag); + entry->property = nullptr; + entry->value = nullptr; + } else if (flag == WithAvailability) { + HAMqtt* mqtt = HAMqtt::instance(); + const bool isSharedAvailability = mqtt->getDevice()->isSharedAvailabilityEnabled(); + const bool isAvailabilityConfigured = _deviceType->isAvailabilityConfigured(); + + if (!isSharedAvailability && !isAvailabilityConfigured) { + return; // not configured + } + + SerializerEntry* entry = addEntry(); + entry->type = TopicEntryType; + entry->property = AHATOFSTR(HAAvailabilityTopic); + entry->value = isSharedAvailability + ? mqtt->getDevice()->getAvailabilityTopic() + : nullptr; + } +} + +void HASerializer::topic(const __FlashStringHelper* topic) +{ + if (!_deviceType || !topic) { + return; + } + + SerializerEntry* entry = addEntry(); + entry->type = TopicEntryType; + entry->property = topic; +} + +HASerializer::SerializerEntry* HASerializer::addEntry() +{ + return &_entries[_entriesNb++]; // intentional lack of protection against overflow +} + +uint16_t HASerializer::calculateSize() const +{ + uint16_t size = + strlen_P(HASerializerJsonDataPrefix) + + strlen_P(HASerializerJsonDataSuffix); + + for (uint8_t i = 0; i < _entriesNb; i++) { + const uint16_t entrySize = calculateEntrySize(&_entries[i]); + if (entrySize == 0) { + continue; + } + + size += entrySize; + + // items separator + if (i > 0) { + size += strlen_P(HASerializerJsonPropertiesSeparator); + } + } + + return size; +} + +bool HASerializer::flush() const +{ + HAMqtt* mqtt = HAMqtt::instance(); + if (!mqtt || (_deviceType && !mqtt->getDevice())) { + return false; + } + + mqtt->writePayload(AHATOFSTR(HASerializerJsonDataPrefix)); + + for (uint8_t i = 0; i < _entriesNb; i++) { + if (i > 0) { + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertiesSeparator)); + } + + if (!flushEntry(&_entries[i])) { + return false; + } + } + + mqtt->writePayload(AHATOFSTR(HASerializerJsonDataSuffix)); + return true; +} + +uint16_t HASerializer::calculateEntrySize(const SerializerEntry* entry) const +{ + switch (entry->type) { + case PropertyEntryType: + return + // property name + strlen_P(HASerializerJsonPropertyPrefix) + + strlen_P(AHAFROMFSTR(entry->property)) + + strlen_P(HASerializerJsonPropertySuffix) + + // property value + calculatePropertyValueSize(entry); + + case TopicEntryType: + return calculateTopicEntrySize(entry); + + case FlagEntryType: + return calculateFlagSize( + static_cast(entry->subtype) + ); + + default: + return 0; + } +} + +uint16_t HASerializer::calculateTopicEntrySize( + const SerializerEntry* entry +) const +{ + uint16_t size = 0; + + // property name + size += + strlen_P(HASerializerJsonPropertyPrefix) + + strlen_P(AHAFROMFSTR(entry->property)) + + strlen_P(HASerializerJsonPropertySuffix); + + // topic escape + size += 2 * strlen_P(HASerializerJsonEscapeChar); + + // topic + if (entry->value) { + size += strlen(static_cast(entry->value)); + } else { + if (!_deviceType) { + return 0; + } + + size += calculateDataTopicLength( + _deviceType->uniqueId(), + entry->property + ) - 1; // exclude null terminator + } + + return size; +} + +uint16_t HASerializer::calculateFlagSize(const FlagType flag) const +{ + const HAMqtt* mqtt = HAMqtt::instance(); + const HADevice* device = mqtt->getDevice(); + + if (flag == WithDevice && device->getSerializer()) { + const uint16_t deviceLength = device->getSerializer()->calculateSize(); + if (deviceLength == 0) { + return 0; + } + + return + strlen_P(HASerializerJsonPropertyPrefix) + + strlen_P(HADeviceProperty) + + strlen_P(HASerializerJsonPropertySuffix) + + deviceLength; + } else if (flag == WithUniqueId && _deviceType) { + uint16_t uniqueIdLength = strlen(_deviceType->uniqueId()); + + if (device->isExtendedUniqueIdsEnabled()) { + uniqueIdLength += strlen(device->getUniqueId()) + 1; // with separator + } + + return + // property name + strlen_P(HASerializerJsonPropertyPrefix) + + strlen_P(HAUniqueIdProperty) + + strlen_P(HASerializerJsonPropertySuffix) + + // property value + 2 * strlen_P(HASerializerJsonEscapeChar) + + uniqueIdLength; + } + + return 0; +} + +uint16_t HASerializer::calculatePropertyValueSize( + const SerializerEntry* entry +) const +{ + switch (entry->subtype) { + case ConstCharPropertyValue: + case ProgmemPropertyValue: { + const char* value = static_cast(entry->value); + const uint16_t len = + entry->subtype == ConstCharPropertyValue ? strlen(value) : strlen_P(value); + return 2 * strlen_P(HASerializerJsonEscapeChar) + len; + } + + case BoolPropertyType: { + const bool value = *static_cast(entry->value); + return value ? strlen_P(HATrue) : strlen_P(HAFalse); + } + + case NumberPropertyType: { + const HANumeric* value = static_cast( + entry->value + ); + return value->calculateSize(); + } + + case ArrayPropertyType: { + const HASerializerArray* array = static_cast( + entry->value + ); + return array->calculateSize(); + } + + default: + return 0; + } +} + +bool HASerializer::flushEntry(const SerializerEntry* entry) const +{ + HAMqtt* mqtt = HAMqtt::instance(); + + switch (entry->type) { + case PropertyEntryType: { + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix)); + mqtt->writePayload(entry->property); + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertySuffix)); + + return flushEntryValue(entry); + } + + case TopicEntryType: + return flushTopic(entry); + + case FlagEntryType: + return flushFlag(entry); + + default: + return true; + } +} + +bool HASerializer::flushEntryValue(const SerializerEntry* entry) const +{ + HAMqtt* mqtt = HAMqtt::instance(); + + switch (entry->subtype) { + case ConstCharPropertyValue: + case ProgmemPropertyValue: { + const char* value = static_cast(entry->value); + mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar)); + + if (entry->subtype == ConstCharPropertyValue) { + mqtt->writePayload(value, strlen(value)); + } else { + mqtt->writePayload(AHATOFSTR(value)); + } + + mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar)); + return true; + } + + case BoolPropertyType: { + const bool value = *static_cast(entry->value); + mqtt->writePayload(AHATOFSTR(value ? HATrue : HAFalse)); + return true; + } + + case NumberPropertyType: { + const HANumeric* value = static_cast( + entry->value + ); + + char tmp[HANumeric::MaxDigitsNb + 1]; + const uint16_t length = value->toStr(tmp); + + mqtt->writePayload(tmp, length); + return true; + } + + case ArrayPropertyType: { + const HASerializerArray* array = static_cast( + entry->value + ); + const uint16_t size = array->calculateSize(); + char tmp[size + 1]; // including null terminator + tmp[0] = 0; + array->serialize(tmp); + mqtt->writePayload(tmp, size); + + return true; + } + + default: + return false; + } +} + +bool HASerializer::flushTopic(const SerializerEntry* entry) const +{ + HAMqtt* mqtt = HAMqtt::instance(); + + // property name + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix)); + mqtt->writePayload(entry->property); + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertySuffix)); + + // value (escaped) + mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar)); + + if (entry->value) { + const char* topic = static_cast(entry->value); + mqtt->writePayload(topic, strlen(topic)); + } else { + const uint16_t length = calculateDataTopicLength( + _deviceType->uniqueId(), + entry->property + ); + if (length == 0) { + return false; + } + + char topic[length]; + generateDataTopic( + topic, + _deviceType->uniqueId(), + entry->property + ); + + mqtt->writePayload(topic, length - 1); + } + + mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar)); + return true; +} + +bool HASerializer::flushFlag(const SerializerEntry* entry) const +{ + HAMqtt* mqtt = HAMqtt::instance(); + const HADevice* device = mqtt->getDevice(); + const FlagType flag = static_cast(entry->subtype); + + if (flag == WithDevice && device) { + // property name + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix)); + mqtt->writePayload(AHATOFSTR(HADeviceProperty)); + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertySuffix)); + + // property value + return device->getSerializer()->flush(); + } else if (flag == WithUniqueId && _deviceType) { + // property name + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix)); + mqtt->writePayload(AHATOFSTR(HAUniqueIdProperty)); + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertySuffix)); + + // value + const char* uniqueId = _deviceType->uniqueId(); + mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar)); + + if (device->isExtendedUniqueIdsEnabled()) { + const char* deviceUniqueId = device->getUniqueId(); + mqtt->writePayload(deviceUniqueId, strlen(deviceUniqueId)); + mqtt->writePayload(AHATOFSTR(HASerializerUnderscore)); + } + + mqtt->writePayload(uniqueId, strlen(uniqueId)); + mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar)); + + return true; + } + + return false; +} \ No newline at end of file diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializer.h b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializer.h new file mode 100644 index 0000000..ea5a85e --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializer.h @@ -0,0 +1,270 @@ + +#ifndef AHA_SERIALIZER_H +#define AHA_SERIALIZER_H + +#include + +#include "HADictionary.h" +#include "HASerializerArray.h" + +class HAMqtt; +class HABaseDeviceType; + +/** + * This class allows to create JSON objects easily. + * Its main purpose is to handle configuration of a device type that's going to + * be published to the MQTT broker. + */ +class HASerializer +{ +public: + /// Type of the object's entry. + enum EntryType { + UnknownEntryType = 0, + PropertyEntryType, + TopicEntryType, + FlagEntryType + }; + + /// The type of a flag for a FlagEntryType. + enum FlagType { + WithDevice = 1, + WithAvailability, + WithUniqueId + }; + + /// Available data types of entries. + enum PropertyValueType { + UnknownPropertyValueType = 0, + ConstCharPropertyValue, + ProgmemPropertyValue, + BoolPropertyType, + NumberPropertyType, + ArrayPropertyType + }; + + /// Representation of a single entry in the object. + struct SerializerEntry { + /// Type of the entry. + EntryType type; + + /// Subtype of the entry. It can be `FlagType`, `PropertyValueType` or `TopicType`. + uint8_t subtype; + + /// Pointer to the property name (progmem string). + const __FlashStringHelper* property; + + /// Pointer to the property value. The value type is determined by `subtype`. + const void* value; + + SerializerEntry(): + type(UnknownEntryType), + subtype(0), + property(nullptr), + value(nullptr) + { } + }; + + /** + * Calculates the size of a configuration topic for the given component and object ID. + * The configuration topic has structure as follows: `[discovery prefix]/[component]/[device ID]_[objectId]/config` + * + * @param component The name of the HA component (e.g. `binary_sensor`). + * @param objectId The unique ID of a device type that's going to publish the config. + */ + static uint16_t calculateConfigTopicLength( + const __FlashStringHelper* component, + const char* objectId + ); + + /** + * Generates the configuration topic for the given component and object ID. + * The topic will be stored in the `output` variable. + * + * @param output Buffer where the topic will be written. + * @param component The name of the HA component (e.g. `binary_sensor`). + * @param objectId The unique ID of a device type that's going to publish the config. + */ + static bool generateConfigTopic( + char* output, + const __FlashStringHelper* component, + const char* objectId + ); + + /** + * Calculates the size of the given data topic for the given objectId. + * The data topic has structure as follows: `[data prefix]/[device ID]_[objectId]/[topic]` + * + * @param objectId The unique ID of a device type that's going to publish the data. + * @param topic The topic name (progmem string). + */ + static uint16_t calculateDataTopicLength( + const char* objectId, + const __FlashStringHelper* topic + ); + + /** + * Generates the data topic for the given object ID. + * The topic will be stored in the `output` variable. + * + * @param output Buffer where the topic will be written. + * @param objectId The unique ID of a device type that's going to publish the data. + * @param topic The topic name (progmem string). + */ + static bool generateDataTopic( + char* output, + const char* objectId, + const __FlashStringHelper* topic + ); + + /** + * Checks whether the given topic matches the data topic that can be generated + * using the given objectId and topicP. + * This method can be used to check if the received message matches some data topic. + * + * @param actualTopic The actual topic to compare. + * @param objectId The unique ID of a device type that may be the owner of the topic. + * @param topic The topic name (progmem string). + */ + static bool compareDataTopics( + const char* actualTopic, + const char* objectId, + const __FlashStringHelper* topic + ); + + /** + * Creates instance of the serializer for the given device type. + * Please note that the number JSON object's entries needs to be known upfront. + * This approach reduces number of memory allocations. + * + * @param deviceType The device type that owns the serializer. + * @param maxEntriesNb Maximum number of the output object entries. + */ + HASerializer(HABaseDeviceType* deviceType, const uint8_t maxEntriesNb); + + /** + * Frees the dynamic memory allocated by the class. + */ + ~HASerializer(); + + /** + * Returns the number of items that were added to the serializer. + */ + inline uint8_t getEntriesNb() const + { return _entriesNb; } + + /** + * Returns pointer to the serializer's entries. + */ + inline SerializerEntry* getEntries() const + { return _entries; } + + /** + * Adds a new entry to the serialized with a type of `PropertyEntryType`. + * + * @param property Pointer to the name of the property (progmem string). + * @param value Pointer to the value that's being set. + * @param valueType The type of the value that's passed to the method. + */ + void set( + const __FlashStringHelper* property, + const void* value, + PropertyValueType valueType = ConstCharPropertyValue + ); + + /** + * Adds a new entry to the serializer with a type of `FlagEntryType`. + * + * @param flag Flag to add. + */ + void set(const FlagType flag); + + /** + * Adds a new entry to the serialize with a type of `TopicEntryType`. + * + * @param topic The topic name to add (progmem string). + */ + void topic(const __FlashStringHelper* topic); + + /** + * Calculates the output size of the serialized JSON object. + */ + uint16_t calculateSize() const; + + /** + * Flushes the JSON object to the MQTT stream. + * Please note that this method only writes the MQTT payload. + * The MQTT session needs to be opened before. + */ + bool flush() const; + +private: + /// Pointer to the device type that owns the serializer. + HABaseDeviceType* _deviceType; + + /// The number of entries added to the serializer. + uint8_t _entriesNb; + + /// Maximum number of entries that can be added to the serializer. + uint8_t _maxEntriesNb; + + /// Pointer to the serializer entries. + SerializerEntry* _entries; + + /** + * Creates a new entry in the serializer's memory. + * If the limit of entries is hit, the nullptr is returned. + */ + SerializerEntry* addEntry(); + + /** + * Calculates the serialized size of the given entry. + * Internally, this method recognizes the type of the entry and calls + * a proper calculate method listed below. + */ + uint16_t calculateEntrySize(const SerializerEntry* entry) const; + + /** + * Calculates the size of the entry of type `TopicEntryType`. + */ + uint16_t calculateTopicEntrySize(const SerializerEntry* entry) const; + + /** + * Calculates the size of the entry of type `FlagEntryType`. + */ + uint16_t calculateFlagSize(const FlagType flag) const; + + /** + * Calculates the size of the entry's value if the entry is `PropertyEntryType`. + */ + uint16_t calculatePropertyValueSize(const SerializerEntry* entry) const; + + /** + * Calculates the size of the array if the property's value is a type of `ArrayPropertyType`. + */ + uint16_t calculateArraySize(const HASerializerArray* array) const; + + /** + * Flushes the given entry to the MQTT. + * Internally this method recognizes the type of the entry and calls + * a proper flush method listed below. + */ + bool flushEntry(const SerializerEntry* entry) const; + + /** + * Flushes the value of the `PropertyEntryType` entry. + */ + bool flushEntryValue(const SerializerEntry* entry) const; + + /** + * Flushes the entry of type `TopicEntryType` to the MQTT. + */ + bool flushTopic(const SerializerEntry* entry) const; + + /** + * Flushes the entry of type `FlagEntryType` to the MQTT. + */ + bool flushFlag(const SerializerEntry* entry) const; +}; + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializerArray.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializerArray.cpp new file mode 100644 index 0000000..20446f0 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializerArray.cpp @@ -0,0 +1,93 @@ + +#include + +#include "HASerializerArray.h" +#include "HADictionary.h" + +HASerializerArray::HASerializerArray(const uint8_t size, const bool progmemItems) : + _progmemItems(progmemItems), + _size(size), + _itemsNb(0), + _items(new ItemType[size]) +{ + +} + +HASerializerArray::~HASerializerArray() +{ + delete[] _items; +} + +bool HASerializerArray::add(ItemType item) +{ + if (_itemsNb >= _size) { + return false; + } + + _items[_itemsNb++] = item; + return true; +} + +const char* HASerializerArray::getItem(const uint8_t index) const +{ + if (index >= _itemsNb) { + return nullptr; + } + + return _items[index]; +} + +uint16_t HASerializerArray::calculateSize() const +{ + uint16_t size = + strlen_P(HASerializerJsonArrayPrefix) + + strlen_P(HASerializerJsonArraySuffix); + + if (_itemsNb == 0) { + return size; + } + + // separators between elements + size += (_itemsNb - 1) * strlen_P(HASerializerJsonPropertiesSeparator); + + for (uint8_t i = 0; i < _itemsNb; i++) { + size += + 2 * strlen_P(HASerializerJsonEscapeChar) + + (_progmemItems ? strlen_P(_items[i]) : strlen(_items[i])); + } + + return size; +} + +bool HASerializerArray::serialize(char* output) const +{ + if (!output) { + return false; + } + + strcat_P(output, HASerializerJsonArrayPrefix); + + for (uint8_t i = 0; i < _itemsNb; i++) { + if (i > 0) { + strcat_P(output, HASerializerJsonPropertiesSeparator); + } + + strcat_P(output, HASerializerJsonEscapeChar); + + if (_progmemItems) { + strcat_P(output, _items[i]); + } else { + strcat(output, _items[i]); + } + + strcat_P(output, HASerializerJsonEscapeChar); + } + + strcat_P(output, HASerializerJsonArraySuffix); + return true; +} + +void HASerializerArray::clear() +{ + _itemsNb = 0; +} diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializerArray.h b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializerArray.h new file mode 100644 index 0000000..b09cde9 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HASerializerArray.h @@ -0,0 +1,81 @@ + +#ifndef AHA_SERIALIZERARRAY_H +#define AHA_SERIALIZERARRAY_H + +#include + +/** + * HASerializerArray represents array of items that can be used as a HASerializer property. + */ +class HASerializerArray +{ +public: + typedef const char* ItemType; + + /** + * Constructs HASerializerArray with the static size (number of elements). + * The array is allocated dynamically in the memory based on the given size. + * + * @param size The desired number of elements that will be stored in the array. + * @param progmemItems Specifies whether items are going to be stored in the flash memory. + */ + HASerializerArray(const uint8_t size, const bool progmemItems = true); + ~HASerializerArray(); + + /** + * Returns the number of elements that were added to the array. + * It can be lower than size of the array. + */ + inline uint8_t getItemsNb() const + { return _itemsNb; } + + /** + * Returns pointer to the array. + */ + inline ItemType* getItems() const + { return _items; } + + /** + * Adds a new element to the array. + * + * @param itemP Item to add (string). + * @returns Returns `true` if item has been added to the array successfully. + */ + bool add(ItemType item); + + /** + * Returns a pointer to the item at the given index. + * If the element doesn't exist, null is returned. + */ + const char* getItem(const uint8_t index) const; + + /** + * Calculates the size of the serialized array (JSON representation). + */ + uint16_t calculateSize() const; + + /** + * Serializes array as JSON to the given output. + */ + bool serialize(char* output) const; + + /** + * Clears the array. + */ + void clear(); + +private: + /// Specifies whether items are stored in the flash memory. + const bool _progmemItems; + + /// The maximum size of the array. + const uint8_t _size; + + /// The number of items that were added to the array. + uint8_t _itemsNb; + + /// Pointer to the array elements. + ItemType* _items; +}; + +#endif diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/utils/HAUtils.cpp b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HAUtils.cpp new file mode 100644 index 0000000..a21eefc --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HAUtils.cpp @@ -0,0 +1,48 @@ +#ifdef ARDUINO_ARCH_SAMD +#include +#endif + +#include +#include "HAUtils.h" +#include "HADictionary.h" + +bool HAUtils::endsWith(const char* str, const char* suffix) +{ + if (str == nullptr || suffix == nullptr) { + return false; + } + + const uint16_t lenstr = strlen(str); + const uint16_t lensuffix = strlen(suffix); + + if (lensuffix > lenstr || lenstr == 0 || lensuffix == 0) { + return false; + } + + return (strncmp(str + lenstr - lensuffix, suffix, lensuffix) == 0); +} + +void HAUtils::byteArrayToStr( + char* dst, + const byte* src, + const uint16_t length +) +{ + for (uint8_t i = 0; i < length; i++) { + dst[i*2] = pgm_read_byte(&HAHexMap[((char)src[i] & 0XF0) >> 4]); + dst[i*2+1] = pgm_read_byte(&HAHexMap[((char)src[i] & 0x0F)]); + } + + dst[length * 2] = 0; +} + +char* HAUtils::byteArrayToStr( + const byte* src, + const uint16_t length +) +{ + char* dst = new char[(length * 2) + 1]; // include null terminator + byteArrayToStr(dst, src, length); + + return dst; +} diff --git a/firmware/uniclock/lib/arduino-home-assistant/src/utils/HAUtils.h b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HAUtils.h new file mode 100644 index 0000000..630b204 --- /dev/null +++ b/firmware/uniclock/lib/arduino-home-assistant/src/utils/HAUtils.h @@ -0,0 +1,52 @@ +#ifndef AHA_HAUTILS_H +#define AHA_HAUTILS_H + +#include + +/** + * This class provides some useful methods to make life easier. + */ +class HAUtils +{ +public: + /** + * Checks whether the given `str` ends with the given `suffix`. + * + * @param str Input string to check. + * @param suffix Suffix to find + * @returns True if the given suffix is present at the end of the given string. + */ + static bool endsWith( + const char* str, + const char* suffix + ); + + /** + * Converts the given byte array into hex string. + * Each byte will be represented by two bytes, so the output size will be `length * 2` + * + * @param dst Destination where the string will be saved. + * @param src Bytes array to convert. + * @param length Length of the bytes array. + */ + static void byteArrayToStr( + char* dst, + const byte* src, + const uint16_t length + ); + + /** + * Converts the given byte array into hex string. + * This method allocates a new memory. + * + * @param src Bytes array to convert. + * @param length Length of the bytes array. + * @returns Newly allocated string containing the hex representation. + */ + static char* byteArrayToStr( + const byte* src, + const uint16_t length + ); +}; + +#endif