├── .gitignore ├── LICENSE ├── Seville.h ├── config.h.example ├── Seville.cpp ├── README.md └── Seville-MQTT.ino /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | config.h 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Robbie Trencheny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Seville.h: -------------------------------------------------------------------------------- 1 | // Seville Classics Ultra Slimline Fan 2 | // 3 | // Copyright 2019 Robbie Trencheny (@robbie) 4 | 5 | #ifndef SEVILLE_H_ 6 | #define SEVILLE_H_ 7 | 8 | #define __STDC_LIMIT_MACROS 9 | #include 10 | #ifndef UNIT_TEST 11 | #include 12 | #else 13 | #include 14 | #endif 15 | #include "IRremoteESP8266.h" 16 | #include "IRsend.h" 17 | 18 | // Constants 19 | const uint16_t kSevilleBits = 64; 20 | const uint16_t kSevilleDefaultRepeat = kNoRepeat; 21 | const uint16_t kSevilleStateLength = 8; 22 | 23 | const uint16_t kSevilleHdrMark = 9350; 24 | const uint16_t kSevilleHdrSpace = 4500; 25 | const uint16_t kSevilleBitMark = 590; 26 | const uint16_t kSevilleOneSpace = 1671; 27 | const uint16_t kSevilleZeroSpace = 546; 28 | const uint16_t kSevilleMsgSpace = 10000; 29 | 30 | // Digit 2, Byte 1 - Power 31 | const uint8_t kSevillePowerOn = 0x11; 32 | const uint8_t kSevillePowerOff = 0x10; 33 | 34 | // Digit 4, Byte 3 - Timer 35 | const uint8_t kSevilleTimerNone = 0x20; 36 | const uint8_t kSevilleTimerHalfHour = 0x21; 37 | const uint8_t kSevilleTimerHour = 0x22; 38 | const uint8_t kSevilleTimerHourAndAHalfHours = 0x23; 39 | const uint8_t kSevilleTimerTwoHours = 0x24; 40 | const uint8_t kSevilleTimerTwoAndAHalfHours = 0x25; 41 | const uint8_t kSevilleTimerThreeHours = 0x26; 42 | const uint8_t kSevilleTimerThreeAndAHalfHours = 0x27; 43 | const uint8_t kSevilleTimerFourHours = 0x28; 44 | const uint8_t kSevilleTimerFourAndAHalfHours = 0x29; 45 | const uint8_t kSevilleTimerFiveHours = 0x2A; 46 | const uint8_t kSevilleTimerFiveAndAHalfHours = 0x2B; 47 | const uint8_t kSevilleTimerSixHours = 0x2C; 48 | const uint8_t kSevilleTimerSixAndAHalfHours = 0x2D; 49 | const uint8_t kSevilleTimerSevenHours = 0x2E; 50 | const uint8_t kSevilleTimerSevenAndAHalfHours = 0x2F; 51 | 52 | // Digit 6, Byte 5 - Oscillation 53 | const uint8_t kSevilleOscillationOn = 0x31; 54 | const uint8_t kSevilleOscillationOff = 0x30; 55 | 56 | // Digit 8, Byte 7 - Speed 57 | const uint8_t kSevilleSpeedEco = 0x43; 58 | const uint8_t kSevilleSpeedLow = 0x40; 59 | const uint8_t kSevilleSpeedMedium = 0x41; 60 | const uint8_t kSevilleSpeedHigh = 0x42; 61 | 62 | // Digit 10, Byte 9 - "Wind" mode 63 | const uint8_t kSevilleWindNormal = 0x50; 64 | const uint8_t kSevilleWindNatural = 0x51; 65 | const uint8_t kSevilleWindSleeping = 0x52; 66 | 67 | class IRSevilleFan { 68 | public: 69 | explicit IRSevilleFan(uint16_t pin); 70 | 71 | void send(const uint16_t repeat = kSevilleDefaultRepeat); 72 | 73 | void begin(); 74 | 75 | void setPower(bool state); 76 | bool getPower(); 77 | char* getPowerString(); 78 | 79 | void setTimer(uint8_t timer); 80 | uint8_t getTimer(); 81 | char* getTimerString(); 82 | 83 | void setOscillation(bool osc); 84 | bool getOscillation(); 85 | char* getOscillationString(); 86 | 87 | void setSpeed(uint8_t speed); 88 | uint8_t getSpeed(); 89 | char* getSpeedString(); 90 | 91 | void setWind(uint8_t wind); 92 | uint8_t getWind(); 93 | char* getWindString(); 94 | 95 | uint8_t* getRaw(); 96 | 97 | void reset(); 98 | 99 | private: 100 | uint8_t remote_state[kSevilleStateLength]; 101 | void checksum(); 102 | IRsend _irsend; 103 | void sendSeville(uint64_t data, uint16_t nbits = kSevilleBits, 104 | uint16_t repeat = kSevilleDefaultRepeat); 105 | void sendSeville(unsigned char data[], uint16_t nbytes = kSevilleStateLength, 106 | uint16_t repeat = kSevilleDefaultRepeat); 107 | }; 108 | 109 | #endif // SEVILLE_H_ 110 | -------------------------------------------------------------------------------- /config.h.example: -------------------------------------------------------------------------------- 1 | // WiFi 2 | #define WIFI_SSID "" 3 | #define WIFI_PASSWORD "" 4 | 5 | // MQTT. Leave username/password blank if none 6 | #define MQTT_SERVER "192.168.1.2" 7 | #define MQTT_PORT 1883 8 | #define MQTT_USER "" 9 | #define MQTT_PASS "" 10 | #define MQTT_CLIENT_ID "ESP8266Client" 11 | #define MQTT_QOS 0 12 | 13 | // Topics 14 | #define ALIVE_TOPIC "esp8266/fan/status" 15 | 16 | #define ON_STATE_TOPIC "esp8266/fan/on/state" 17 | #define ON_SET_TOPIC "esp8266/fan/on/set" 18 | 19 | #define OSCILLATE_STATE_TOPIC "esp8266/fan/oscillate/state" 20 | #define OSCILLATE_SET_TOPIC "esp8266/fan/oscillate/set" 21 | 22 | #define SPEED_STATE_TOPIC "esp8266/fan/speed/state" 23 | #define SPEED_SET_TOPIC "esp8266/fan/speed/set" 24 | 25 | #define TIMER_STATE_TOPIC "esp8266/fan/timer/state" 26 | #define TIMER_SET_TOPIC "esp8266/fan/timer/set" 27 | 28 | #define WIND_STATE_TOPIC "esp8266/fan/wind/state" 29 | #define WIND_SET_TOPIC "esp8266/fan/wind/set" 30 | 31 | #define HOME_ASSISTANT_ATTRIBUTES_TOPIC "esp8266/fan/attributes" 32 | #define HOME_ASSISTANT_MQTT_DISCOVERY_TOPIC "homeassistant/fan/seville/config" 33 | #define HOME_ASSISTANT_DISCOVERY_NAME "Seville Classics UltraSlimline Tower Fan" 34 | 35 | // Payloads 36 | #define ONLINE_PAYLOAD "online" 37 | #define OFFLINE_PAYLOAD "offline" 38 | 39 | #define POWER_ON_PAYLOAD "ON" 40 | #define POWER_OFF_PAYLOAD "OFF" 41 | 42 | #define OSCILLATION_ON_PAYLOAD "ON" 43 | #define OSCILLATION_OFF_PAYLOAD "OFF" 44 | 45 | #define SPEED_OFF_PAYLOAD "off" 46 | #define SPEED_ECO_PAYLOAD "eco" 47 | #define SPEED_LOW_PAYLOAD "low" 48 | #define SPEED_MEDIUM_PAYLOAD "medium" 49 | #define SPEED_HIGH_PAYLOAD "high" 50 | 51 | #define WIND_NORMAL_PAYLOAD "normal" 52 | #define WIND_SLEEPING_PAYLOAD "sleeping" 53 | #define WIND_NATURAL_PAYLOAD "natural" 54 | 55 | #define TIMER_NONE_PAYLOAD "0:00" 56 | #define TIMER_HALF_HOUR_PAYLOAD "0:30" 57 | #define TIMER_HOUR_PAYLOAD "1:00" 58 | #define TIMER_HOUR_AND_A_HALF_HOURS_PAYLOAD "1:30" 59 | #define TIMER_TWO_HOURS_PAYLOAD "2:00" 60 | #define TIMER_TWO_AND_A_HALF_HOURS_PAYLOAD "2:30" 61 | #define TIMER_THREE_HOURS_PAYLOAD "3:00" 62 | #define TIMER_THREE_AND_A_HALF_HOURS_PAYLOAD "3:30" 63 | #define TIMER_FOUR_HOURS_PAYLOAD "4:00" 64 | #define TIMER_FOUR_AND_A_HALF_HOURS_PAYLOAD "4:30" 65 | #define TIMER_FIVE_HOURS_PAYLOAD "5:00" 66 | #define TIMER_FIVE_AND_A_HALF_HOURS_PAYLOAD "5:30" 67 | #define TIMER_SIX_HOURS_PAYLOAD "6:00" 68 | #define TIMER_SIX_AND_A_HALF_HOURS_PAYLOAD "6:30" 69 | #define TIMER_SEVEN_HOURS_PAYLOAD "7:00" 70 | #define TIMER_SEVEN_AND_A_HALF_HOURS_PAYLOAD "7:30" 71 | 72 | // Pin configurations 73 | #define IR_PIN 14 // NodeMCU D5 74 | 75 | #define RED_LED 16 // NodeMCU D0 76 | #define BLUE_LED 2 // NodeMCU D4 77 | 78 | // RED_LED breathing setting 79 | #define BRIGHT 350 // max led intensity (1-500) 80 | #define INHALE 1250 // Inhalation time in milliseconds. 81 | #define PULSE INHALE*1000/BRIGHT 82 | #define REST 1000 // Rest Between Inhalations. 83 | -------------------------------------------------------------------------------- /Seville.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Robbie Trencheny (@robbie) 2 | // 3 | // Code to control Seville Classics Ultra Slimline Fan. 4 | 5 | #include "Seville.h" 6 | #include 7 | #ifndef ARDUINO 8 | #include 9 | #endif 10 | #include "IRremoteESP8266.h" 11 | #include "IRsend.h" 12 | #include "IRutils.h" 13 | 14 | IRSevilleFan::IRSevilleFan(uint16_t pin) : _irsend(pin) { reset(); } 15 | 16 | void IRSevilleFan::begin() { _irsend.begin(); } 17 | 18 | void IRSevilleFan::send(const uint16_t repeat) { 19 | checksum(); 20 | sendSeville(remote_state); 21 | } 22 | 23 | void IRSevilleFan::checksum() { 24 | uint8_t checksum = 0; 25 | for (uint8_t i = 0; i < kSevilleStateLength - 1; i++) checksum ^= remote_state[i]; 26 | remote_state[7] = checksum; 27 | } 28 | 29 | void IRSevilleFan::reset() { 30 | for (uint8_t i = 0; i < kSevilleStateLength; i++) remote_state[i] = 0x00; 31 | 32 | remote_state[5] = 0x00; 33 | remote_state[6] = 0x60; 34 | remote_state[7] = 0x70; 35 | 36 | setPower(false); 37 | setSpeed(kSevilleSpeedEco); 38 | setOscillation(false); 39 | setTimer(kSevilleTimerNone); 40 | setWind(kSevilleWindNormal); 41 | } 42 | 43 | uint8_t* IRSevilleFan::getRaw() { 44 | checksum(); 45 | return remote_state; 46 | } 47 | 48 | void IRSevilleFan::setPower(bool state) { 49 | if (state) 50 | remote_state[0] = kSevillePowerOn; 51 | else 52 | remote_state[0] = kSevillePowerOff; 53 | } 54 | 55 | bool IRSevilleFan::getPower() { 56 | return remote_state[0] == kSevillePowerOn; 57 | } 58 | 59 | char* IRSevilleFan::getPowerString() { 60 | if(getPower()) { 61 | return "on"; 62 | } 63 | return "off"; 64 | } 65 | 66 | void IRSevilleFan::setTimer(uint8_t timer) { 67 | if (timer > kSevilleTimerSevenAndAHalfHours) timer = kSevilleTimerSevenAndAHalfHours; 68 | 69 | if (timer) { 70 | remote_state[1] = timer; 71 | } else { 72 | remote_state[1] = kSevilleTimerNone; 73 | } 74 | } 75 | 76 | uint8_t IRSevilleFan::getTimer() { return remote_state[1]; } 77 | 78 | char* IRSevilleFan::getTimerString() { 79 | switch (remote_state[1]) { 80 | case kSevilleTimerNone: 81 | return "0:00"; 82 | case kSevilleTimerHalfHour: 83 | return "0:30"; 84 | case kSevilleTimerHour: 85 | return "1:00"; 86 | case kSevilleTimerHourAndAHalfHours: 87 | return "1:30"; 88 | case kSevilleTimerTwoHours: 89 | return "2:00"; 90 | case kSevilleTimerTwoAndAHalfHours: 91 | return "2:30"; 92 | case kSevilleTimerThreeHours: 93 | return "3:00"; 94 | case kSevilleTimerThreeAndAHalfHours: 95 | return "3:30"; 96 | case kSevilleTimerFourHours: 97 | return "4:00"; 98 | case kSevilleTimerFourAndAHalfHours: 99 | return "4:30"; 100 | case kSevilleTimerFiveHours: 101 | return "5:00"; 102 | case kSevilleTimerFiveAndAHalfHours: 103 | return "5:30"; 104 | case kSevilleTimerSixHours: 105 | return "6:00"; 106 | case kSevilleTimerSixAndAHalfHours: 107 | return "6:30"; 108 | case kSevilleTimerSevenHours: 109 | return "7:00"; 110 | case kSevilleTimerSevenAndAHalfHours: 111 | return "7:30"; 112 | default: 113 | return "unknown"; 114 | } 115 | } 116 | 117 | void IRSevilleFan::setOscillation(bool osc) { 118 | if (osc) 119 | remote_state[2] = kSevilleOscillationOn; 120 | else 121 | remote_state[2] = kSevilleOscillationOff; 122 | } 123 | 124 | bool IRSevilleFan::getOscillation() { 125 | return remote_state[2] == kSevilleOscillationOn; 126 | } 127 | 128 | char* IRSevilleFan::getOscillationString() { 129 | if(getOscillation()) { 130 | return "on"; 131 | } 132 | return "off"; 133 | } 134 | 135 | void IRSevilleFan::setSpeed(uint8_t speed) { 136 | if (speed) 137 | remote_state[3] = speed; 138 | else 139 | remote_state[3] = kSevilleSpeedEco; 140 | } 141 | 142 | uint8_t IRSevilleFan::getSpeed() { return remote_state[3]; } 143 | 144 | char* IRSevilleFan::getSpeedString() { 145 | switch (remote_state[3]) { 146 | case kSevilleSpeedEco: 147 | return "eco"; 148 | case kSevilleSpeedLow: 149 | return "low"; 150 | case kSevilleSpeedMedium: 151 | return "medium"; 152 | case kSevilleSpeedHigh: 153 | return "high"; 154 | default: 155 | return "unknown"; 156 | } 157 | } 158 | 159 | void IRSevilleFan::setWind(uint8_t wind) { 160 | if (wind) 161 | remote_state[4] = wind; 162 | else 163 | remote_state[4] = kSevilleWindNormal; 164 | } 165 | 166 | uint8_t IRSevilleFan::getWind() { return remote_state[4]; } 167 | 168 | char* IRSevilleFan::getWindString() { 169 | switch (remote_state[4]) { 170 | case kSevilleWindNormal: 171 | return "normal"; 172 | case kSevilleWindNatural: 173 | return "natural"; 174 | case kSevilleWindSleeping: 175 | return "sleeping"; 176 | default: 177 | return "unknown"; 178 | } 179 | } 180 | 181 | // Send a Seville Classics message. 182 | // 183 | // Args: 184 | // data: The raw message to be sent. 185 | // nbytes: Nr. of bytes in the array. (Default is kSevilleStateLength) 186 | // repeat: Nr. of times the message is to be repeated. (Default = 0). 187 | // 188 | // Status: ALPHA / Untested. 189 | void IRSevilleFan::sendSeville(unsigned char data[], uint16_t nbytes, uint16_t repeat) { 190 | if (nbytes != kSevilleStateLength) 191 | return; // Wrong nr. of bits to send a proper message. 192 | // Set IR carrier frequency 193 | 194 | _irsend.sendGeneric(kSevilleHdrMark, kSevilleHdrSpace, 195 | kSevilleBitMark, kSevilleOneSpace, 196 | kSevilleBitMark, kSevilleZeroSpace, 197 | kSevilleBitMark, kSevilleMsgSpace, 198 | data, nbytes, 199 | 38, true, 200 | repeat, 50); 201 | } 202 | 203 | void IRSevilleFan::sendSeville(uint64_t data, uint16_t nbits, uint16_t repeat) { 204 | if (nbits != kSevilleBits) 205 | return; // Wrong nr. of bits to send a proper message. 206 | 207 | // Set IR carrier frequency 208 | _irsend.enableIROut(38, 50); 209 | for (uint16_t r = 0; r <= repeat; r++) { 210 | // Header 211 | _irsend.mark(kSevilleHdrMark); 212 | _irsend.space(kSevilleHdrSpace); 213 | 214 | _irsend.sendData(kSevilleBitMark, kSevilleOneSpace, kSevilleBitMark, kSevilleZeroSpace, data, nbits, true); 215 | 216 | _irsend.mark(kSevilleBitMark); 217 | _irsend.space(kSevilleMsgSpace); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Seville-MQTT 2 | ============================== 3 | 4 | A sketch for the NodeMCU/ESP8266 which allows controlling a Seville Classics UltraSlimline 40-inch Tower Fan via MQTT. 5 | 6 | # Prerequisites 7 | 1. NodeMCU or ESP8266. 8 | 2. An IR emitter LED. 9 | 3. NodeMCU needs to be configured in the Arduino IDE. Detailed instructions are available [here](https://github.com/esp8266/Arduino#installing-with-boards-manager). 10 | 11 | # Setup 12 | 1. Connect an IR LED to a pin on NodeMCU. The default configuration uses GPIO pin 14 (D5 on NodeMCU) because it is conveniently located right next to a ground. 13 | 2. Copy `config.h.example` to `config.h`. 14 | 3. Change `config.h` to match your settings. 15 | 4. Ensure all required libraries are loaded in the Arduino IDE: 16 | - [ArduinoJSON](https://github.com/bblanchon/ArduinoJson) 17 | - [ArduinoOTA](https://github.com/esp8266/Arduino/tree/master/libraries/ArduinoOTA) 18 | - [IRemoteESP8266](https://github.com/markszabo/IRremoteESP8266) 19 | - [PubSubClient](https://github.com/knolleary/pubsubclient) 20 | 5. Load the sketch. 21 | 6. When the sketch starts up it will set the fan to eco speed and turn it off to ensure that the fan is in a clean state. 22 | 23 | # Notes 24 | - The red LED flashes everytime a message is sent via MQTT. 25 | - The red LED will stay solid if the board is disconnected from wifi. 26 | - The blue LED will blink anytime IR data is emitted. 27 | - ArduinoOTA is enabled allowing you to upload new versions without needing to plug into a computer. See [the docs](http://esp8266.github.io/Arduino/versions/2.0.0/doc/ota_updates/ota_updates.html#arduino-ide) for more information. 28 | 29 | # Topics 30 | | Name | Default State Topic | Default Command Topic | Defined as | Accepts | Description | 31 | |-----------|------------------------------|----------------------------|------------------------------------------------|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| 32 | | Alive | `esp8266/ac/status` | N/A | `ALIVE_TOPIC` | N/A | Contains the current status of the board. `alive` if it's online, otherwise `dead`. | 33 | | Power | `esp8266/ac/on/state` | `esp8266/ac/on/set` | `ON_STATE_TOPIC`, `ON_SET_TOPIC` | Boolean | Turns the fan on/off | 34 | | Oscillate | `esp8266/ac/oscillate/state` | `esp8266/ac/oscillate/set` | `OSCILLATE_STATE_TOPIC`, `OSCILLATE_SET_TOPIC` | Boolean | Turns oscillation on/off | 35 | | Speed | `esp8266/ac/speed/state` | `esp8266/ac/speed/set` | `SPEED_STATE_TOPIC`, `SPEED_SET_TOPIC` | `eco`, `low`, `medium`, `high` | Sets the fan speed | 36 | | Timer | `esp8266/ac/timer/state` | `esp8266/ac/timer/set` | `TIMER_STATE_TOPIC`, `TIMER_SET_TOPIC` | `00:30` to `07:30` (hh:mm) | Sets the timer | 37 | | Wind Mode | `esp8266/ac/wind/state` | `esp8266/ac/wind/set` | `WIND_STATE_TOPIC`, `WIND_SET_TOPIC` | `normal`, `sleeping`, `natural` | Sets the "wind mode" | 38 | 39 | # Home Assistant Example Configuration 40 | ```yaml 41 | mqtt: 42 | host: 192.168.1.2 43 | username: username 44 | password: password 45 | discovery: true 46 | 47 | fan: 48 | - platform: mqtt 49 | name: "Seville Fan" 50 | availability_topic: "esp8266/fan/status" 51 | command_topic: "esp8266/fan/on/set" 52 | json_attributes_topic: "esp8266/fan/attributes" 53 | oscillation_command_topic: "esp8266/fan/oscillate/set" 54 | oscillation_state_topic: "esp8266/fan/oscillate/state" 55 | speeds: 56 | - 'off' 57 | - eco 58 | - low 59 | - medium 60 | - high 61 | speed_command_topic: "esp8266/fan/speed/set" 62 | speed_state_topic: "esp8266/fan/speed/state" 63 | state_topic: "esp8266/fan/on/state" 64 | 65 | input_select: 66 | seville_fan_timer: 67 | name: "Seville Fan - Timer" 68 | icon: mdi:timer 69 | initial: '0:00' 70 | options: 71 | - '0:00' 72 | - '0:30' 73 | - '1:00' 74 | - '1:30' 75 | - '2:00' 76 | - '2:30' 77 | - '3:00' 78 | - '3:30' 79 | - '4:00' 80 | - '4:30' 81 | - '5:00' 82 | - '5:30' 83 | - '6:00' 84 | - '6:30' 85 | - '7:00' 86 | - '7:30' 87 | seville_fan_wind: 88 | name: "Seville Fan - Wind Mode" 89 | icon: mdi:weather-windy 90 | initial: 'Normal' 91 | options: 92 | - 'Normal' 93 | - 'Sleeping' 94 | - 'Natural' 95 | 96 | automation: 97 | - alias: Set Fan Wind Mode 98 | hide_entity: True 99 | trigger: 100 | platform: state 101 | entity_id: input_select.seville_fan_wind 102 | action: 103 | - service: mqtt.publish 104 | data_template: 105 | topic: "esp8266/fan/wind/set" 106 | payload_template: '{{ states.input_select.seville_fan_wind.state }}' 107 | 108 | - alias: Set Fan Timer 109 | hide_entity: True 110 | trigger: 111 | platform: state 112 | entity_id: input_select.seville_fan_timer 113 | action: 114 | - service: mqtt.publish 115 | data_template: 116 | topic: "esp8266/fan/timer/set" 117 | payload_template: '{{ states.input_select.seville_fan_timer.state }}' 118 | ``` 119 | 120 | # Debugging 121 | Serial outputs at 115200 baud. You can view the output in the Arduino IDE Serial Monitor. 122 | 123 | # Acknowledgements 124 | - This sketch is based on work from [@zeroflow's](https://github.com/zeroflow) [ESPAircon](https://github.com/zeroflow/ESPAircon). 125 | - Initial confirmation that the IR codes I was seeing were correct came from [@scruss's](https://github.com/scruss) [IRTowerFanExample](https://github.com/scruss/IRTowerFanExample). 126 | - I got some great ideas from [@balloob's](https://github.com/balloob) [sketch he made to control his AC unit](https://gist.github.com/balloob/daf310faa80112817d6826fbe5fc399d). 127 | 128 | # Contributing 129 | Fork, modify, pull request. 130 | 131 | # LICENSE 132 | MIT 133 | 134 | # Protocol notes 135 | The IRemoteESP8266 library thinks that the Seville remote is sending NEC codes but whenever I attempted to send NEC codes back they wouldn't work. 136 | 137 | The only way I could get this working was to send "raw" IR codes. Quite annoying, since the sketch gets quite a bit bigger with the raw data. 138 | 139 | The below notes are from when I was initially documenting the NEC code structure. 140 | 141 | [@scruss's](https://github.com/scruss) [IRTowerFanExample](https://github.com/scruss/IRTowerFanExample) has already defined most of these codes, but his work was dated so I need to reconfirm. 142 | 143 | You can view a full list of his codes (more complete then what I have below) [here](https://github.com/scruss/IRTowerFanExample/blob/master/IRTowerFan.h). 144 | 145 | ``` 146 | 1st digit - Prefix bit (1) 147 | 2nd digit - Power Off/On (0/1) 148 | 3rd digit - Always 2? 149 | 4th digit - Timer in 0.5 hour increments from 0.5 to 7.5 (1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F) 150 | 5th digit - Always 3? 151 | 6th digit - Oscillate Off/On (0/1) 152 | 7th digit - Always 4? 153 | 8th digit - Speed (Eco: 3, Low: 0, Medium: 1, High: 2) 154 | 155 | Wind is supposedly sending the same speed command twice. 156 | 157 | 10203043 - Off, eco, no wind, no timer, no oscillate 158 | 11203043 - On, eco, no wind, no timer, no oscillate 159 | 11203040 - On, low, no wind, no timer, no oscillate 160 | 11203041 - On, med, no wind, no timer, no oscillate 161 | 11203042 - On, hih, no wind, no timer, no oscillate 162 | 11203143 - On, eco, no wind, no timer, oscillate 163 | 11203140 - On, low, no wind, no timer, oscillate 164 | 11203141 - On, med, no wind, no timer, oscillate 165 | 11203142 - On, hih, no wind, no timer, oscillate 166 | 11213043 - On, eco, no wind, 0.5 timer, no oscillate 167 | 11223043 - On, eco, no wind, 1.0 timer, no oscillate 168 | 11233043 - On, eco, no wind, 1.5 timer, no oscillate 169 | 11243043 - On, eco, no wind, 2.0 timer, no oscillate 170 | 11253043 - On, eco, no wind, 2.5 timer, no oscillate 171 | 11263043 - On, eco, no wind, 3.0 timer, no oscillate 172 | 11273043 - On, eco, no wind, 3.5 timer, no oscillate 173 | 11283043 - On, eco, no wind, 4.0 timer, no oscillate 174 | 11293043 - On, eco, no wind, 4.5 timer, no oscillate 175 | 112A3043 - On, eco, no wind, 5.0 timer, no oscillate 176 | 112B3043 - On, eco, no wind, 5.5 timer, no oscillate 177 | 112C3043 - On, eco, no wind, 6.0 timer, no oscillate 178 | 112D3043 - On, eco, no wind, 6.5 timer, no oscillate 179 | 112E3043 - On, eco, no wind, 7.0 timer, no oscillate 180 | 112F3043 - On, eco, no wind, 7.5 timer, no oscillate 181 | ``` 182 | -------------------------------------------------------------------------------- /Seville-MQTT.ino: -------------------------------------------------------------------------------- 1 | // Sketch to control a Seville Classics UltraSlimline 40-inch Tower Fan 2 | // Author: Robbie Trencheny 3 | // https://github.com/robbiet480/Seville-MQTT 4 | // License: MIT 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "config.h" 14 | #include "Seville.h" 15 | 16 | #define MQTT_MAX_PACKET_SIZE 768 17 | 18 | IRSevilleFan fan(IR_PIN); 19 | 20 | void mqttCallback(char* topic, byte* payload, unsigned int length); 21 | 22 | WiFiClient espClient; 23 | PubSubClient mqttClient(MQTT_SERVER, MQTT_PORT, mqttCallback, espClient); 24 | 25 | char hostname[20]; 26 | char chipid[20]; 27 | char localIP[16]; 28 | int startupCompleted = 0; 29 | 30 | unsigned long sendTimer; 31 | bool waitingToSend = false; 32 | 33 | bool are_equal(char* a, char* b) { 34 | return strcmp(a, b)==0; 35 | } 36 | 37 | bool are_equal(byte* a, char* b) { 38 | return are_equal((char *)a, b); 39 | } 40 | 41 | void mqttCallback(char* topic, byte* payload, unsigned int length) { 42 | // Since messages are retained, this logic skips the callback 43 | // for those until all have been processed. 44 | if(startupCompleted < 5) { 45 | startupCompleted += 1; 46 | return; 47 | } 48 | 49 | payload[length] = '\0'; 50 | 51 | // In order to republish this payload, a copy must be made 52 | // as the orignal payload buffer will be overwritten whilst 53 | // constructing the PUBLISH packet. 54 | 55 | // Allocate the correct amount of memory for the payload copy 56 | byte* copied_payload = (byte*)malloc(length); 57 | // Copy the payload to the new buffer 58 | memcpy(copied_payload, payload, length); 59 | 60 | Serial.printf("Message arrived [%s]: %s\n", topic, payload); 61 | 62 | char publishing_topic[64]; 63 | 64 | if (are_equal(topic, ON_SET_TOPIC)) { 65 | bool on = are_equal(payload, POWER_ON_PAYLOAD); 66 | fan.setPower(on); 67 | strcpy(publishing_topic, ON_STATE_TOPIC); 68 | mqttClient.publish(SPEED_STATE_TOPIC, mapSpeedVal(), true); 69 | } else if (are_equal(topic, OSCILLATE_SET_TOPIC)) { 70 | bool oscillate = are_equal(payload, OSCILLATION_ON_PAYLOAD); 71 | fan.setOscillation(oscillate); 72 | strcpy(publishing_topic, OSCILLATE_STATE_TOPIC); 73 | } else if (are_equal(topic, SPEED_SET_TOPIC)) { 74 | if (are_equal(payload, SPEED_OFF_PAYLOAD)) { 75 | fan.setPower(false); 76 | mqttClient.publish(ON_STATE_TOPIC, POWER_OFF_PAYLOAD, true); 77 | } else if (are_equal(payload, SPEED_ECO_PAYLOAD)) { 78 | fan.setSpeed(kSevilleSpeedEco); 79 | } else if (are_equal(payload, SPEED_LOW_PAYLOAD)) { 80 | fan.setSpeed(kSevilleSpeedLow); 81 | } else if (are_equal(payload, SPEED_MEDIUM_PAYLOAD)) { 82 | fan.setSpeed(kSevilleSpeedMedium); 83 | } else if (are_equal(payload, SPEED_HIGH_PAYLOAD)) { 84 | fan.setSpeed(kSevilleSpeedHigh); 85 | } else { 86 | Serial.printf("Unknown speed value: %s\n!", payload); 87 | return; 88 | } 89 | strcpy(publishing_topic, SPEED_STATE_TOPIC); 90 | } else if (are_equal(topic, WIND_SET_TOPIC)) { 91 | if (are_equal(payload, WIND_NORMAL_PAYLOAD)) { 92 | fan.setWind(kSevilleWindNormal); 93 | } else if (are_equal(payload, WIND_SLEEPING_PAYLOAD)) { 94 | fan.setWind(kSevilleWindSleeping); 95 | } else if (are_equal(payload, WIND_NATURAL_PAYLOAD)) { 96 | fan.setWind(kSevilleWindNatural); 97 | } else { 98 | Serial.printf("Unknown wind value: %s\n!", payload); 99 | return; 100 | } 101 | strcpy(publishing_topic, WIND_STATE_TOPIC); 102 | } else if (are_equal(topic, TIMER_SET_TOPIC)) { 103 | if (are_equal(payload, TIMER_NONE_PAYLOAD)) { 104 | fan.setTimer(kSevilleTimerNone); 105 | } else if (are_equal(payload, TIMER_HALF_HOUR_PAYLOAD)) { 106 | fan.setTimer(kSevilleTimerHalfHour); 107 | } else if (are_equal(payload, TIMER_HOUR_PAYLOAD)) { 108 | fan.setTimer(kSevilleTimerHour); 109 | } else if (are_equal(payload, TIMER_HOUR_AND_A_HALF_HOURS_PAYLOAD)) { 110 | fan.setTimer(kSevilleTimerHourAndAHalfHours); 111 | } else if (are_equal(payload, TIMER_TWO_HOURS_PAYLOAD)) { 112 | fan.setTimer(kSevilleTimerTwoHours); 113 | } else if (are_equal(payload, TIMER_TWO_AND_A_HALF_HOURS_PAYLOAD)) { 114 | fan.setTimer(kSevilleTimerTwoAndAHalfHours); 115 | } else if (are_equal(payload, TIMER_THREE_HOURS_PAYLOAD)) { 116 | fan.setTimer(kSevilleTimerThreeHours); 117 | } else if (are_equal(payload, TIMER_THREE_AND_A_HALF_HOURS_PAYLOAD)) { 118 | fan.setTimer(kSevilleTimerThreeAndAHalfHours); 119 | } else if (are_equal(payload, TIMER_FOUR_HOURS_PAYLOAD)) { 120 | fan.setTimer(kSevilleTimerFourHours); 121 | } else if (are_equal(payload, TIMER_FOUR_AND_A_HALF_HOURS_PAYLOAD)) { 122 | fan.setTimer(kSevilleTimerFourAndAHalfHours); 123 | } else if (are_equal(payload, TIMER_FIVE_HOURS_PAYLOAD)) { 124 | fan.setTimer(kSevilleTimerFiveHours); 125 | } else if (are_equal(payload, TIMER_FIVE_AND_A_HALF_HOURS_PAYLOAD)) { 126 | fan.setTimer(kSevilleTimerFiveAndAHalfHours); 127 | } else if (are_equal(payload, TIMER_SIX_HOURS_PAYLOAD)) { 128 | fan.setTimer(kSevilleTimerSixHours); 129 | } else if (are_equal(payload, TIMER_SIX_AND_A_HALF_HOURS_PAYLOAD)) { 130 | fan.setTimer(kSevilleTimerSixAndAHalfHours); 131 | } else if (are_equal(payload, TIMER_SEVEN_HOURS_PAYLOAD)) { 132 | fan.setTimer(kSevilleTimerSevenHours); 133 | } else if (are_equal(payload, TIMER_SEVEN_AND_A_HALF_HOURS_PAYLOAD)) { 134 | fan.setTimer(kSevilleTimerSevenAndAHalfHours); 135 | } else { 136 | Serial.printf("Unknown timer value: %s\n!", payload); 137 | return; 138 | } 139 | strcpy(publishing_topic, TIMER_STATE_TOPIC); 140 | } else { 141 | Serial.println("No topic matched!"); 142 | } 143 | 144 | sendTimer = millis(); 145 | waitingToSend = true; 146 | 147 | if (!are_equal(publishing_topic, "")) { 148 | digitalWrite(RED_LED, LOW); 149 | mqttClient.publish(publishing_topic, copied_payload, true); 150 | digitalWrite(RED_LED, HIGH); 151 | } 152 | 153 | free(copied_payload); 154 | } 155 | 156 | void setup() { 157 | Serial.begin(115200); 158 | Serial.println("Booting"); 159 | 160 | pinMode(IR_PIN, OUTPUT); 161 | 162 | pinMode(RED_LED, OUTPUT); 163 | pinMode(BLUE_LED, OUTPUT); 164 | 165 | sprintf(chipid, "%08X", ESP.getChipId()); 166 | sprintf(hostname, "Seville-MQTT-%08X", ESP.getChipId()); 167 | 168 | fan.begin(); 169 | 170 | setupWiFi(); 171 | 172 | ArduinoOTA.setHostname(hostname); 173 | ArduinoOTA.begin(); 174 | } 175 | 176 | void setupWiFi() { 177 | // Disable built in access point 178 | WiFi.mode(WIFI_STA); 179 | 180 | WiFi.hostname(hostname); 181 | 182 | // We start by connecting to a WiFi network 183 | Serial.println(); 184 | Serial.print("Connecting to "); 185 | Serial.println(WIFI_SSID); 186 | 187 | WiFi.begin(WIFI_SSID, WIFI_PASSWORD); 188 | 189 | while (WiFi.status() != WL_CONNECTED) { 190 | delay(100); 191 | Serial.print("."); 192 | } 193 | 194 | sprintf(localIP, "%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3] ); 195 | 196 | Serial.println(""); 197 | Serial.println("WiFi connected"); 198 | Serial.printf("IP address: %s\n", localIP); 199 | } 200 | 201 | void reconnect() { 202 | // Loop until we're reconnected 203 | while (!mqttClient.connected()) { 204 | Serial.print("Attempting MQTT connection..."); 205 | Serial.printf("state=%s\n", mqttClientState()); 206 | 207 | // Attempt to connect 208 | if (mqttClient.connect(hostname, MQTT_USER, MQTT_PASS, ALIVE_TOPIC, MQTT_QOS, 1, OFFLINE_PAYLOAD)) { 209 | Serial.printf("Connected to MQTT Broker (%s)\n", MQTT_SERVER); 210 | Serial.printf("MQTT connection state: %s\n", mqttClientState()); 211 | 212 | fan.reset(); 213 | fan.send(); 214 | 215 | waitingToSend = false; 216 | sendTimer = 0; 217 | 218 | // Set all the default values on the topics 219 | mqttClient.publish(ALIVE_TOPIC, ONLINE_PAYLOAD, true); 220 | 221 | mqttClient.publish(ON_SET_TOPIC, POWER_OFF_PAYLOAD, false); 222 | mqttClient.publish(OSCILLATE_SET_TOPIC, OSCILLATION_OFF_PAYLOAD, false); 223 | mqttClient.publish(SPEED_SET_TOPIC, SPEED_ECO_PAYLOAD, false); 224 | mqttClient.publish(TIMER_SET_TOPIC, TIMER_NONE_PAYLOAD, false); 225 | mqttClient.publish(WIND_SET_TOPIC, WIND_NORMAL_PAYLOAD, false); 226 | 227 | mqttClient.publish(ON_STATE_TOPIC, POWER_OFF_PAYLOAD, true); 228 | mqttClient.publish(OSCILLATE_STATE_TOPIC, OSCILLATION_OFF_PAYLOAD, true); 229 | mqttClient.publish(SPEED_STATE_TOPIC, SPEED_ECO_PAYLOAD, true); 230 | mqttClient.publish(TIMER_STATE_TOPIC, TIMER_NONE_PAYLOAD, true); 231 | mqttClient.publish(WIND_STATE_TOPIC, WIND_NORMAL_PAYLOAD, true); 232 | 233 | // Subscribe to all topics 234 | mqttClient.subscribe(ON_SET_TOPIC); 235 | mqttClient.subscribe(OSCILLATE_SET_TOPIC); 236 | mqttClient.subscribe(SPEED_SET_TOPIC); 237 | mqttClient.subscribe(TIMER_SET_TOPIC); 238 | mqttClient.subscribe(WIND_SET_TOPIC); 239 | 240 | publishAttributes(); 241 | publishDiscovery(); 242 | } else { 243 | Serial.printf("failed, rc=%s try again in 5 seconds\n", mqttClientState()); 244 | // Wait 5 seconds before retrying 245 | delay(5000); 246 | } 247 | } 248 | } 249 | 250 | void loop() { 251 | if (!mqttClient.connected()) { 252 | Serial.println("Disconnected from MQTT, starting reconnection!"); 253 | Serial.print("Current WiFi state is: "); 254 | Serial.println(WiFi.status()); 255 | 256 | // Breathing from https://arduining.com/2015/08/20/nodemcu-breathing-led-with-arduino-ide/ 257 | 258 | //ramp increasing intensity, Inhalation: 259 | for (int i=1;i0;i--){ 269 | digitalWrite(RED_LED, LOW); // turn the LED on. 270 | delayMicroseconds(i*10); // wait 271 | digitalWrite(RED_LED, HIGH); // turn the LED off. 272 | delayMicroseconds(PULSE-i*10); // wait 273 | i--; 274 | delay(0); // to prevent watchdog firing. 275 | } 276 | delay(REST); // take a rest... 277 | reconnect(); 278 | } 279 | if(waitingToSend && (millis()-sendTimer >= 250UL)) { 280 | waitingToSend = false; 281 | sendTimer = 0; 282 | Serial.println("Flushing pending commands to IR!"); 283 | digitalWrite(BLUE_LED, LOW); 284 | fan.send(); 285 | digitalWrite(BLUE_LED, HIGH); 286 | printState(); 287 | } 288 | ArduinoOTA.handle(); 289 | mqttClient.loop(); 290 | } 291 | 292 | void printState() { 293 | uint8_t* ir_code = fan.getRaw(); 294 | char ir_code_str[24]; 295 | sprintf(ir_code_str, "%02X %02X %02X %02X %02X %02X %02X %02X", 296 | ir_code[0], ir_code[1], ir_code[2], ir_code[3], ir_code[4], ir_code[5], ir_code[6], ir_code[7]); 297 | 298 | Serial.printf("New Fan State: Power: %s, Timer: %s, Oscillation: %s, Speed: %s, Wind: %s, IR Code: 0x %s\n", 299 | fan.getPowerString(), fan.getTimerString(), fan.getOscillationString(), 300 | fan.getSpeedString(), fan.getWindString(), ir_code_str); 301 | } 302 | 303 | void publishAttributes() { 304 | StaticJsonDocument<512> root; 305 | root["BSSID"] = WiFi.BSSIDstr(); 306 | root["Chip ID"] = chipid; 307 | root["Hostname"] = hostname; 308 | root["IP Address"] = localIP; 309 | root["MAC Address"] = WiFi.macAddress(); 310 | root["RSSI"] = WiFi.RSSI(); 311 | root["SSID"] = WiFi.SSID(); 312 | char outgoingJsonBuffer[512]; 313 | serializeJson(root, outgoingJsonBuffer); 314 | mqttClient.publish(HOME_ASSISTANT_ATTRIBUTES_TOPIC, outgoingJsonBuffer, true); 315 | } 316 | 317 | void publishDiscovery() { 318 | StaticJsonDocument<768> root; 319 | root["availability_topic"] = ALIVE_TOPIC; 320 | root["command_topic"] = ON_SET_TOPIC; 321 | root["json_attributes_topic"] = HOME_ASSISTANT_ATTRIBUTES_TOPIC; 322 | root["name"] = HOME_ASSISTANT_DISCOVERY_NAME; 323 | root["oscillation_command_topic"] = OSCILLATE_SET_TOPIC; 324 | root["oscillation_state_topic"] = OSCILLATE_STATE_TOPIC; 325 | root["payload_available"] = ONLINE_PAYLOAD; 326 | root["payload_high_speed"] = SPEED_HIGH_PAYLOAD; 327 | root["payload_low_speed"] = SPEED_LOW_PAYLOAD; 328 | root["payload_medium_speed"] = SPEED_MEDIUM_PAYLOAD; 329 | root["payload_not_available"] = OFFLINE_PAYLOAD; 330 | root["payload_off"] = POWER_OFF_PAYLOAD; 331 | root["payload_on"] = POWER_ON_PAYLOAD; 332 | root["payload_oscillation_off"] = OSCILLATION_OFF_PAYLOAD; 333 | root["payload_oscillation_on"] = OSCILLATION_ON_PAYLOAD; 334 | root["speed_command_topic"] = SPEED_SET_TOPIC; 335 | root["speed_state_topic"] = SPEED_STATE_TOPIC; 336 | root["state_topic"] = ON_STATE_TOPIC; 337 | root["unique_id"] = chipid; 338 | JsonArray speeds = root.createNestedArray("speeds"); 339 | speeds.add(SPEED_OFF_PAYLOAD); 340 | speeds.add(SPEED_ECO_PAYLOAD); 341 | speeds.add(SPEED_LOW_PAYLOAD); 342 | speeds.add(SPEED_MEDIUM_PAYLOAD); 343 | speeds.add(SPEED_HIGH_PAYLOAD); 344 | char outgoingJsonBuffer[768]; 345 | serializeJson(root, outgoingJsonBuffer); 346 | mqttClient.publish(HOME_ASSISTANT_MQTT_DISCOVERY_TOPIC, outgoingJsonBuffer, true); 347 | } 348 | 349 | char* mqttClientState() { 350 | switch(mqttClient.state()) { 351 | case MQTT_CONNECTION_TIMEOUT: 352 | return "Connection Timeout (code: -4)"; 353 | case MQTT_CONNECTION_LOST: 354 | return "Connection Lost (code: -3)"; 355 | case MQTT_CONNECT_FAILED: 356 | return "Connect Failed (code: -2)"; 357 | case MQTT_DISCONNECTED: 358 | return "Disconnected (code: -1)"; 359 | case MQTT_CONNECTED: 360 | return "Connected (code: 0)"; 361 | case MQTT_CONNECT_BAD_PROTOCOL: 362 | return "Connect Bad Protocol (code: 1)"; 363 | case MQTT_CONNECT_BAD_CLIENT_ID: 364 | return "Connect Bad Client Id (code: 2)"; 365 | case MQTT_CONNECT_UNAVAILABLE: 366 | return "Connect Unavailable (code: 3)"; 367 | case MQTT_CONNECT_BAD_CREDENTIALS: 368 | return "Connect Bad Credentials (code: 4)"; 369 | case MQTT_CONNECT_UNAUTHORIZED: 370 | return "Connect Unauthorized (code: 5)"; 371 | default: 372 | return "Unknown"; 373 | } 374 | } 375 | 376 | char* mapSpeedVal() { 377 | if(!fan.getPower()) { 378 | return SPEED_OFF_PAYLOAD; 379 | } 380 | switch(fan.getSpeed()) { 381 | case kSevilleSpeedEco: 382 | return SPEED_ECO_PAYLOAD; 383 | case kSevilleSpeedLow: 384 | return SPEED_LOW_PAYLOAD; 385 | case kSevilleSpeedMedium: 386 | return SPEED_MEDIUM_PAYLOAD; 387 | case kSevilleSpeedHigh: 388 | return SPEED_HIGH_PAYLOAD; 389 | } 390 | } 391 | --------------------------------------------------------------------------------