├── BlindsServo.cpp ├── BlindsServo.h ├── README.md ├── blinds2mqtt.h └── blinds2mqtt.ino /BlindsServo.cpp: -------------------------------------------------------------------------------- 1 | #include "BlindsServo.h" 2 | 3 | BlindsServo::BlindsServo(int id, int servoPin, int minValue, int maxValue, int maxDegree, boolean reversed, boolean debug) { 4 | init(id, servoPin, minValue, maxValue, maxDegree, reversed, debug); 5 | } 6 | 7 | BlindsServo::BlindsServo() { 8 | 9 | } 10 | 11 | BlindsServo::~BlindsServo() { 12 | servo.detach(); 13 | } 14 | 15 | void BlindsServo::init(int id, int servoPin, int minPulseValue, int maxPulseValue, int maxDegree, boolean reversed, boolean debug) { 16 | this->id = id; 17 | this->servoPin = servoPin; 18 | this->servoMinPulse = minPulseValue; 19 | this->servoMaxPulse = maxPulseValue; 20 | this->servoMaxDegree = maxDegree; 21 | this->isDebug = debug; 22 | this->isReversed = reversed; 23 | attached = false; 24 | } 25 | 26 | void BlindsServo::attach() { 27 | servo.attach(servoPin, servoMinPulse, servoMaxPulse); 28 | attached = true; 29 | } 30 | 31 | void BlindsServo::detach() { 32 | servo.detach(); 33 | attached = false; 34 | } 35 | 36 | void BlindsServo::goToAngle(int angle) { 37 | debugPrint("Go to angle: " + String(angle)); 38 | 39 | // Ensure limits 40 | if(angle > servoMaxDegree || angle < 0) { 41 | return; 42 | } 43 | 44 | if (isMoving()) { 45 | setStop(); // Stop 46 | } 47 | 48 | target = angle; 49 | } 50 | 51 | 52 | void BlindsServo::setStop() { 53 | // Cancel current move 54 | debugPrint("Stop moving!"); 55 | target = currentPosition; 56 | 57 | statusChangedCallback(id); 58 | } 59 | 60 | void BlindsServo::setOpen() { 61 | goToAngle(servoMaxDegree); 62 | } 63 | 64 | void BlindsServo::setClose() { 65 | goToAngle(0); 66 | } 67 | 68 | void BlindsServo::goToPosition(int position) { 69 | if(position >= 0 && position <= 100) { 70 | float angle = (float) ((float) position / (float) 100) * (float) 270; 71 | int res = (int)round(angle); 72 | goToAngle(res); 73 | } 74 | } 75 | 76 | void BlindsServo::loop() { 77 | // Do the main loop 78 | if (currentPosition != target && attached == false) { 79 | attach(); 80 | } 81 | 82 | // Before move, we take the prev position 83 | previousPosition = currentPosition; 84 | blindsStatus currentStatus = getStatus(); 85 | 86 | if(currentPosition < target){ 87 | servo.writeMicroseconds(angleToServo(currentPosition++)); 88 | if (previousStatus != currentStatus) { 89 | statusChangedCallback(id); 90 | } 91 | positionChangedCallback(id); 92 | } 93 | else if(currentPosition > target){ 94 | servo.writeMicroseconds(angleToServo(currentPosition--)); 95 | if (previousStatus != currentStatus) { 96 | statusChangedCallback(id); 97 | } 98 | positionChangedCallback(id); 99 | } 100 | 101 | // Notify that we have reached the target 102 | if (currentPosition == target && previousPosition != currentPosition) { 103 | detach(); 104 | statusChangedCallback(id); 105 | positionChangedCallback(id); 106 | } 107 | 108 | previousStatus = currentStatus; 109 | } 110 | 111 | boolean BlindsServo::isOpening() { 112 | return target > currentPosition; 113 | } 114 | 115 | boolean BlindsServo::isClosing() { 116 | return target < currentPosition; 117 | } 118 | 119 | int BlindsServo::currentAngleInPercent() { 120 | return (int)round(((float) currentPosition / (float) servoMaxDegree) * 100); 121 | } 122 | 123 | int BlindsServo::getId() { 124 | return id; 125 | } 126 | int BlindsServo::getAngle() { 127 | return currentPosition; 128 | } 129 | 130 | boolean BlindsServo::isClosed() { 131 | return currentPosition == 0; 132 | } 133 | 134 | boolean BlindsServo::isMoving() { 135 | return currentPosition != target; 136 | } 137 | 138 | BlindsServo::blindsStatus BlindsServo::getStatus() { 139 | if (currentPosition == 0) { 140 | return BlindsServo::CLOSED; 141 | } else if(target < currentPosition) { 142 | return BlindsServo::CLOSING; 143 | } else if(target > currentPosition) { 144 | return BlindsServo::OPENING; 145 | } 146 | 147 | return BlindsServo::OPEN; 148 | } 149 | 150 | 151 | void BlindsServo::setDebug(bool debug) { 152 | isDebug = debug; 153 | } 154 | 155 | void BlindsServo::debugPrint(String text) { 156 | if (isDebug) { 157 | String value = "(" + String(id) + ") " + text; 158 | debugPrintCallback(value); 159 | } 160 | } 161 | 162 | void BlindsServo::setDebugPrintCallback(DEBUG_PRINT_CALLBACK_SIGNATURE) { 163 | if (isDebug) { 164 | this->debugPrintCallback = debugPrintCallback; 165 | } 166 | } 167 | 168 | void BlindsServo::setStatusChangedCallback(STATUS_CHANGED_CALLBACK_SIGNATURE) { 169 | this->statusChangedCallback = statusChangedCallback; 170 | } 171 | 172 | void BlindsServo::setPositionChangedCallback(POSITION_CHANGED_CALLBACK_SIGNATURE) { 173 | this->positionChangedCallback = positionChangedCallback; 174 | } 175 | 176 | // Privates 177 | int BlindsServo::angleToServo(int angle){ 178 | // Convert from min - max to 0 - 270 179 | 180 | // formula: (degree * (max - min / servoMax)) + offset 181 | float result = (angle * (float)((float)(servoMaxPulse - servoMinPulse) / (float)servoMaxDegree)) + servoMinPulse; 182 | 183 | int res = (int)round(result); 184 | 185 | if(isReversed) { // Servo reversing support 186 | res = (servoMaxPulse + servoMinPulse) - res; 187 | } 188 | 189 | // Ensure result is valid 190 | if (res < servoMinPulse || res > servoMaxPulse) { 191 | return servoMinPulse; 192 | } 193 | 194 | return res; 195 | } 196 | -------------------------------------------------------------------------------- /BlindsServo.h: -------------------------------------------------------------------------------- 1 | #ifndef BLINDSSERVO_H 2 | #define BLINDSSERVO_H 3 | 4 | #include 5 | #include 6 | 7 | #define DEBUG_PRINT_CALLBACK_SIGNATURE std::function debugPrintCallback 8 | #define STATUS_CHANGED_CALLBACK_SIGNATURE std::function statusChangedCallback 9 | #define POSITION_CHANGED_CALLBACK_SIGNATURE std::function positionChangedCallback 10 | 11 | 12 | class BlindsServo { 13 | public: 14 | enum blindsStatus { OPEN, CLOSED, OPENING, CLOSING }; 15 | 16 | BlindsServo(int id, int servoPin, int minPulseValue, int maxPulseValue, int maxDegree, boolean reversed = false, boolean debug = false); 17 | BlindsServo(); 18 | ~BlindsServo(); 19 | 20 | // Main loop 21 | void loop(); // Main loop 22 | 23 | // Debug 24 | void setDebug(bool debug); 25 | 26 | // Callbacks 27 | void setDebugPrintCallback(DEBUG_PRINT_CALLBACK_SIGNATURE); 28 | void setStatusChangedCallback(STATUS_CHANGED_CALLBACK_SIGNATURE); 29 | void setPositionChangedCallback(POSITION_CHANGED_CALLBACK_SIGNATURE); 30 | 31 | public: 32 | // Getters 33 | boolean isOpening(); 34 | boolean isClosing(); 35 | int currentAngleInPercent(); 36 | int getId(); 37 | int getAngle(); 38 | boolean isClosed(); 39 | boolean isMoving(); 40 | 41 | blindsStatus getStatus(); 42 | 43 | // Setters 44 | void setStop(); // Stops the movement 45 | void setOpen(); 46 | void setClose(); 47 | void goToPosition(int position); // 0-100 48 | 49 | private: 50 | void init(int id, int servoPin, int minPulseValue, int maxPulseValue, int maxDegree, boolean reversed, boolean debug); 51 | void attach(); 52 | void detach(); 53 | void goToAngle(int angle); // Goes to specific angle 54 | int angleToServo(int angle); 55 | void debugPrint(String text); 56 | 57 | private: 58 | Servo servo; 59 | boolean attached; 60 | 61 | int id = 0; // id of the servo 62 | 63 | // Previous status 64 | blindsStatus previousStatus; 65 | 66 | // Position management 67 | int target = 0; 68 | int currentPosition = -1; // -1 = unknown value (initial state) 69 | int previousPosition = -1; 70 | 71 | // Servo configuration 72 | int servoMinPulse; 73 | int servoMaxPulse; 74 | int servoPin; 75 | int servoMaxDegree; 76 | boolean isReversed = false; 77 | 78 | // Debugging options 79 | boolean isDebug = false; 80 | 81 | // Callbacks 82 | DEBUG_PRINT_CALLBACK_SIGNATURE {nullptr}; 83 | STATUS_CHANGED_CALLBACK_SIGNATURE { nullptr }; 84 | POSITION_CHANGED_CALLBACK_SIGNATURE { nullptr }; 85 | }; 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Window blinds to home assistant 2 | DIY window blinds controller software for ESP8266 (Wemos D1) to control existing window blinds using servos. 3 | 4 | The full DIY guide can be found from: https://www.creatingsmarthome.com/?p=629 5 | 6 | ## Support the developer? 7 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/tokorhon) 8 | 9 | -------------------------------------------------------------------------------- /blinds2mqtt.h: -------------------------------------------------------------------------------- 1 | // wifi settings 2 | const char* ssid = "****"; // Your WiFi SSID 3 | const char* password = "****"; // Your WiFi password 4 | 5 | // Servo pins to use (each servo to be placed on it's own IO pin) 6 | // To set servos turn as reversed, set reversed pins array to wich servos should turn opposite direction. example: { D7, D6 } 7 | // Reversed servos should be subset of servoPins e.g. reversedPin must be also in servoPins array! 8 | const unsigned int servoPins[] = { D7, D6, D5 }; 9 | const unsigned int reversedPins[] = { }; // Array of reversed servo pins. Must be subset of servoPins. 10 | const unsigned int turnTime = 50; // how many milliseconds after one point of turn (must be greater than amount of turn time of all the servos) 11 | 12 | // mqtt server settings 13 | const char* mqtt_server = "192.168.1.*"; // Your MQTT server address 14 | const int mqtt_port = 1883; // Your MQTT server port 15 | const char* mqtt_username = "****"; // Your MQTT user 16 | const char* mqtt_password = "****"; // Your MQTT password 17 | 18 | // mqtt client settings 19 | const char* client_id = "blinds"; // Must be unique on the MQTT network 20 | const boolean retain_status = true; // Retain status messages (keeps blinds status available after HA reset) 21 | const boolean retain_position = true; // Retain position messages 22 | 23 | // Home assistant configuration 24 | // Friendly name of the device. If using multiple servos, a number will be appended at the end of the name e.g. "Blinds 1", "Blinds 2" 25 | const String friendly_name = "Blinds"; 26 | 27 | // -- Below should not be changed if using same servos as mentioned in the blog post 28 | // Servo settings 29 | const int servo_min_pulse = 500; 30 | const int servo_max_pulse = 2500; 31 | const int servo_max_angle = 270; 32 | 33 | // OTA Settings 34 | const char* ota_password = "BlindsOTA"; 35 | 36 | // Mqtt topics (for advanced use, no need to modify) 37 | const char* blinds_state_topic = "blinds/%s/%d/state"; 38 | const char* blinds_command_topic = "blinds/%s/%d/set"; 39 | const char* blinds_position_topic = "blinds/%s/%d/position"; 40 | const char* blinds_set_position_topic = "blinds/%s/%d/position/set"; 41 | const char* blinds_debug_topic = "blinds/%s/debug"; // debug topic 42 | const char* ha_config_topic = "homeassistant/cover/%s/%d/config"; 43 | -------------------------------------------------------------------------------- /blinds2mqtt.ino: -------------------------------------------------------------------------------- 1 | // DIY Smart Blinds Controller for ESP8266 (Wemos D1 Mini) 2 | // Supports Home Assistant MQTT auto discovery straight out of the box 3 | // (c) Toni Korhonen 2021 4 | // https://www.creatingsmarthome.com/?p=629 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "blinds2mqtt.h" 14 | #include "BlindsServo.h" 15 | 16 | #define SW_VERSION "0.4.0" 17 | 18 | #define JSON_BUFFER_LENGTH 2048 19 | #define MQTT_TOPIC_MAX_LENGTH 256 20 | 21 | #define DEBUG false // default value for debug 22 | 23 | // Callbacks 24 | void mqttCallback(char* topic, byte* payload, unsigned int payloadLength); 25 | 26 | // Wifi 27 | WiFiClient wifiClient; 28 | 29 | // MQTT 30 | PubSubClient client(mqtt_server, mqtt_port, mqttCallback, wifiClient); 31 | 32 | BlindsServo servos[sizeof(servoPins)]; 33 | 34 | // debug 35 | bool debug = DEBUG; 36 | int numberOfServos = 0; 37 | int pos = 0; 38 | 39 | String uniqueId; 40 | 41 | unsigned long loopStart; 42 | 43 | void setup() { 44 | // Setup serial port 45 | Serial.begin(115200); 46 | 47 | uniqueId = WiFi.macAddress(); 48 | uniqueId.replace(":", "-"); 49 | 50 | wifiConnect(); 51 | 52 | // Setup OTA 53 | initOTA(); 54 | 55 | numberOfServos = sizeof(servoPins) / sizeof(servoPins[0]); 56 | int numberOfReversedServos = sizeof(reversedPins) / sizeof(servoPins[0]); // We might not have any servos as reversed, thus using servoPin size 57 | 58 | Serial.print("numberOfServos = "); 59 | Serial.println(numberOfServos); 60 | 61 | for (int i = 0; i < numberOfServos; i++) { 62 | unsigned int servoPin = servoPins[i]; 63 | boolean reversed = false; 64 | 65 | // Check if servo is in reversed array 66 | for (int r = 0; r < numberOfReversedServos; r++) { 67 | unsigned int reversedPin = reversedPins[r]; 68 | if(servoPin == reversedPin) { 69 | reversed = true; 70 | break; 71 | } 72 | } 73 | 74 | servos[i] = BlindsServo(i+1, servoPin, servo_min_pulse, servo_max_pulse, servo_max_angle, reversed, debug); 75 | servos[i].setDebugPrintCallback(debugPrint); 76 | servos[i].setStatusChangedCallback(statusChanged); 77 | servos[i].setPositionChangedCallback(positionChanged); 78 | } 79 | 80 | mqttConnect(); 81 | client.setCallback(mqttCallback); 82 | } 83 | 84 | void wifiConnect() { 85 | Serial.println("Connecting wifi.."); 86 | WiFi.begin(ssid, password); 87 | while (WiFi.status() != WL_CONNECTED) { 88 | // wait 500ms 89 | Serial.print("."); 90 | delay(500); 91 | } 92 | 93 | WiFi.mode(WIFI_STA); 94 | 95 | // Connected to WiFi 96 | Serial.println(); 97 | Serial.print("Connected! IP address: "); 98 | Serial.println(WiFi.localIP()); 99 | } 100 | 101 | void mqttConnect() { 102 | if (!!!client.connected()) { 103 | Serial.println("Try to connect mqtt.."); 104 | int count = 20; 105 | while (count-- > 0 && !!!client.connect(client_id, mqtt_username, mqtt_password)) { 106 | delay(500); 107 | } 108 | 109 | for (int i = 0; i < numberOfServos; i++) { 110 | BlindsServo s = servos[i]; 111 | Serial.print("Publishing ha config for servo "); 112 | Serial.println(String(s.getId())); 113 | 114 | subscribeAndPublishConfig(s.getId()); 115 | } 116 | } 117 | } 118 | 119 | 120 | void initOTA() { 121 | ArduinoOTA.setHostname(client_id); 122 | ArduinoOTA.setPassword(ota_password); 123 | ArduinoOTA.begin(); 124 | } 125 | 126 | void loop() 127 | { 128 | // check that we are connected 129 | if (!client.loop()) { 130 | mqttConnect(); 131 | } 132 | 133 | unsigned long now = millis(); 134 | 135 | if(now - loopStart >= turnTime) { 136 | // Over one sec from start loop passed 137 | loopStart = now; 138 | 139 | // Main servo loop 140 | for (int i = 0; i < numberOfServos; i++) { 141 | servos[i].loop(); 142 | } 143 | } 144 | 145 | // Rest of the loop 146 | MDNS.update(); 147 | ArduinoOTA.handle(); 148 | } 149 | 150 | BlindsServo& servoById(int id) { 151 | for (int i = 0; i < numberOfServos; i++) { 152 | BlindsServo& s = servos[i]; 153 | if (s.getId() == id) { 154 | return s; 155 | } 156 | } 157 | } 158 | 159 | String getValue(String data, char separator, int index) 160 | { 161 | int found = 0; 162 | int strIndex[] = {0, -1}; 163 | int maxIndex = data.length()-1; 164 | 165 | for(int i=0; i<=maxIndex && found<=index; i++){ 166 | if(data.charAt(i)==separator || i==maxIndex){ 167 | found++; 168 | strIndex[0] = strIndex[1]+1; 169 | strIndex[1] = (i == maxIndex) ? i+1 : i; 170 | } 171 | } 172 | 173 | return found>index ? data.substring(strIndex[0], strIndex[1]) : ""; 174 | } 175 | 176 | // MQTT 177 | void subscribeAndPublishConfig(int servoId) { 178 | // Subscribe to servo topic 179 | char topic[MQTT_TOPIC_MAX_LENGTH]; 180 | sprintf(topic, blinds_command_topic, uniqueId.c_str(), servoId); 181 | 182 | if (client.subscribe(topic)) { 183 | // OK 184 | debugPrint("Subscribed to blinds set topic (" + String(servoId) + "), uniqueId = " + uniqueId); 185 | } else { 186 | // FAIL 187 | debugPrint("Failed to subscribe to blinds set topic (" + String(servoId) + ")"); 188 | } 189 | 190 | char set_position_t[MQTT_TOPIC_MAX_LENGTH]; 191 | sprintf(set_position_t, blinds_set_position_topic, uniqueId.c_str(), servoId); 192 | if (client.subscribe(set_position_t)) { 193 | // OK 194 | debugPrint("Subscribed to blinds set position topic (" + String(servoId) + "), uniqueId = " + uniqueId); 195 | publishConfig(servoId); // Publish config right after subscribed to command topic 196 | } else { 197 | // FAIL 198 | debugPrint("Failed to subscribe to blinds set topic (" + String(servoId) + ")"); 199 | } 200 | 201 | publishConfig(servoId); // Publish config right after subscribed to command topic 202 | } 203 | 204 | void mqttCallback(char* topic, byte * payload, unsigned int length) { 205 | Serial.print("Got MQTT callback! "); 206 | Serial.print("topic = "); 207 | Serial.println(topic); 208 | 209 | String myString = String(topic); 210 | 211 | String v = getValue(topic, '/', 2); // Get the second value 212 | if (!v.length()) { 213 | return; 214 | } 215 | 216 | int servoId = v.toInt(); // Get servo id 217 | 218 | // Check 219 | if (getValue(topic, '/', 0) == "blinds") { //Ensure blinds topic 220 | if(getValue(topic, '/', 1) == uniqueId) { // Ensure correct device 221 | String thirdValue = getValue(topic, '/', 3); 222 | if(thirdValue == "set") { // Ensure set topic 223 | handleSet(payload, length, servoId); 224 | } else if(thirdValue == "position") { 225 | String fourthValue = getValue(topic, '/', 4); 226 | if(fourthValue == "set") { // Ensure set topic 227 | handleSetPosition(payload, length, servoId); 228 | } 229 | } 230 | } 231 | } 232 | } 233 | 234 | void handleSet(byte * payload, unsigned int length, int servoId) { 235 | BlindsServo& s = servoById(servoId); 236 | if (!strncmp((char *)payload, "OPEN", length)) { 237 | s.setOpen(); 238 | } else if (!strncmp((char *)payload, "CLOSE", length)) { 239 | s.setClose(); 240 | } else if (!strncmp((char *)payload, "STOP", length)) { 241 | s.setStop(); 242 | } 243 | } 244 | 245 | void handleSetPosition(byte * payload, unsigned int length, int servoId) { 246 | 247 | BlindsServo& s = servoById(servoId); 248 | String myString = (char*)payload; 249 | int pos = myString.toInt(); 250 | if(pos >= 0 && pos <= 100) { 251 | s.goToPosition(pos); 252 | } 253 | } 254 | 255 | void publishConfig(int servoId) { 256 | Serial.println("Publishing ha config."); 257 | 258 | DynamicJsonDocument root(JSON_BUFFER_LENGTH); 259 | 260 | // State topic 261 | char state_t[MQTT_TOPIC_MAX_LENGTH]; 262 | sprintf(state_t, blinds_state_topic, uniqueId.c_str(), servoId); 263 | root["state_topic"] = state_t; 264 | 265 | // Command topic 266 | char command_t[MQTT_TOPIC_MAX_LENGTH]; 267 | sprintf(command_t, blinds_command_topic, uniqueId.c_str(), servoId); 268 | root["command_topic"] = command_t; 269 | 270 | // Position topics 271 | char position_t[MQTT_TOPIC_MAX_LENGTH]; 272 | sprintf(position_t, blinds_position_topic, uniqueId.c_str(), servoId); 273 | root["position_topic"] = position_t; 274 | 275 | char set_position_t[MQTT_TOPIC_MAX_LENGTH]; 276 | sprintf(set_position_t, blinds_set_position_topic, uniqueId.c_str(), servoId); 277 | root["set_position_topic"] = set_position_t; 278 | 279 | // Others 280 | root["name"] = numberOfServos > 1 ? friendly_name + " " + String(servoId) : friendly_name; 281 | root["device_class"] = "blind"; 282 | root["unique_id"] = "blinds/" + uniqueId + "/servo" + String(servoId); 283 | 284 | // Device 285 | addDevice(root); 286 | 287 | // Publish 288 | String mqttOutput; 289 | serializeJson(root, mqttOutput); 290 | 291 | char t[MQTT_TOPIC_MAX_LENGTH]; 292 | sprintf(t, ha_config_topic, uniqueId.c_str(), servoId); 293 | client.beginPublish(t, mqttOutput.length(), true); 294 | client.print(mqttOutput); 295 | client.endPublish(); 296 | } 297 | 298 | void addDevice(DynamicJsonDocument& root) { 299 | JsonObject device = root.createNestedObject("device"); 300 | 301 | JsonArray identifiers = device.createNestedArray("identifiers"); 302 | identifiers.add(uniqueId.c_str()); 303 | 304 | device["name"] = "blinds2mqtt"; 305 | device["model"] = "esp8266"; 306 | device["sw_version"] = SW_VERSION; 307 | } 308 | 309 | //Callbacks 310 | void debugPrint(String message) { 311 | if (debug) { 312 | Serial.println(message); 313 | // publish to debug topic 314 | char t[MQTT_TOPIC_MAX_LENGTH]; 315 | sprintf(t, blinds_debug_topic, uniqueId.c_str()); 316 | client.publish(t, message.c_str()); 317 | } 318 | } 319 | 320 | void positionChanged(int servoId) { 321 | // Do nothing, only inform position when status is changed 322 | } 323 | 324 | void statusChanged(int servoId) { 325 | BlindsServo& s = servoById(servoId); 326 | 327 | String statusMsg = "OPEN"; 328 | 329 | switch(s.getStatus()) { 330 | case BlindsServo::OPEN: 331 | statusMsg = "open"; 332 | break; 333 | case BlindsServo::CLOSED: 334 | statusMsg = "closed"; 335 | break; 336 | case BlindsServo::CLOSING: 337 | statusMsg = "closing"; 338 | break; 339 | case BlindsServo::OPENING: 340 | statusMsg = "opening"; 341 | break; 342 | default: 343 | break; 344 | } 345 | 346 | // Publish status 347 | char t[MQTT_TOPIC_MAX_LENGTH]; 348 | sprintf(t, blinds_state_topic, uniqueId.c_str(), servoId); 349 | client.publish(t, statusMsg.c_str(), retain_status); 350 | 351 | // Publish position 352 | char position_t[MQTT_TOPIC_MAX_LENGTH]; 353 | sprintf(position_t, blinds_position_topic, uniqueId.c_str(), servoId); 354 | client.publish(position_t, String(s.currentAngleInPercent()).c_str(), retain_position); 355 | } 356 | --------------------------------------------------------------------------------