diff --git a/README.md b/README.md index 457dd2c..aeddb6d 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,10 @@ -
- 👉 See Winderoo in action on youtube 👈 -
-
- Winderoo - The Open Source Watch Winder +
+ + Winderoo - The Open Source Watch Winder +

@@ -36,7 +35,7 @@ * Simple setup. Flash the firmware and File System with a few clicks, then connect your phone (or other device) to the winder's setup wifi network & add it to your home network. * There's no app required! You control it from a web browser. * Minimal electronics / programming experience required -* Web UI is fully tranlated into 5 langauges (more are welcome!) +* Web UI is fully translated into 5 languages (more are welcome!) ### Winderoo Requires a Different Microcontroller @@ -52,7 +51,7 @@ #### Download and install the following on your computer: 1. [Visual Studio Code](https://code.visualstudio.com/) 1. [PlatformIO](https://platformio.org/install/ide?install=vscode) - - Note: the "extensions" button has changed since Platformio has created their install guide. You can access the extions pane with the following key combinations: + - Note: the "extensions" button has changed since Platformio has created their install guide. You can access the extensions pane with the following key combinations: - Windows: Ctrl+Shift+X - macOS: Command+Shift+X 1. You may or may not need these drivers, but some 'knock off' ESP32 dev boards require them. @@ -70,12 +69,12 @@ This project welcomes contributions. Please follow the regular git workflow; fork + PR to contribute. ### Translations / Localization -Winderoo has multi-language support, and we welcome adding more langauges. +Winderoo has multi-language support, and we welcome adding more languages. To add another language: 1. Duplicate one of the current localizations, and translate the text: - `src/angular/osww-frontend/src/assets/i18n/` stores the current localizations. -1. Add a new langauge menu item here to enable your langauge: +1. Add a new language menu item here to enable your language: - `src/angular/osww-frontend/src/app/header/header.component.html` 1. Open a Pull Request diff --git a/docs/images/home-assistant-gui.png b/docs/images/home-assistant-gui.png new file mode 100644 index 0000000..f4ec402 Binary files /dev/null and b/docs/images/home-assistant-gui.png differ diff --git a/docs/images/winderoo-splash.png b/docs/images/winderoo-splash.png new file mode 100644 index 0000000..4062e3e Binary files /dev/null and b/docs/images/winderoo-splash.png differ diff --git a/docs/install-software.md b/docs/install-software.md index 3b2f154..1b9f4fc 100644 --- a/docs/install-software.md +++ b/docs/install-software.md @@ -21,8 +21,8 @@ - If you downloaded the repository as a zip, uzip it before proceeding to step 2.

how to download
1. Open the extracted folder (or cloned repository if using git) in Visual Studio Code -1. **Build Options - IMPORTANT** - - if you're building Winderoo with an OLED screen or you desire fine-grained motor control (pulse width modulation), you must enable one (or two) build flags to tell PlatformIO to include additional libraries. +1. **Build Options - (PWM, OLED, Home Assistant)** + - if you're building Winderoo with an OLED screen, intend to use Home Assistant, or you desire fine-grained motor control (pulse width modulation), you must enable some of the following build flags to tell PlatformIO to include additional libraries. - To toggle these build flags, navigate to the file called `platformio.ini`:
how to download
- In this file, you'll see the following block of code: @@ -30,11 +30,23 @@ build_flags = -D OLED_ENABLED=false -D PWM_MOTOR_CONTROL=false + -D HOME_ASSISTANT_ENABLED=false ``` + - Change `-D HOME_ASSISTANT_ENABLED=false` to `-D HOME_ASSISTANT_ENABLED=true` to enable Winderoo's Home Assistant integration + - > 🚦 I'd strongly recommend you have a dedicated MQTT user; do not use your main account. + + - If this feature is enabled, you must add your Home Assistant's IP, username, and password. + - You can add them in [`main.cpp`](https://github.com/mwood77/winderoo/blob/main/src/platformio/osww-server/src/main.cpp#L51-L54) inside the configuration block: + ```cpp + // Home Assistant Configuration + const char* HOME_ASSISTANT_BROKER_IP = "YOUR_HOME_ASSISTANT_IP"; + const char* HOME_ASSISTANT_USERNAME = "REPLACE_THIS_WITH_HOME_ASSISTANT_LOGIN_USERNAME"; + const char* HOME_ASSISTANT_PASSWORD = "REPLACE_THIS_WITH_HOME_ASSISTANT_LOGIN_PASSWORD"; + ``` - Change `-D OLED_ENABLED=false` to `-D OLED_ENABLED=true` to enable OLED screen support - Change `-D PWM_MOTOR_CONTROL=false` to `-D PWM_MOTOR_CONTROL=true` to enable PWM motor control; at the time of writing, Winderoo with PWM only supports `MX1508` derived motor controllers. - - > PWM_MOTOR_CONTROL is an experimental flag. You will encounter incorrect cycle time estimation and other possible bugs unless you align the motor speed to **20 RPM** (see [Troubleshooting](#troubleshooting)). Use at your own risk. - - PlatformIO will now compile Winderoo with OLED screen and or PWM motor support + - > `PWM_MOTOR_CONTROL` is an experimental flag. You will encounter incorrect cycle time estimation and other possible bugs unless you align the motor speed to **20 RPM** (see [Troubleshooting](#troubleshooting)). Use at your own risk. + - PlatformIO will now compile Winderoo with OLED screen, Home Assistant, and or PWM motor support 1. Select 'PlatformIO' (alien/insect looking button) on the workspace menu and wait for visual studio code to finish initializing the project
platformIO button
1. Expand the main heading: **"esp32doit-devkit-v1"**: @@ -48,7 +60,7 @@ 1. If you have a different LED state, compare it with this table: - [Understanding Winderoo's LED Blink Status](user-manual.md#understanding-winderoos-led-blink-status) -## Next steps: +## Next steps Ok, you've got 2 LEDs illuminated on your board. Great! Let's make sure the code works. @@ -64,6 +76,68 @@ Ok, you've got 2 LEDs illuminated on your board. Great! Let's make sure the code 1. If you see Winderoo's user interface, you're all done! - [Here is an overview of Winderoo's user interface](./user-manual.md) +### Home Assistant +If you've enabled Winderoo's Home Assistant integration, Winderoo will stream a number of entities into Home Assistant over MQTT. If you're unsure what MQTT or need to set this up in Home Assistant, [please see this document](https://www.home-assistant.io/integrations/mqtt). + +Winderoo should be automatically discovered by Home Assistant within 60 seconds. The following entities are available to Home Assistant: +```yml + - button.winderoo_start + - button.winderoo_stop + + - sensor.winderoo_status : "Winding | Stopped" + - sensor.winderoo_wifi_reception : "Excellent | Good | Fair | Poor" + + - number.winderoo_rotations_per_day : 100 <-> 960 + + - select.winderoo_direction : "CCW | BOTH | CW" + - select.winderoo_hour : 00 <-> 23 + - select.winderoo_minutes : 00 <-> 50 + + - switch.winderoo_timer_enabled : "true | false" + - switch.winderoo_oled : "true | false" + - switch.winderoo_power : "true | false" +``` + +You can replicate Winderoo's GUI with a basic Home Assistant card: + + + + + + + + + +
Home Assistant GUICode
+ Winderoo - The Open Source Watch Winder + +

+type: entities
+entities:
+- entity: sensor.winderoo_status
+- entity: number.winderoo_rotations_per_day
+    name: Rotations Per Day
+- entity: select.winderoo_direction
+    name: Direction
+- entity: button.winderoo_start
+    name: Start
+- entity: button.winderoo_stop
+    name: Stop
+- entity: switch.winderoo_timer_enabled
+    name: Timer Enabled
+- entity: select.winderoo_hour
+    name: Hour
+- entity: select.winderoo_minutes
+    name: Minutes
+- entity: switch.winderoo_oled
+    name: OLED
+- entity: switch.winderoo_power
+    name: Power
+title: Winderoo
+show_header_toggle: false
+
+
+ ## Troubleshooting ### Motor Turns too fast when using PWM > [!WARNING] diff --git a/docs/user-manual.md b/docs/user-manual.md index c18645a..025efec 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -17,7 +17,7 @@ ### Reset / Change WiFi Network | UI Element | Function | | :---: |:---: | -| | This is will open a pop-up window, which will ask you to confirm reset, and walk you through the reset proceedure. Use this to change the WiFi network Winderoo connects to. | +| | This is will open a pop-up window, which will ask you to confirm reset, and walk you through the reset procedure. Use this to change the WiFi network Winderoo connects to. | | | This will open a pop-up menu where you can select which language you'd prefer. | ### Status Bar @@ -57,7 +57,7 @@ | UI Element | Function | | :---: |:---: | | | This will enable or disable the winder's timer. When the switch is set to **ENABLED**, Winderoo will begin winding at a your desired 'Cycle Start Time.' If the switch is set to **DISABLED**, you must start the winder using the [control buttons](#control-buttons) | -| | Set which time you'd like Winderoo to begin winding at. **_Important!_** WInderoo will _always_ start at this time, even if you've already triggered a manual run with a [control button](#control-buttons). To stop this behaviour, see: [Enable / Disable Winding](#enable--disable-winding) | +| | Set which time you'd like Winderoo to begin winding at. **_Important!_** Winderoo will _always_ start at this time, even if you've already triggered a manual run with a [control button](#control-buttons). To stop this behavior, see: [Enable / Disable Winding](#enable--disable-winding) | ### OLED Screen @@ -72,13 +72,13 @@ ### Save / Update Settings | UI Element | Function | | :---: |:---: | -| | This will capture and save all settings (winding direction, rotations per day, cycle start time). If a winding routine is currenty running, it does not reset the current routine (it will update and finish accordingly). If you wish to make sure the routine is changed, manually stop, then start the routine. See [control buttons](#control-buttons). | +| | This will capture and save all settings (winding direction, rotations per day, cycle start time). If a winding routine is currently running, it does not reset the current routine (it will update and finish accordingly). If you wish to make sure the routine is changed, manually stop, then start the routine. See [control buttons](#control-buttons). | ## Understanding Winderoo's LED Blink Status - Most ESP32 dev boards have a primary RED LED that is always on. This cannot be shut off via firmware. - - If you find it bothersome, you can cover it with electrical tape, de-solder it, or cut the trace with an exacto knife. + - If you find it bothersome, you can cover it with electrical tape, de-solder it, or cut the trace with an x-acto knife. - Most ESP32 dev boards have a secondary BLUE LED, however some may be a different colour. - Please use the following table to understand what Winderoo is telling you. @@ -90,4 +90,4 @@ | | secondary LED is illuminated | Winderoo is ready for setup. Connect to the WiFi network called "Winderoo Setup" and add Winderoo to your WiFi network. | | | slow blinking | Winderoo has successfully connected to your WiFi network. When the the **BLUE LED** stops blinking, you may access Winderoo's UI from your web browser. | | | fast blinking | Winderoo is resetting, wait until the **BLUE LED** turns solid to begin WiFi setup. | -| | extremely slow blinking | Winderoo's winding capabilities have been turned 'OFF' via the software swtich, or an optional physical button. Winderoo will not wind until it has been turned to 'ON.' | \ No newline at end of file +| | extremely slow blinking | Winderoo's winding capabilities have been turned 'OFF' via the software switch, or an optional physical button. Winderoo will not wind until it has been turned to 'ON.' | \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 58d9335..9772eeb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -23,6 +23,7 @@ check_tool = cppcheck, clangtidy build_flags = -D OLED_ENABLED=false -D PWM_MOTOR_CONTROL=false + -D HOME_ASSISTANT_ENABLED=false check_flags = clangtidy: -fix-errors,--format-style=google lib_deps = @@ -34,3 +35,4 @@ lib_deps = fbiego/ESP32Time@^2.0.0 adafruit/Adafruit SSD1306@^2.5.9 electromagus/ESPMX1508@^1.0.5 + dawidchyrzynski/home-assistant-integration@^2.1.0 diff --git a/src/platformio/osww-server/src/main.cpp b/src/platformio/osww-server/src/main.cpp index 9d41dc0..528577a 100644 --- a/src/platformio/osww-server/src/main.cpp +++ b/src/platformio/osww-server/src/main.cpp @@ -33,7 +33,7 @@ * externalButton = OPTIONAL - If you want to use an external ON/OFF button, connect it to this pin 13. If you need to use another pin, change the value here. * * If you're using a NeoPixel equipped board, you'll need to change directionalPinA, directionalPinB and ledPin (pin 18 on most, I think) to appropriate GPIOs. - * Faiulre to set these pins on NeoPixel boards will result in kernel panics. + * Failure to set these pins on NeoPixel boards will result in kernel panics. */ int durationInSecondsToCompleteOneRevolution = 8; int directionalPinA = 25; @@ -47,6 +47,11 @@ bool OLED_ROTATE_SCREEN_180 = false; int SCREEN_WIDTH = 128; // OLED display width, in pixels int SCREEN_HEIGHT = 64; // OLED display height, in pixels int OLED_RESET = -1; // Reset pin number (or -1 if sharing Arduino reset pin) + +// Home Assistant Configuration +const char* HOME_ASSISTANT_BROKER_IP = "YOUR_HOME_ASSISTANT_IP"; +const char* HOME_ASSISTANT_USERNAME = "YOUR_HOME_ASSISTANT_LOGIN_USERNAME"; +const char* HOME_ASSISTANT_PASSWORD = "YOUR_HOME_ASSISTANT_LOGIN_PASSWORD"; /* * ************************************************************************************* * ******************************* END CONFIGURABLES *********************************** @@ -89,6 +94,7 @@ AsyncWebServer server(80); HTTPClient http; WiFiClient client; ESP32Time rtc; +String winderooVersion = "3.0.0"; #if PWM_MOTOR_CONTROL MotorControl motor(directionalPinA, directionalPinB, true); @@ -100,6 +106,26 @@ ESP32Time rtc; Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); #endif +#ifdef HOME_ASSISTANT_ENABLED + #include + + HADevice device; + HAMqtt mqtt(client, device); + + // Define HA Sensors + HASwitch ha_oledSwitch("oled"); + HANumber ha_rpd("rpd"); + HASelect ha_selectDirection("direction"); + HASwitch ha_timerSwitch("timerEnabled"); + HAButton ha_startButton("startButton"); + HAButton ha_stopButton("stopButton"); + HASelect ha_selectHours("hour"); + HASelect ha_selectMinutes("minutes"); + HASwitch ha_powerSwitch("power"); + HASensor ha_rssiReception("rssiReception"); + HASensor ha_activityState("activity"); +#endif + void drawCentreStringToMemory(const char *buf, int x, int y) { int16_t x1, y1; @@ -167,11 +193,12 @@ static void drawWifiStatus() { if (WiFi.RSSI() > -50) { - // Excelent reception - 4 bars + // Excellent reception - 4 bars display.fillRect(14, 55+8, 2, 2, WHITE); display.fillRect(18, 55+6, 2, 4, WHITE); display.fillRect(22, 55+4, 2, 6, WHITE); display.fillRect(26, 55+2, 2, 8, WHITE); + if (HOME_ASSISTANT_ENABLED) ha_rssiReception.setValue("Excellent"); } else if (WiFi.RSSI() > -60) { @@ -179,17 +206,20 @@ static void drawWifiStatus() { display.fillRect(14, 55+8, 2, 2, WHITE); display.fillRect(18, 55+6, 2, 4, WHITE); display.fillRect(22, 55+4, 2, 6, WHITE); + if (HOME_ASSISTANT_ENABLED) ha_rssiReception.setValue("Good"); } else if (WiFi.RSSI() > -70) { // Fair reception - 2 bars display.fillRect(14, 55+8, 2, 2, WHITE); display.fillRect(18, 55+6, 2, 4, WHITE); + if (HOME_ASSISTANT_ENABLED) ha_rssiReception.setValue("Fair"); } else { // Terrible reception - 1 bar display.fillRect(14, 55+8, 2, 2, WHITE); + if (HOME_ASSISTANT_ENABLED) ha_rssiReception.setValue("Poor"); } } } @@ -261,8 +291,76 @@ template static void drawMultiLineText(const String (&message)[N]) { } } +// Home Assistant Helper Functions +/** + * @brief Returns the index corresponding to a given direction for Home Assistant. + * + * This function takes a direction string and returns an integer index that + * corresponds to the direction for Home Assistant. The mapping is as follows: + * - "CCW" -> 0 + * - "BOTH" -> 1 + * - Any other string -> 2 + * + * @param direction The direction string. Expected values are "CCW", "BOTH", or any other string. + * @return int The index corresponding to the given direction. + */ +int getDirectionIndexForHomeAssistant(String direction) +{ + if (direction == "CCW") + { + return 0; + } + else if (direction == "BOTH") + { + return 1; + } + else + { + return 2; + } +} + +/** + * @brief Converts a given minute value to an index used by Home Assistant. + * + * This function takes a minute value and returns a corresponding index + * that is used by Home Assistant. The mapping is as follows: + * - 0 minutes -> index 0 + * - 10 minutes -> index 1 + * - 20 minutes -> index 2 + * - 30 minutes -> index 3 + * - 40 minutes -> index 4 + * - 50 minutes -> index 5 + * + * If the minute value does not match any of the predefined cases, the function + * returns 0 by default. + * + * @param minuteValue The minute value to be converted to an index. + * @return The index corresponding to the given minute value. + */ +int getTimerMinutesIndexForHomeAssistant(int minuteValue) +{ + switch(minuteValue) + { + case 0: + return 0; + case 10: + return 1; + case 20: + return 2; + case 30: + return 3; + case 40: + return 4; + case 50: + return 5; + default: + return 0; + } +} + /** - * Calclates the duration and estimated finish time of the winding routine + * Calculates the duration and estimated finish time of the winding routine * * @return epoch - estimated epoch when winding routine will finish */ @@ -307,6 +405,7 @@ void beginWindingRoutine() Serial.println(finishTime); drawNotification("Winding"); + if (HOME_ASSISTANT_ENABLED) ha_activityState.setValue("Winding"); } /** @@ -473,6 +572,7 @@ void startWebserver() if( strcmp(p->name().c_str(), "timerEnabled") == 0 ) { userDefinedSettings.timerEnabled = p->value().c_str(); + if (HOME_ASSISTANT_ENABLED) ha_timerSwitch.setState(userDefinedSettings.timerEnabled.toInt()); } } @@ -516,9 +616,16 @@ void startWebserver() motor.stop(); display.clearDisplay(); display.display(); + + if (HOME_ASSISTANT_ENABLED) + { + ha_powerSwitch.setState(false); + ha_activityState.setValue("Stopped"); + } } else { drawStaticGUI(true); drawDynamicGUI(); + if (HOME_ASSISTANT_ENABLED) ha_powerSwitch.setState(true); } request->send(204); @@ -558,6 +665,19 @@ void startWebserver() String requestAction = json["action"].as(); screenSleep = json["screenSleep"].as(); + // Update Home Assistant state + + if (HOME_ASSISTANT_ENABLED) + { + ha_timerSwitch.setState(userDefinedSettings.timerEnabled.toInt()); + ha_selectHours.setState(userDefinedSettings.hour.toInt()); + ha_selectMinutes.setState(getTimerMinutesIndexForHomeAssistant(userDefinedSettings.minutes.toInt())); + ha_oledSwitch.setState(!screenSleep); // Invert state because naming is hard... + ha_rpd.setState(static_cast(requestTPD.toInt())); + ha_selectDirection.setState(getDirectionIndexForHomeAssistant(requestRotationDirection)); + } + + // Update motor direction if (strcmp(requestRotationDirection.c_str(), userDefinedSettings.direction.c_str()) != 0) { @@ -606,6 +726,7 @@ void startWebserver() routineRunning = false; userDefinedSettings.status = "Stopped"; drawNotification("Stopped"); + if (HOME_ASSISTANT_ENABLED) ha_activityState.setValue("Stopped"); } // Update screen sleep state @@ -703,7 +824,7 @@ void triggerLEDCondition(int blinkState) /** * This is a non-block button listener function. - * Credit to github OSWW ontribution from user @danagarcia + * Credit to github OSWW contribution from user @danagarcia * * @param pauseInSeconds the amount of time to pause and listen */ @@ -723,6 +844,7 @@ void awaitWhileListening(int pauseInSeconds) routineRunning = false; userDefinedSettings.status = "Stopped"; Serial.println("[STATUS] - Switched off!"); + if (HOME_ASSISTANT_ENABLED) ha_activityState.setValue("Stopped"); } } else @@ -767,11 +889,268 @@ void saveWifiCallback() delay(1500); } +// MQTT & Home Assistant Handlers +void mqttOnConnected() +{ + Serial.println("[STATUS] - MQTT connected!"); +} + +void mqttOnDisconnected() +{ + Serial.println("[STATUS] - MQTT disconnected!"); +} + +void onOledSwitchCommand(bool state, HASwitch* sender) +{ + if (state) + { + screenSleep = false; + display.clearDisplay(); + drawStaticGUI(true); + drawDynamicGUI(); + } + else + { + screenSleep = true; + display.clearDisplay(); + display.display(); + } + + sender->setState(state); +} + +void onRpdChangeCommand(HANumeric number, HANumber* sender) +{ + char buffer[10]; + number.toStr(buffer); + userDefinedSettings.rotationsPerDay = String(buffer); + + bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); + if ( !writeSuccess ) + { + Serial.println("[ERROR] - Failed to write number state [MQTT]"); + } + + sender->setCurrentState(number); +} + +void onSelectDirectionCommand(int8_t index, HASelect* sender) { + switch (index) { + case 0: + // Option "CCW" was selected + userDefinedSettings.direction = "CCW"; + break; + + case 1: + // Option "BOTH" was selected + userDefinedSettings.direction = "BOTH"; + break; + + case 2: + // Option "CW" was selected + userDefinedSettings.direction = "CW"; + break; + + default: + // unknown option + return; + } + + bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); + if ( !writeSuccess ) + { + Serial.println("[ERROR] - Failed to write direction select state [MQTT]"); + } + + sender->setState(index); +} + +void onTimerSwitchCommand(bool state, HASwitch* sender) +{ + userDefinedSettings.timerEnabled = state ? "1" : "0"; + bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); + if ( !writeSuccess ) + { + Serial.println("[ERROR] - Failed to write timer switch state [MQTT]"); + } + + sender->setState(state); +} + +void handleHAStartButton(HAButton* sender) +{ + if (!routineRunning) + { + beginWindingRoutine(); + } +} + +void handleHAStopButton(HAButton* sender) +{ + motor.stop(); + routineRunning = false; + userDefinedSettings.status = "Stopped"; + drawNotification("Stopped"); + ha_activityState.setValue("Stopped"); +} + +void onSelectHoursCommand(int8_t index, HASelect* sender) +{ + // Ugly but more reliable + switch (index) + { + case 0: + userDefinedSettings.hour = "00"; + break; + case 1: + userDefinedSettings.hour = "01"; + break; + case 2: + userDefinedSettings.hour = "02"; + break; + case 3: + userDefinedSettings.hour = "03"; + break; + case 4: + userDefinedSettings.hour = "04"; + break; + case 5: + userDefinedSettings.hour = "05"; + break; + case 6: + userDefinedSettings.hour = "06"; + break; + case 7: + userDefinedSettings.hour = "07"; + break; + case 8: + userDefinedSettings.hour = "08"; + break; + case 9: + userDefinedSettings.hour = "09"; + break; + case 10: + userDefinedSettings.hour = "10"; + break; + case 11: + userDefinedSettings.hour = "11"; + break; + case 12: + userDefinedSettings.hour = "12"; + break; + case 13: + userDefinedSettings.hour = "13"; + break; + case 14: + userDefinedSettings.hour = "14"; + break; + case 15: + userDefinedSettings.hour = "15"; + break; + case 16: + userDefinedSettings.hour = "16"; + break; + case 17: + userDefinedSettings.hour = "17"; + break; + case 18: + userDefinedSettings.hour = "18"; + break; + case 19: + userDefinedSettings.hour = "19"; + break; + case 20: + userDefinedSettings.hour = "20"; + break; + case 21: + userDefinedSettings.hour = "21"; + break; + case 22: + userDefinedSettings.hour = "22"; + break; + case 23: + userDefinedSettings.hour = "23"; + break; + default: + return; + } + + bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); + if ( !writeSuccess ) + { + Serial.println("[ERROR] - Failed to write hours select state [MQTT]"); + } + + sender->setState(index); +} + +void onSelectMinutesCommand(int8_t index, HASelect* sender) +{ + switch(index) + { + case 0: + userDefinedSettings.minutes = "00"; + break; + case 1: + userDefinedSettings.minutes = "10"; + break; + case 2: + userDefinedSettings.minutes = "20"; + break; + case 3: + userDefinedSettings.minutes = "30"; + break; + case 4: + userDefinedSettings.minutes = "40"; + break; + case 5: + userDefinedSettings.minutes = "50"; + break; + default: + return; + } + + bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); + if ( !writeSuccess ) + { + Serial.println("[ERROR] - Failed to write minutes select state [MQTT]"); + } + + sender->setState(index); +} + +void onPowerSwitchCommand(bool state, HASwitch* sender) +{ + userDefinedSettings.winderEnabled = state ? "1" : "0"; + + if (userDefinedSettings.winderEnabled == "0") + { + Serial.println("[STATUS] - Switched off!"); + userDefinedSettings.status = "Stopped"; + routineRunning = false; + motor.stop(); + display.clearDisplay(); + display.display(); + ha_activityState.setValue("Stopped"); + } else { + drawStaticGUI(true); + drawDynamicGUI(); + } + + bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); + if ( !writeSuccess ) + { + Serial.println("[ERROR] - Failed to write power switch state [MQTT]"); + } + + sender->setState(state); +} + void setup() { WiFi.mode(WIFI_STA); Serial.begin(115200); - setCpuFrequencyMhz(80); + setCpuFrequencyMhz(160); // Prepare pins pinMode(directionalPinA, OUTPUT); @@ -827,6 +1206,90 @@ void setup() } MDNS.addService("_winderoo", "_tcp", 80); Serial.println("[STATUS] - mDNS started"); + + // Configure Home Assistant + if (HOME_ASSISTANT_ENABLED) + { + byte mac[6]; + WiFi.macAddress(mac); + device.setUniqueId(mac, sizeof(mac)); + + device.setName("Winderoo"); + device.setManufacturer("mwood77"); + device.setModel("Winderoo"); + device.setSoftwareVersion(winderooVersion.c_str()); + device.enableSharedAvailability(); + + ha_oledSwitch.setName("OLED"); + ha_oledSwitch.setIcon("mdi:overscan"); + ha_oledSwitch.setCurrentState(!screenSleep); + ha_oledSwitch.onCommand(onOledSwitchCommand); + + ha_rpd.setName("Rotations Per Day"); + ha_rpd.setIcon("mdi:rotate-3d-variant"); + ha_rpd.setMin(100); + ha_rpd.setMax(960); + ha_rpd.setStep(10); + ha_rpd.setCurrentState(static_cast(userDefinedSettings.rotationsPerDay.toInt())); + ha_rpd.setOptimistic(true); + ha_rpd.onCommand(onRpdChangeCommand); + + ha_selectDirection.setName("Direction"); + ha_selectDirection.setIcon("mdi:arrow-left-right"); + ha_selectDirection.setOptions("CCW;BOTH;CW"); + ha_selectDirection.onCommand(onSelectDirectionCommand); + ha_selectDirection.setCurrentState(getDirectionIndexForHomeAssistant(userDefinedSettings.direction)); + + ha_timerSwitch.setName("Timer Enabled"); + ha_timerSwitch.setIcon("mdi:timer"); + ha_timerSwitch.setCurrentState(userDefinedSettings.timerEnabled.toInt()); + ha_timerSwitch.onCommand(onTimerSwitchCommand); + + ha_startButton.setName("Start"); + ha_startButton.setIcon("mdi:play"); + ha_startButton.onCommand(handleHAStartButton); + + ha_stopButton.setName("Stop"); + ha_stopButton.setIcon("mdi:stop"); + ha_stopButton.onCommand(handleHAStopButton); + + ha_selectHours.setName("Hour"); + ha_selectHours.setIcon("mdi:timer-sand-full"); + ha_selectHours.setOptions("00;01;02;03;04;05;06;07;08;09;10;11;12;13;14;15;16;17;18;19;20;21;22;23"); + ha_selectHours.setCurrentState(userDefinedSettings.hour.toInt()); + ha_selectHours.onCommand(onSelectHoursCommand); + + ha_selectMinutes.setName("Minutes"); + ha_selectMinutes.setIcon("mdi:timer-sand-empty"); + ha_selectMinutes.setOptions("00;10;20;30;40;50"); + ha_selectMinutes.setCurrentState(userDefinedSettings.minutes.toInt()); + ha_selectMinutes.onCommand(onSelectMinutesCommand); + + ha_powerSwitch.setName("Power"); + ha_powerSwitch.setIcon("mdi:power"); + ha_powerSwitch.setCurrentState(userDefinedSettings.winderEnabled.toInt()); + ha_powerSwitch.onCommand(onPowerSwitchCommand); + + ha_activityState.setName("Status"); + ha_activityState.setIcon("mdi:information"); + ha_activityState.setValue(userDefinedSettings.status.c_str()); + + ha_rssiReception.setName("WiFi Reception"); + ha_rssiReception.setIcon("mdi:antenna"); + + mqtt.onConnected(mqttOnConnected); + mqtt.onDisconnected(mqttOnDisconnected); + mqtt.begin(HOME_ASSISTANT_BROKER_IP, HOME_ASSISTANT_USERNAME, HOME_ASSISTANT_PASSWORD); + Serial.println("[STATUS] - HA Configured - Will attempt to connect to MQTT broker"); + + if (OLED_ENABLED) + { + String configuredHomeAssistantMessage[2] = {"Configured for", "Home Assistant"}; + drawMultiLineText(configuredHomeAssistantMessage); + delay(1500); + } + } + if (OLED_ENABLED) { display.clearDisplay(); @@ -834,7 +1297,10 @@ void setup() drawNotification("Connected to WiFi"); } + drawNotification("Getting time..."); getTime(); + + drawNotification("Starting webserver..."); startWebserver(); if (strcmp(userDefinedSettings.status.c_str(), "Winding") == 0) @@ -949,6 +1415,7 @@ void loop() if (OLED_ENABLED && !screenSleep) { drawNotification("Winding Complete"); + if (HOME_ASSISTANT_ENABLED) ha_activityState.setValue("Winding Complete"); } bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); @@ -971,5 +1438,15 @@ void loop() drawDynamicGUI(); } + if (HOME_ASSISTANT_ENABLED) + { + mqtt.loop(); + // We report these every cycle as if the device's MQTT connection is dropped, + // it will not be able to report its up-to-date state to Home Assistant. + // This mitigates de-sync between HA and the web gui. + ha_powerSwitch.setState(userDefinedSettings.winderEnabled.toInt()); + ha_activityState.setValue(userDefinedSettings.status.c_str()); + } + wm.process(); }