├── autoexec.be ├── assets ├── FS1000A.png ├── CC1101-Module.png ├── ESP32-D1-Mini.png ├── HA-Dashboard.png ├── HA-DevicePage.png ├── Tasmota-Main.png └── ESP32-MiniKit-Somfy-GPIOs.png ├── tasmota32-somfy.bin ├── tasmota32s3-somfy.bin ├── LICENSE ├── README.md └── RFtxSMFY_V2.be /autoexec.be: -------------------------------------------------------------------------------- 1 | load('RFtxSMFY_V2.be') -------------------------------------------------------------------------------- /assets/FS1000A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew01144/Tasmota-SomfyRTS/HEAD/assets/FS1000A.png -------------------------------------------------------------------------------- /tasmota32-somfy.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew01144/Tasmota-SomfyRTS/HEAD/tasmota32-somfy.bin -------------------------------------------------------------------------------- /tasmota32s3-somfy.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew01144/Tasmota-SomfyRTS/HEAD/tasmota32s3-somfy.bin -------------------------------------------------------------------------------- /assets/CC1101-Module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew01144/Tasmota-SomfyRTS/HEAD/assets/CC1101-Module.png -------------------------------------------------------------------------------- /assets/ESP32-D1-Mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew01144/Tasmota-SomfyRTS/HEAD/assets/ESP32-D1-Mini.png -------------------------------------------------------------------------------- /assets/HA-Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew01144/Tasmota-SomfyRTS/HEAD/assets/HA-Dashboard.png -------------------------------------------------------------------------------- /assets/HA-DevicePage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew01144/Tasmota-SomfyRTS/HEAD/assets/HA-DevicePage.png -------------------------------------------------------------------------------- /assets/Tasmota-Main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew01144/Tasmota-SomfyRTS/HEAD/assets/Tasmota-Main.png -------------------------------------------------------------------------------- /assets/ESP32-MiniKit-Somfy-GPIOs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew01144/Tasmota-SomfyRTS/HEAD/assets/ESP32-MiniKit-Somfy-GPIOs.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Andrew Russell 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tasmota-SomfyRTS 2 | A Berry script to to control Somfy powered blinds from Home Assistant using Tasmota. 3 | 4 | ## How it works 5 | - It emulates a Somfy RTS controller like a [Telis 4 RTS](https://www.somfy.co.uk/products/1810648/telis-4-soliris-rts) or [SITUO 5 RTS](https://www.somfy.co.uk/products/1870495/situo-5-rts-pure-color) using standard Tasmota features and components. Once setup, you can pair the Tasmota to the blind in the same way that you pair a Somfy controller to a blind. 6 | - We provide Tasmota with a Berry script that uses Tasmota's support for Shutters and Blinds to provide standard `cover` entities that Home Assistant can discover and control. 7 | - We use the `IRsend` component (rather than RFsend) to send the data because it allows us to send raw packets that we can construct in the Berry script. 8 | - Tasmota's support for Shutters and Blinds controls the blinds using relays, so we provide it with _virtual_ relays. The physical relays do not exist, but changes in their GPIOs can trigger Tasmota rules. When we see the Up relay operate, we will construct a Somfy Up command and send it using `IRsend`. 9 | - We use a custom build of the Tasmota firmware. 10 | - We need two standard Tasmota features, `Full IR Support` and `Shutters and Blinds`. However, none of the standard Tasmota binaries include these two features together. Furthermore, it is highly desirable to set `#define IR_SEND_USE_MODULATION 0` which is not available in the standard builds. No source code is changed. 11 | - Pre-built firmware files are provided on this project page. 12 | 13 | ## History of this project 14 | - The README.md has been simplified, focused on Home Assistant, and assumes familiarity with Tasmota. 15 | - Berry persistence fix: 16 | - v1.1.x worked with Tasmota v12.0.2, but broke some releases after that, see [arendst/Tasmota#22187](https://github.com/arendst/Tasmota/issues/22187). 17 | - Without persistence, a crash or power-cycle could cause the RollingCode to reset, which may then cause the Somfy blind to ignore further messages. Controlled restarts were fine. 18 | - This version (v1.2.x) uses persist.dirty(), which works from Tasmota v14.2.0, and possibly earlier releases. 19 | - Tasmota Shutters and Blinds integration: 20 | - This part of the code has been improved and tidied, but functionality is the same. 21 | 22 | ## Installation overview 23 | - Use a multi-core ESP32, such as the ESP32 or ESP32-S3 (Not the ESP32-S2 or ESP32-C3). 24 | - Flash the ESP32 with Tasmota and connect it to a CC1101 transmitter module with 7 wires. (For some applications, the simpler FS1000A transmitter module may be sufficient. See [this discussion](#using-a-simple-fs1000a-transmitter-module).) 25 | - Download the Tasmota ESP32 firmware from this project, and upgrade your Tasmota using this image. Or, [build your own firmware](#building-you-own-firmware). 26 | - Download the Berry files from this project and upload them to your Tasmota's filesystem. 27 | - Assign GPIOs for the transmitter. The CC1101 transmitter module will use four SSPI pins and one IRsend pin, as well as 3.3V and GND. You can choose any pins that are convenient. 28 | - Assign GPIOs for the _virtual_ Up/Down relays. 29 | - The GPIOs must be real GPIOs that appear on the Tasmota `Configure module` page, but do not need to be available as pins on the ESP32 module. 30 | - Configure the blinds in Tasmota. 31 | - Pair this 'virtual' Somfy controller to your Somfy blind. 32 | - Test and admire! 33 | 34 | ## Installation procedure 35 | - This example is for two blinds. We will need to configure 4 _virtual_ relays; an Up and a Down relay for each of the two blinds. 36 | 37 | ### Flash the ESP32 with Tasmota and discover the device in Home Assistant 38 | - Take your ESP32 or ESP32-S3 module and flash it with the default Tasmota firmware (not the firmware from this project). Configure it using your normal procedure and check that it appears in the list of Home Assistant devices. 39 | - Tasmota in Home Assistant requires an MQTT broker (such as HA's Mosquitto Addon) and the Tasmota integration. 40 | - For my installation, I configured the following items in Tasmota: 41 | - `Hostname tas-somfy`, `DeviceName tas-somfy`, `Topic tas-somfy`, `FriendlyName tas-somfy`, `MqttUser XXXX`, `MqttPassword XXXX`, `MqttHost 192.168.1.XXX`. 42 | 43 | ### Upgrade the Tasmota firmware 44 | - Download the tasmota.bin file appropriate to your ESP32 module from this project and upgrade your Tasmota with it. (Or, use [your own firmware](#building-you-own-firmware).) 45 | - `Tasmota` > `Firmware Upgrade` > `Choose file` > `Start upgrade`. 46 | 47 | ### Assign GPIOs 48 | - You will need to configure each of the GPIOs in the list below. The particular GPIO numbers you choose depend on the physical layout of the module and how you will wire it. These GPIO numbers work nicely for the ESP32-D1-Mini module. Skip the SSPI GPIOs if you are using an [FS1000A](#using-a-simple-fs1000a-transmitter-module) transmitter module. 49 | - `Tasmota` > `Configuration` > `Module`. 50 | 51 | ![GPIOs](assets/ESP32-MiniKit-Somfy-GPIOs.png) 52 | 53 | 54 | ### Connect the CC1101 transmitter module to your ESP32 55 | - This requires 7 wires. 56 | - Ensure you use a 433MHz version of this module. Carefully check the pinout: similar-looking modules have slightly different pinouts. 57 | - (The [FS1000A](#using-a-simple-fs1000a-transmitter-module) uses just 3 wires: 5V, IRsend and GND.) 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
CC1101ESP32Modules
3V33V3 69 | 70 | 72 | 73 |
SCKSSPI SCLK
MISOSSPI MISO
MOSISSPI MOSI
CSNSSPI CS
GDO0IRsend
GDO2not used
GNDGND
83 | 84 | 85 | 86 | 87 | ### Upload the Berry files to Tasmota 88 | - Download the two Berry files from this project page (`autoexec.be` and `RFtxSMFY_V2.be`), and upload them to your Tasmota's file system. 89 | - `Tasmota` > `Tools` > `Manage File system` > `Choose file` > `Upload`. 90 | 91 | ### Configure the blinds in Tasmota 92 | - Go to the Tasmota console: `Tasmota` > `Tools` > `Console`. 93 | - Type the following commands: 94 | ``` 95 | SetOption80 1 # Enable shutters and blinds 96 | Interlock 1,2 3,4 # Set interlocks, so that Up and Down relays cannot be On at the same time 97 | Interlock 1 # Enable interlocks, required by ShutterMode 98 | ShutterMode 1 # Blinds will use one relay for Up and another relay for Down 99 | ShutterRelay1 1 # Relay 1 and 2 will be Up and Down for Blind 1 100 | ShutterRelay2 3 # Relay 3 and 4 will be Up and Down for Blind 2 101 | ShutterOpenDuration1 9.5 102 | ShutterCloseDuration1 8 103 | ShutterOpenDuration2 9.5 104 | ShutterCloseDuration2 8 105 | Restart 1 # Ensures Home Assistant discovers the new Cover entities 106 | ``` 107 | At this point, you should see Shutter controls on the Tasmota main UI page, and `cover` entities on the HA device page. 108 |

109 | 110 | 111 |

112 | At some point, set the Open and Close durations to match the actual time it takes your blinds to open and close. 113 | 114 | ### Pair the Tasmota with your blinds 115 | - Initialize a controller instance: Go to the Tasmota Console and type `RFtxSMFY {"Idx":1,"Id":101,"RollingCode":1}`. 116 | - Take your existing Somfy controller and press the `Prog` button until the blind briefly jogs up and down. _The Prog button is on the back of the controller, and requires a paperclip to press it._ 117 | - In the Tasmota Console, type `RFtxSMFY {"Idx":1,"Button":8}`. This emulates a press of the `Prog` button on the new controller, and the blind should jog up and down to confirm the pairing. 118 | - Test: Go to the Tasmota main UI page and click the Up and Down buttons for the blind. 119 | - Repeat the procedure for the second blind using these commands: `RFtxSMFY {"Idx":2,"Id":102,"RollingCode":1}`, `RFtxSMFY {"Idx":2,"Button":8}` 120 | - Home Assistant should have discovered the `cover` entities when Tasmota was restarted. Go to the Device page for this Tasmota: You should be able to operate the blinds using the buttons on this page. On your Dashboard, you can create a `cover` card and link it to these entities. 121 | 122 | 123 | ---- 124 | ---- 125 | ---- 126 | ---- 127 | 128 | 129 | 130 | ## Technical details 131 | 132 | ### Troubleshooting 133 | - The Tasmota Console should look like like this: 134 | ``` 135 | # Tasmota Restart 1 136 | 00:00:00.001 HDW: ESP32-D0WD-V3 v3.0 137 | 00:00:00.047 UFS: FlashFS mounted with 264 kB free 138 | 00:00:00.058 CFG: Loaded from File, Count 34 139 | 00:00:00.066 QPC: Count 1 140 | 00:00:00.069 SPI: Soft using GPIO18(CLK), GPIO23(MOSI) and GPIO19(MISO) 141 | 00:00:00.113 BRY: Berry initialized, RAM used 3266 bytes 142 | 00:00:00.122 SHT: Use defaults 143 | 00:00:00.122 SHT: About to load settings from file /.drvset027 144 | 00:00:00.137 Project tasmota - tas-somfy Version 15.1.0(Somfy)-3_3_0(2025-11-03T10:42:57) 145 | 00:00:00.138 SHT: ShutterMode: 1 146 | 00:00:00.139 SHT: ShutterMode: 1 147 | 00:00:00.386 BRY: Successfully loaded 'autoexec.be' 148 | 00:00:02.001 WIF: Connecting to AP1 ASSIDX Channel 1 BSSId 74:83:C2:AB:CD:EF in mode HT40 as tas-somfy... 149 | 00:00:03.958 WIF: Connected 150 | 11:15:28.084 HTP: Web server active on tas-somfy with IP address 192.168.X.XX 151 | 11:15:29.160 MQT: Attempting connection... 152 | 11:15:29.183 MQT: Connected 153 | 11:15:29.186 MQT: tele/tas-somfy/LWT = Online (retained) 154 | 155 | 156 | # ShutterOpen1 157 | 10:57:00.901 MQT: stat/tas-somfy/RESULT = {"POWER1":"ON"} 158 | 10:57:00.903 MQT: stat/tas-somfy/POWER1 = ON 159 | 10:57:00.905 MQT: stat/tas-somfy/RESULT = {"Shutter1":{"Position":37,.... 160 | 10:57:01.374 MQT: stat/tas-somfy/RESULT = {"IRSend":"Done"} 161 | 10:57:01.450 MQT: stat/tas-somfy/RESULT = {"Shutter1":{"Position":43,.... 162 | 10:57:01.925 MQT: stat/tas-somfy/RESULT = {"Shutter1":{"Position":47,.... 163 | 10:57:02.928 MQT: stat/tas-somfy/RESULT = {"Shutter1":{"Position":58,.... 164 | 10:57:03.966 MQT: stat/tas-somfy/RESULT = {"Shutter1":{"Position":69,.... 165 | 10:57:05.015 MQT: stat/tas-somfy/RESULT = {"Shutter1":{"Position":80,.... 166 | 10:57:05.941 MQT: stat/tas-somfy/RESULT = {"Shutter1":{"Position":90,.... 167 | 10:57:06.890 MQT: stat/tas-somfy/RESULT = {"POWER1":"OFF"} 168 | 10:57:06.893 MQT: stat/tas-somfy/POWER1 = OFF 169 | 10:57:06.897 MQT: stat/tas-somfy/SHUTTER1 = 100 170 | 10:57:06.898 MQT: stat/tas-somfy/RESULT = {"Shutter1":{"Position":100,"Direction":0,"Target":100,"Tilt":0}} 171 | 10:57:07.487 SHT: About to save settings to file /.drvset027 172 | ``` 173 | 174 | - If the `RFtxSMFY` command is **Unknown**, then there is an error in loading `autoexec.be` or `RFtxSMFY_V2.be`. 175 | - If the `IRsend` command gives a **format error**, then you may have only the base-level IR support, and not the Full IR support. 176 | - Attach a led (via a 220R resistor) to the `IRsend` pin. You should see it flash when a command is sent. 177 | - To view the transmitted signal, you can use a cheap 433Mhz receiver module with a cheap [FX2 LA](https://sigrok.org/wiki/Fx2lafw) Saleae/PulseView compatible logic analyzer. The waveform and timings should look similar to the signal from the Somfy controller. 178 | 179 | ### If you need to change the ESP32 module 180 | - From your old ESP32 module, save `_persist.json` from the filesystem. Change the device names and topic to avoid clashing with the new device. 181 | - In HA, delete the Tasmota device. Restart HA (this removes the orphaned entities). 182 | - Set up the new ESP32 device using the instructions above, using the same name as the old unit, but skip the pairing procedure. 183 | - Remove `autoexec.be`. Restart. 184 | - Upload `_persist.json` and `autoexec.be`. Restart. (The `Id` and `RollingCode` are contained in `_persist.json`, thus maintaining the pairing.) 185 | - In HA, on the Device page, go to the `...` menu and select `Recreate entity IDs`. 186 | - The blinds should now operate as before. 187 | 188 | ### Using a simple FS1000A transmitter module 189 |

190 | 191 | - The advantage of the FS1000A transmitter module is that you may already have one, it is quick to connect needing only 3 wires, and it is breadboard-friendly. However, it uses the common 433.92MHz frequency, whereas Somfy uses a more unusual 433.42MHz. The 433.92MHz signal should be fine within 2m - 3m distance of the blind, so may be adequate for testing and some applications. 192 | - You can modify the FS1000A to 433.42MHz by changing the SAW device, but I suggest the better solution is to use the CC1101 which has a programmable frequency. 193 | - Connect the FS1000A's Data pin to the IRsend GPIO, and do not configure the 4 SSPI GPIOs. This will cause an error message in the console; optionally set `hasCC1101 = 0` at the top of `RFtxSMFY` to suppress the error. 194 | 195 | ### Building you own firmware 196 | - TasmoCompiler 197 | - TasmoCompiler on GitPod runs in the cloud and used to be the easiest method, but now (I think) requires you to register a payment card, even for the Free Tier. Follow [this link](https://gitpod.io/#https://github.com/benzino77/tasmocompiler). 198 | - TasmoCompiler on your local machine is very easy if you already have docker set up. Follow [these instructions](https://github.com/benzino77/tasmocompiler#how-to-start-using-tasmocompiler). 199 | - Once the TasmoCompiler UI is up, select the type of processor: ESP32: `Generic` or `ESP32 S3`. 200 | - In addition to the default components, select `Home Assistant`, `IR Support`, and `Shutters and Blinds`. 201 | - At `Custom parameters`, paste this: 202 | ``` 203 | #define CODE_IMAGE_STR "Somfy" 204 | #define IR_SEND_USE_MODULATION 0 205 | ``` 206 | 207 | - When the build is complete, download the `firmware.bin` file. 208 | 209 | - PlatformIO 210 | - If using PlatformIO, then add this to your `user_config_override.h`. 211 | ``` 212 | #define CODE_IMAGE_STR "Somfy" 213 | #define USE_IR_REMOTE_FULL 214 | #define IR_SEND_USE_MODULATION 0 215 | ``` 216 | 217 | 218 | ### The RFtxSMFY command 219 | 220 | In normal usage where Home Assistant talks to Tasmota's Shutters and Blinds, you will not need to use the `RFtxSMFY` command except for pairing. The information below is provided in case you want to access the lower level interface. 221 | 222 | There are two ways of using `RFtxSMFY`: Stateful or Stateless. 223 | - **Stateful**: Supports 8 virtual controllers. `RollingCode` is maintained on the ESP32 and uses its persistent memory. Stateful commands require the `Idx` parameter. Examples: 224 | - `RFtxSMFY {"Idx":1,"Id":123,"RollingCode":1}` Initialize virtual controller #1 with Id 123 and start its rolling code at 1. Always set both parameters in this command. 225 | - `RFtxSMFY {"Idx":1,"Button":2}` Transmit 'Up' from virtual controller #1. 226 | - `RFtxSMFY {"Idx":1,"Button":4,"StopAfterMs":2500}` Transmit 'Down' from virtual controller #1, then transmit 'Stop' after 2.5 seconds. 227 | - You may need to know the current values of `Id` and `RollingCode`, for example, to transfer an existing virtual controller to a different ESP32. These values can be seen by viewing the `_persist.json` file in the Tasmota Manage File system Console. 228 | - **Stateless**: Supports any number of virtual controllers. The RollingCode must be maintained on the host that sends the commands to Tasmota. Increment the RollingCode once after each command, and twice for StopAfterMs. Stateless commands do not use the `Idx` parameter. Examples: 229 | - `RFtxSMFY {"Id":123,"RollingCode":6,"Button":2}` 230 | - `RFtxSMFY {"Id":123,"RollingCode":7,"Button":4,"StopAfterMs":2500}` 231 | - **Parameters** 232 | - `Idx` (1-8) The virtual controller number/index used in Stateful mode. 233 | - `Id` (1-16777215) The Id of this virtual controller; you will pair the blind with this Id. It should be different from the Id of any other controllers you have. Use as many Ids as you need. In Stateful mode, this is stored in persistent memory. 234 | - `RollingCode` (0-65535) The Somfy RTS protocol sends a 'rolling code' that increments by 1 each time a command is transmitted. If there is a significant gap between the rolling code you transmit and the rolling code it last received, it will ignore the command. Normally, start at 1. In Stateful mode, this is stored in persistent memory. 235 | - `Button` The buttons on the Somfy Remote Control: Stop/Up/Down/Prog = 1/2/4/8 236 | - `StopAfterMs` Can be used to move a blind for a defined number of milliseconds. 237 | - `Gap` Gap between frames in milliseconds. Default 27. 238 | - `nFrames` Number of frames to send. Default 3. `{"Idx":1,"Button":8,"nFrames":12,"Gap":72}` emulates a long (2 sec) press of the `Prog` button, which may be useful for un-pairing an Id. 239 | - `UseSomfyFreq` (1|0) For use with CC1101, 1: Transmit at 433.42MHz (default), 0: Transmit at 433.92MHz. Can be useful for troubleshooting. 240 | 241 | 242 | ### My usage 243 | I have been using this Tasmota/Berry solution since July 2022 with 100% reliability. Initially, I used the Stateless mode, with my host computer maintaining the rolling code, current position, and calculating the travel-time to the requested position. I used an FS1000A modified to 433.42MHz. From January 2024, I have been using Tasmota's Shutters and Blinds with Home Assistant and a CC1101 transmitter module. Much earlier, I used an ESP8266 running my own firmware since May 2018, but have been on a mission to eliminate as much of my own code as I can from my house. 244 | 245 | ### Acknowledgements 246 | - The Somfy frame building code in makeSomfyFrame() originates from [this project](https://github.com/Nickduino/Somfy_Remote). 247 | - The standard work describing the Somfy RTS protocol can be found [here](https://pushstack.wordpress.com/somfy-rts-protocol/). 248 | - The Tasmota shutter integration was inspired by [this project]( https://github.com/GitHobi/Tasmota/wiki/Somfy-RTS-support-with-Tasmota#using-rules-to-control-blinds). 249 | 250 | -------------------------------------------------------------------------------- /RFtxSMFY_V2.be: -------------------------------------------------------------------------------- 1 | 2 | # Configuration options: 3 | var modFreq = 0 4 | # Set to 1 for Tasmota images built with default options. (Default). 5 | # Set to 0 for Tasmota images built with "#define IR_SEND_USE_MODULATION 0" (Preferred). 6 | var hasCC1101 = 1 # Set to 1 if using a CC1101 transmitter module. 7 | var tasShutters = 1 # Set to 1 to create rules to make Tasmota Shutters generate Somfy commands. 8 | 9 | 10 | 11 | #- 12 | 13 | Upload this file to the Tasmota file system and load() it from autoexec.be. 14 | 15 | This adds the RFtxSMFY command to Tasmota to generate Somfy-RTS format packets for 433MHz RF transmission. 16 | Author: Andrew Russell, Sep 2022. 17 | 18 | 19 | This program uses Tasmota's IRsend in raw format to create the bit stream. 20 | To use this code: 21 | Use an ESP32 to get the Berry scripting language. 22 | Use the tasmota32-ir.bin image to get support for RAW in IRsend. 23 | Configure a pin for IRsend. 24 | Connect this pin to an FS1000A 433Mhz transmitter module. 25 | 26 | About the Somfy RTS protocol: 27 | The Somfy RTS protocol is used for controlling motorized blinds that are fitted with Somfy motors. 28 | Somfy uses 433.42MHz instead of the common 433.92MHz. 29 | A standard 433.92MHz transmitter like the FS1000A will work, but limits the range to 2 or 3 meters. 30 | The easiest solution is to buy "433.42MHz TO-39 SAW Resonator Crystals" on eBay to replace the 433.96MHz Resonator on the FS1000A. 31 | Another solution is to use a CC1101 transmitter that has a programmable transmit frequency. 32 | 33 | 34 | Usage: 35 | 36 | mosquitto_pub -t cmnd/esp32-dev-01/RFtxSMFY -m '{"Id":656,"RollingCode":43,"Button":4}' # Down 37 | mosquitto_pub -t cmnd/esp32-dev-01/RFtxSMFY -m '{"Id":656,"RollingCode":43,"Button":4,"StopAfterMs":4000}' # Down, stop after 4 sec. 38 | mosquitto_pub -t cmnd/esp32-dev-01/RFtxSMFY -m '{"Id":656,"RollingCode":43,"Button":8,"nFrames":12,"Gap":72}' # LongPress PROG. 39 | curl -s --data-urlencode 'cmnd=RFtxSMFY {"Id":656,"RollingCode":43,"Button":4}' http://esp32-dev-01/cm 40 | 41 | 42 | How it works: 43 | It uses IRsend's raw mode, because that is a way to generate a time-accurate bitstream in Tasmota. 44 | IR signals use a 38kHz a carrier that is modulated by the bitstream. But we don't want this 38kHz carrier. 45 | There are three options to handle this: 46 | 47 | 1) Use 1kHz modulation, and use marks of less than 500us (default). 48 | At 1kHz, marks shorter than 500us will not show the carrier. Obviously, the spaces do not show any carrier. 49 | For marks longer than 500us, use multiple shorter marks with zero-length spaces between them. 50 | eg: 1500 becomes 490,0,490,0,490 51 | The zero-length spaces actually appear as 6us spaces or glitches. 52 | The FS1000A ignores the glitches. 53 | The CC1101 transmits some of the glitches, but the Somfy ignores them. 54 | You can optionally add a small RC low pass filter to the pin to remove the glitches. (1k2, 47nF) 55 | Set modFreq = 1 to enable the multi-mark logic. 56 | 57 | 2) Use default 38k modulation anyway. 58 | It turns out that the FS1000A ignores the 38kHz modulation. 59 | Not suitable for CC1101. 60 | Set modFreq = 0 to select default 38kHz carrier, and disable the multi-mark logic. 61 | 62 | 3) Disable IR_SEND_USE_MODULATION (preferred). 63 | Build the Tasmota image with "#define IR_SEND_USE_MODULATION 0". 64 | Set modFreq = 0 to disable the multi-mark logic. 65 | 66 | Acknowledgments: 67 | The Somfy frame building code in makeSomfyFrame() originates from https://github.com/Nickduino/Somfy_Remote. 68 | Additional description of the Somfy RTS protocol can be found here: https://pushstack.wordpress.com/somfy-rts-protocol/ 69 | Tasmota shutter integration: https://github.com/GitHobi/Tasmota/wiki/Somfy-RTS-support-with-Tasmota#using-rules-to-control-blinds 70 | 71 | -# 72 | 73 | 74 | 75 | ############################################################################################################ 76 | ############################################################################################################ 77 | ############################################################################################################ 78 | ############################################################################################################ 79 | 80 | 81 | # CC1101 support -------------------------------------------------------- 82 | 83 | 84 | #- 85 | What the CC1101 support code does: 86 | Initialize a CC1101 Tx/Rx module to transmit data in ASK/OOK mode at 433.92MHz or 433.42MHz. 87 | The CC1101 calls this 'asynchronous serial mode'. 88 | The CC1101 is useful because the transmit frequency can be programmed, thus providing the non-standard 433.42MHz frequency that the Somfy RTS protocol uses. 89 | The CC1101 is configured using its SPI interface. Once configured, the SPI interface does not need to be used. 90 | (Although, to be nice, I put the CC1101 into Tx mode before transmitting, and into Idle after transmitting. I could probably leave it in Tx mode.) 91 | The data to be transmitted should be connected to pin GDO0. 92 | 93 | 94 | How I wrote the CC1101 support: 95 | 96 | SPI Interface: 97 | 98 | Berry does not have an SPI object (like it does for I2C), so I bit-bang the SPI with gpio read/writes. 99 | 100 | 101 | 102 | CC1101 setup and register values: 103 | 104 | I used TI's SmartRF Studio 7 tool: 105 | Configure for Generic 433MHz, low data rate. 106 | Adjust to 433.92 and 433.42 MHz, Modulation format: ASK/OOK. 107 | Export 'default set' of registers for each of 433.92 and 433.42. 108 | 109 | I built https://github.com/ruedli/SomfyMQTT, SimpleSomfy.ino 110 | This uses https://github.com/LSatan/SmartRC-CC1101-Driver-Lib, which is based on work by Elechouse (https://www.elechouse.com/). 111 | I built and tested this on a Wemos D1 mini (ESP8266). 112 | Using a Logic Analyzer, I watched the SPI as it boots. 113 | I used info from this to modify any relevant register settings that SmartRF Studio gave me. 114 | 115 | I understand most of what the Logic Analyzer shows me. 116 | The Elechouse code can calculate the MHz settings; I skipped that and use the settings that SmartRF gave me. 117 | The Elechouse code sets the PA table. It turns out this is important. Without the PA table, the Tx pin behaved as active low; I don't know why. 118 | The Elechouse code calls a Calibrate() function after setting the freq. I don't understand this, and have not implemented it. 119 | 120 | 121 | 122 | Thoughts, to do etc 123 | - Can I write the CC1101 support as a class, to hide all the private functions? 124 | - Read something from the CC1101, and report an error if it is not present. 125 | 126 | - The infinite loop in SpiWriteBytes() is ok. 127 | If MISO never goes low, I get: BRY: Exception> 'timeout_error' - Berry code running for too long 128 | But, should I use gpio.INPUT or gpio.INPUT_PULLUP on that pin? 129 | I don't think it matters. 130 | If the CC1101 is absent, gpio.INPUT just runs through the code, gpio.INPUT_PULLUP throws the BRY: Exception> 'timeout_error' 131 | 132 | 133 | -# 134 | 135 | import gpio 136 | 137 | var SCK_PIN = -1 138 | var MISO_PIN = -1 139 | var MOSI_PIN = -1 140 | var CS_PIN = -1 141 | 142 | var cc1101_freq = 0 # init state of CC1101. Can be 0, 42 or 92. 143 | var rfFreq = 92 # default 92. Can be set elsewhere. 144 | var protocol = '' 145 | 146 | def SpiInit() 147 | # Get SPI (actually, Software SPI, SSPI) pins from Tasmota Configuration 148 | SCK_PIN = gpio.pin(gpio.SSPI_SCLK) 149 | MISO_PIN = gpio.pin(gpio.SSPI_MISO) 150 | MOSI_PIN = gpio.pin(gpio.SSPI_MOSI) 151 | CS_PIN = gpio.pin(gpio.SSPI_CS) 152 | 153 | if hasCC1101 && (SCK_PIN < 0 || MISO_PIN < 0 || MOSI_PIN < 0 || CS_PIN < 0) 154 | print("RFtxSMFY Error: CC1101 SPI pin(s) not defined.") 155 | hasCC1101 = 0 156 | end 157 | end 158 | 159 | 160 | 161 | 162 | def SpiWriteBytes(data, nBytes) 163 | if !hasCC1101 return end 164 | gpio.digital_write(CS_PIN, 0); 165 | while gpio.digital_read(MISO_PIN) end # wait for MISO to go low. 166 | for b: 0 .. nBytes-1 167 | var dataToGo = data[b] 168 | var mask = 0x80 169 | for i: 1 .. 8 170 | gpio.digital_write(MOSI_PIN, dataToGo & mask ? 1 : 0) 171 | gpio.digital_write(SCK_PIN, 1) 172 | gpio.digital_write(SCK_PIN, 0) 173 | mask >>= 1 174 | end 175 | end 176 | gpio.digital_write(CS_PIN, 1); 177 | end 178 | 179 | def SpiWriteReg(addr, value) 180 | SpiWriteBytes([addr, value], 2) 181 | end 182 | 183 | def SpiStrobe(addr) 184 | SpiWriteBytes([addr], 1) 185 | end 186 | 187 | def RegConfigSettings(freq) 188 | 189 | gpio.pin_mode(CS_PIN, gpio.OUTPUT) 190 | gpio.digital_write(CS_PIN, 1) 191 | tasmota.delay(50) 192 | 193 | gpio.pin_mode(SCK_PIN, gpio.OUTPUT) 194 | gpio.pin_mode(MISO_PIN, gpio.INPUT) 195 | gpio.pin_mode(MOSI_PIN, gpio.OUTPUT) 196 | 197 | gpio.digital_write(SCK_PIN, 0) 198 | gpio.digital_write(MOSI_PIN, 0) 199 | 200 | # from SmartRF Studio 7, RegConfigSettings(), with modifications from Elechouse. 201 | 202 | SpiStrobe(0x30) # SRES 203 | tasmota.delay(5) 204 | 205 | SpiWriteReg(0x02,0x0D) # IOCFG0 Elechouse uses 0x0D, SmartRF says 0x06. 206 | SpiWriteReg(0x03,0x47) # FIFOTHR 207 | SpiWriteReg(0x08,0x32) # PKTCTRL0 from Elechouse ccMode(1), SmartRF says 0x05 208 | SpiWriteReg(0x0B,0x06) # FSCTRL1 209 | 210 | SpiWriteBytes([0x7E, 0x00,0xC0,0x00,0x00,0x00,0x00,0x00,0x00], 9) # PATABLE, as per Elechouse. 211 | 212 | if freq == 42 213 | # 433.42MHz 214 | print('Setting 433.42MHz/Somfy') 215 | SpiWriteReg(0x0D,0x10) # FREQ2 216 | SpiWriteReg(0x0E,0xAB) # FREQ1 217 | SpiWriteReg(0x0F,0x85) # FREQ0 218 | else 219 | # 433.92MHz 220 | print('Setting 433.92MHz/Normal') 221 | SpiWriteReg(0x0D,0x10) # FREQ2 222 | SpiWriteReg(0x0E,0xB0) # FREQ1 223 | SpiWriteReg(0x0F,0x71) # FREQ0 224 | end 225 | SpiWriteReg(0x10,0xF6) # MDMCFG4 226 | SpiWriteReg(0x11,0x83) # MDMCFG3 227 | SpiWriteReg(0x12,0x33) # MDMCFG2 Elechouse uses 0xBF 228 | SpiWriteReg(0x15,0x15) # DEVIATN 229 | SpiWriteReg(0x18,0x18) # MCSM0 230 | SpiWriteReg(0x19,0x16) # FOCCFG 231 | SpiWriteReg(0x20,0xFB) # WORCTRL 232 | SpiWriteReg(0x22,0x11) # FREND0 233 | SpiWriteReg(0x23,0xE9) # FSCAL3 234 | SpiWriteReg(0x24,0x2A) # FSCAL2 235 | SpiWriteReg(0x25,0x00) # FSCAL1 236 | SpiWriteReg(0x26,0x1F) # FSCAL0 237 | SpiWriteReg(0x2C,0x81) # TEST2 238 | SpiWriteReg(0x2D,0x35) # TEST1 239 | SpiWriteReg(0x2E,0x09) # TEST0 240 | 241 | SpiStrobe(0x36) # SIDLE seems sensible to do this at this time. 242 | 243 | cc1101_freq = freq 244 | end 245 | 246 | 247 | 248 | def SetTx() 249 | # from Elechouse 250 | SpiStrobe(0x36) # SIDLE 251 | SpiStrobe(0x35) # STX 252 | end 253 | 254 | def SetSidle() 255 | # from Elechouse 256 | SpiStrobe(0x36) # SIDLE 257 | end 258 | 259 | 260 | def myIrSend(listStr) 261 | if hasCC1101 && cc1101_freq == 0 262 | SpiInit() # avoid calling this as it has a delay in it. 263 | RegConfigSettings(rfFreq) 264 | end 265 | if hasCC1101 && cc1101_freq != rfFreq 266 | RegConfigSettings(rfFreq) 267 | end 268 | 269 | if hasCC1101 SetTx() end 270 | tasmota.cmd('IRsend ' + str(modFreq) + ',' + listStr) 271 | if hasCC1101 SetSidle() end 272 | end 273 | 274 | 275 | 276 | 277 | ############################################################################################################ 278 | ############################################################################################################ 279 | ############################################################################################################ 280 | ############################################################################################################ 281 | 282 | 283 | 284 | # Somfy support ---------------------------------------------------------------- 285 | 286 | 287 | 288 | import string 289 | 290 | 291 | var list1 292 | 293 | 294 | def makeSomfyFrame(rID, rCode, button, startFrame, nFrames) 295 | #- 296 | This contains the Somfy protocol logic. 297 | Original code from https://github.com/Nickduino/Somfy_Remote 298 | 299 | makeSomfyFrame(rID, rCode, button, 1, 3) generate a normal 3-frame message 300 | makeSomfyFrame(rID, rCode, button, 1, 1) generate a single start frame 301 | makeSomfyFrame(rID, rCode, button, 0, 1) generate a single follow-on frame 302 | 303 | -# 304 | 305 | var halfDigit = 640 # length of halfDigit in uSec 306 | var frame = [0,0,0,0,0,0,0] 307 | list1 = [] 308 | 309 | frame[0] = 0xa7 310 | frame[1] = (button << 4) & 0xf0 # upper nibble is button, lower nibble will be checksum. 311 | frame[2] = (rCode >> 8) & 0xff # 16 bit rolling code 312 | frame[3] = rCode & 0xff 313 | frame[4] = (rID >> 16) & 0xff # 24 bit controller id 314 | frame[5] = (rID >> 8) & 0xff 315 | frame[6] = rID & 0xff 316 | 317 | # Checksum calculation: an XOR of all the nibbles 318 | var checksum = 0; 319 | for i: 0 .. 6 320 | checksum = checksum ^ frame[i] ^ (frame[i] >> 4) 321 | end 322 | frame[1] |= (checksum & 0x0f) 323 | 324 | # print(string.format(' pre-obfust: %02x %02x %02x %02x %02x %02x %02x\n', frame[0], frame[1], frame[2], frame[3], frame[4], frame[5], frame[6])) 325 | 326 | if 0 327 | # Debug: check the checksum, should be zero 328 | checksum = 0 329 | for i: 0 .. 6 330 | checksum = checksum ^ frame[i] ^ (frame[i] >> 4) 331 | end 332 | print('Checksum check: ' .. (checksum & 0x0f)) 333 | end 334 | 335 | if 1 336 | # Obfuscation: XOR each byte with the previous byte (disable for debug) 337 | for i: 1 .. 6 338 | frame[i] ^= frame[i-1] 339 | end 340 | end 341 | # print(string.format('post-obfust: %02x %02x %02x %02x %02x %02x %02x\n', frame[0], frame[1], frame[2], frame[3], frame[4], frame[5], frame[6])) 342 | 343 | # Make nFrames frames 344 | for fn: 1 .. nFrames 345 | if fn > 1 list1.push(-27000) end # space between frames. 346 | var nsync = 0 347 | if(startFrame && fn == 1) 348 | # first frame: hardware wake up pulse and fewer sync pulses 349 | list1.push( 12000) 350 | list1.push(-18000) 351 | nsync = 2 352 | else 353 | # follow-on frames: have more sync pulses 354 | nsync = 7 355 | end 356 | 357 | for i: 1 .. nsync 358 | # software sync pulses 359 | list1.push( halfDigit * 4) 360 | list1.push(-halfDigit * 4) 361 | end 362 | list1.push( 4700) #4550 363 | list1.push(-halfDigit) 364 | 365 | # The frame data: for each of 7 bytes, for each bit. 7x8=56 bits. 366 | # Somfy uses Manchester encoding: rising edge = 1, falling edge = 0. 367 | for i: 0 .. 6 368 | var mask = 0x80 369 | while mask > 0 370 | if(frame[i] & mask) 371 | list1.push(-halfDigit) 372 | list1.push( halfDigit) 373 | else 374 | list1.push( halfDigit) 375 | list1.push(-halfDigit) 376 | end 377 | mask >>= 1 378 | end 379 | end 380 | 381 | end 382 | 383 | end 384 | 385 | 386 | 387 | # For the IRsend raw/condensed format: 388 | # Build a list of individual durations as we encounter them. Remember between calls to frame_bin2text(). 389 | var codes 390 | 391 | 392 | def frame_bin2text() 393 | #- 394 | Input: list1[] (global var, from makeSomfyFrame()): +ve elements are marks, -ve elements are spaces. Values are in uSec. 395 | like: [12000, -18000, 2560, -2560, 2560, -2560, 4700, -1280, 1280, -1280, 1280, -640, 640, -1280, 640, -640, 640, -640, 640,....] 396 | It may include adjacent marks and adjacent spaces, like: [... 640, 640, -640, -640, 640, -640 ...] This is not valid for IRsend. 397 | 398 | Return: A string in Tasmota-compatible IRsend Raw/condensed format. 399 | like: "+470-1AbAbAbAbAbAbAbAbAbAbAbAbAbAbAbAbAbAbAbAbAbAbAbA-18000+416bDbDbDbDbD-2560DbDbDbDbDbDe+460bFbFbFbFbFbFbFbFbF-1280DbDb" 400 | Note: Marks longer than 490us (that's all of them, I think) will be broken into multiple short marks with very small spaces between, 401 | to stop IRsend's 1kHz modulation showing through. 402 | 403 | -# 404 | 405 | var list2 = [] 406 | var list3 = [] 407 | var sOut = '' 408 | 409 | # combine adjacent elements which have the same sign, list1[] to list2[] 410 | var iOut = 0 411 | list2.push(0) 412 | for len: list1 413 | var s1 = len >= 0 414 | var s2 = list2[iOut] >= 0 415 | if s1 == s2 416 | list2[iOut] += len # iOut is always last element of list, could use list2[-1], or list2[size(list2)-1]. 417 | else 418 | list2.push(len) 419 | iOut += 1 420 | end 421 | end 422 | # print(list2) 423 | 424 | if modFreq == 1 425 | # If modulation frequency == 1kHz, then marks can be up to 500us. 426 | # Break up marks longer than maxMark microseconds into multiple shorter marks. 427 | # Input list2[], output list3[]. 428 | for len: list2 429 | if len > 0 430 | var n = 1 431 | while (len / n) > 490 432 | n += 1 433 | end 434 | var len2 = int(len / n) - 10 435 | # Needs n marks of len2 uSec each 436 | list3.push(len2) 437 | for i: 2 .. n 438 | list3.push(-1) # very short space (which will be filtered out in hardware) 439 | list3.push(len2) # the mark 440 | end 441 | else 442 | list3.push(len) 443 | end 444 | end 445 | else 446 | # Allow long marks. 447 | # Suitable for 38kHz modulation with FS1000A Tx module. 448 | # Or no modulation with any Tx module (set with #define IR_SEND_USE_MODULATION 0). 449 | list3 = list2 450 | end 451 | 452 | # if the last element is a space (-ve), then remove it. 453 | # (this makes it possible to append gaps (spaces) in following steps) 454 | if protocol == 'somfy' && list3[-1] < 0 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 455 | list3.remove(-1) # could list3.resize(size(list3)-1) 456 | end 457 | 458 | 459 | # Convert to text string in Tasmota IRsend Raw/condensed format, list3[] to sOut. 460 | if 0 461 | # Plus/minus format, without compression. 462 | for len: list3 463 | if len >= 0 464 | sOut += '+' .. len 465 | else 466 | sOut += str(len) 467 | end 468 | end 469 | else 470 | # condensed format 471 | # var codes = [] # build a list of individual durations as we encounter them. 472 | for len: list3 473 | var lenAbs = len 474 | var sign = '+' 475 | var code2char = 65 # 65=A 476 | if len < 0 477 | lenAbs = -len 478 | sign = '-' 479 | code2char = 97 # 97=a 480 | end 481 | var c = codes.find(lenAbs) 482 | if c == nil 483 | # this duration has not been used before, so use the number... 484 | sOut += sign .. lenAbs 485 | codes.push(lenAbs) # ... and store for future use. 486 | else 487 | # this duration has been used before, so use the single-character code. 488 | sOut += string.char(c + code2char) 489 | end 490 | end 491 | end 492 | 493 | return sOut 494 | 495 | end 496 | 497 | 498 | def makeSomfyMessage(id, rCode, button, nFrames, frameGap) 499 | 500 | codes = [] 501 | makeSomfyFrame(id, rCode, button, 1, 1) # First frame, result in list1[] 502 | var listStr = frame_bin2text() # reads list1[], returns a string 503 | if nFrames == 1 return listStr end 504 | 505 | makeSomfyFrame(id, rCode, button, 0, 1) # Follow-on frames 506 | var listStr2 = frame_bin2text() 507 | var gaplet = frameGap * 1000 # gap between frames 508 | var nGaps = 1 509 | if gaplet > 32000 # max space seems to be 40,000ms. If larger, then use multiple spaces. 510 | nGaps = int(gaplet/32000)+1 511 | gaplet = int(gaplet/nGaps) 512 | end 513 | for i: 2 .. nFrames 514 | listStr += '-' + str(gaplet) # like -32000 515 | for j: 2 .. nGaps 516 | listStr += '+1-' + str(gaplet) # like -32000+1-32000 517 | end 518 | listStr += listStr2 # append a follow-on frame 519 | end 520 | return listStr 521 | 522 | end 523 | 524 | 525 | 526 | # These global vars carry the values from somfy_cmd(), through the tasmota.set_timer(), to somfy_stop(). 527 | var id 528 | var rCode 529 | var button 530 | var nFrames 531 | var frameGap 532 | var useSomfyFreq = 1 # set to 0 for 433.92MHz, can be useful for diagnostics. 533 | 534 | 535 | def somfy_stop() 536 | # Used by the "StopAfterMs" functionality, call-back from tasmota.set_timer() 537 | button = 1 # stop 538 | var listStr = makeSomfyMessage(id, rCode+1, button, nFrames, frameGap) 539 | myIrSend(listStr) 540 | end 541 | 542 | 543 | def somfy_cmd(cmd, ix, payload, payload_json) 544 | var idx = 0 545 | id = 0 546 | rCode = 0 547 | button = 0 548 | nFrames = 3 549 | frameGap = 27 550 | rfFreq = 42 # 433.42MHz 551 | 552 | 553 | # parse payload 554 | if payload_json != nil && payload_json.find("Idx") != nil # does the payload contain an 'Idx' field? 555 | idx = int(payload_json.find("Idx")) 556 | end 557 | if payload_json != nil && payload_json.find("Id") != nil 558 | id = int(payload_json.find("Id")) 559 | end 560 | if payload_json != nil && payload_json.find("RollingCode") != nil 561 | rCode = int(payload_json.find("RollingCode")) 562 | end 563 | if payload_json != nil && payload_json.find("Button") != nil 564 | button = payload_json.find("Button") 565 | var bTxt = string.tolower(button) 566 | if bTxt == 'stop' button = 1 567 | elif bTxt == 'up' button = 2 568 | elif bTxt == 'down' button = 4 569 | elif bTxt == 'prog' button = 8 570 | else button = int(button) 571 | end 572 | end 573 | if payload_json != nil && payload_json.find("nFrames") != nil 574 | nFrames = int(payload_json.find("nFrames")) 575 | end 576 | if payload_json != nil && payload_json.find("Gap") != nil 577 | frameGap = int(payload_json.find("Gap")) 578 | end 579 | if payload_json != nil && payload_json.find("UseSomfyFreq") != nil 580 | useSomfyFreq = int(payload_json.find("UseSomfyFreq")) 581 | end 582 | 583 | protocol = 'somfy' 584 | 585 | 586 | 587 | import persist 588 | if idx > 0 589 | # Stateful mode: id will be remembered, and rCode will be maintained in _persist.json 590 | if !persist.has('sState') 591 | # Persistent storage of [id, rCode] for virtual controllers 1 thru 8. 592 | persist.sState = [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]] # sState[0] not used. 593 | end 594 | if id == 0 595 | # id is not supplied, so retrieve it from persistent storage 596 | id = persist.sState[idx][0] 597 | rCode = persist.sState[idx][1] 598 | else 599 | # id is not supplied, so store it and rCode in persistent storage 600 | persist.sState[idx][0] = id 601 | persist.sState[idx][1] = rCode 602 | end 603 | end 604 | 605 | if payload_json != nil && payload_json.find("StopAfterMs") != nil && button != 0 606 | var delay = int(payload_json.find("StopAfterMs")) 607 | # set this delay before executing the first tasmota.cmd('IRsend') to get the best start-to-start period. 608 | tasmota.set_timer(delay, somfy_stop) 609 | if idx > 0 610 | persist.sState[idx][1] += 1 611 | end 612 | end 613 | 614 | if button != 0 615 | var listStr = makeSomfyMessage(id, rCode, button, nFrames, frameGap) 616 | myIrSend(listStr) 617 | end 618 | 619 | if idx > 0 620 | if button != 0 621 | persist.sState[idx][1] += 1 622 | end 623 | persist.dirty() # persist.save() does not notice a change in arrays, so mark it explicitly. 624 | # Not required V12.0.2. Required V14.2.0. 625 | persist.save() # This is required in case the ESP32 reboots without a clean shutdown. 626 | # comment out while testing to reduce ware on the flash 627 | # At some future version, persist.save(true), as per https://github.com/arendst/Tasmota/pull/22246 628 | end 629 | 630 | # tasmota.resp_cmnd_done() # causes {"IRSend":"Done"} 631 | tasmota.resp_cmnd('{"RFtxSMFY":"Done"}') 632 | 633 | end 634 | 635 | tasmota.add_cmd('RFtxSMFY', somfy_cmd) 636 | 637 | 638 | 639 | ############################################################################################################ 640 | ############################################################################################################ 641 | ############################################################################################################ 642 | ############################################################################################################ 643 | 644 | 645 | 646 | # Tasmota Shutter Integration -------------------------------------------------- 647 | 648 | 649 | 650 | #- 651 | 652 | SetOption80 1 653 | Configure relay1 and relay2 - this seems to be required 654 | ShutterRelay1 1 # optional/implied 655 | ShutterMode1 1 # mode 1: relay1 = up, relay2 = down. 656 | 657 | mosquitto_pub -t cmnd/esp32-dev-01/ShutterOpen -m '' 658 | mosquitto_pub -t cmnd/esp32-dev-01/ShutterPosition -m 50 659 | mosquitto_pub -t cmnd/esp32-dev-01/ShutterClose -m '' 660 | 661 | Configure relay3 and relay4 662 | ShutterRelay2 3 # Shutter2 will use relay 3 and 4, required 663 | ShutterMode2 1 664 | 665 | 666 | -# 667 | 668 | 669 | 670 | #- 671 | Rules to connect Somfy blinds to the Tasmota Shutters functionality. 672 | 673 | Inspired by https://github.com/GitHobi/Tasmota/wiki/Somfy-RTS-support-with-Tasmota#configuring-tasmota 674 | 675 | This code has 3 main functions: I call these F1, F2 and F3. 676 | F1 provides the connection from the Tasmota-Shutter functionality to the RFtxSMFY command. 677 | F2 and F3 are nice-to-haves. 678 | 679 | 680 | F1) Send Somfy Up/Down/Stop commands when the state of the Up or Down relays change. 681 | Tasmota controls shutters and blinds by operating relays; usually one relay for Up, and another relay for Down. 682 | We need to configure some dummy relays for the Shutters function to connect to. 683 | Then, a relay change triggers a rule to send an RFtxSMFY command. 684 | When the Up relay turns on, send a Somfy Up command. 685 | When the Down relay turns on, send a Somfy Down command. 686 | When either relay turns off, send a Somfy Stop command. 687 | So far, so simple. 688 | 689 | 690 | F2) Suppress 'move to configured position' (also known as the 'my' button) commands. 691 | Don't send a Somfy Stop when the blind has already stopped. 692 | eg: ShutterClose will cause the Down relay to turn on for a period, then off. 693 | This will generate a Somfy Down, then a Somfy Stop. 694 | But if the Somfy blind receives a Stop when the blind is not moving, it interprets it as 'move to configured position'. 695 | This is undesirable: ShutterClose would close the blind, then move to the configured position. 696 | We can fix that with some simple logic: 697 | When either relay turns off, send a Stop command UNLESS the position is 0 or 100, where the blind will have stopped itself. 698 | 699 | Actually, this is not really required, because RFtxSMFY is probably a unique controller ID, for which the 'configured position' has 700 | not been configured. So, sending a Stop from this controller ID will be ignored. 701 | 702 | However, this is still a good thing to do. Without it, we might stop the blind just before it reaches the end-stop. 703 | 704 | 705 | F3) Provide 'Calibrate' commands. 706 | Send a 'calibrate' Up or Down command even if the blind is already at the 100 or 0 position. 707 | (Added Jan-2024) 708 | Consider: 709 | Assume the blind is at position 90. 710 | ShutterClose: Tasmota sends a Somfy Down, and when it thinks the blind has reached position 0, it updates the position to 0. 711 | Next, we move the blind to position 60 using a Somfy hand controller. 712 | ShutterClose: Tasmota thinks the blind is at 0, so does not send any commands, and the blind stays at 60. 713 | 714 | If we always send an Up or Down, even when Tasmota thinks the blind is at the end stop, it will resync in these cases. 715 | 716 | The logic for this is a bit more complex: 717 | If we get a Shutter#Position event where the position is the same as the previously reported position, 718 | and is 0 or 100, then consider sending an Down or Up command. 719 | Look in the code for additional checks that need to be done. 720 | 721 | 722 | 723 | 724 | ### History 725 | 2022-10-06 - First published integration with Tasmota Shutters. 726 | Using method described in https://github.com/GitHobi/Tasmota/wiki/Somfy-RTS-support-with-Tasmota#configuring-tasmota 727 | Users report that it works, but I have not used it. 728 | 2024-01-15 - Configured Tasmota Shutters on my own system. 729 | Benefits: 730 | 1. Simplification - action.pl (on my server) no longer needs to maintain state/position. 731 | 2. Tasmota supports realtime stop/start commands, whereas action.pl only provided go-to-position commands. 732 | 'calibrate' function is still provided by action.pl. 733 | 2024-01-21 - New Shutter Integration Berry code: 734 | 1. Provides 'calibrate' function. 735 | 2. Uses json data from the Shutter1#Position trigger, instead of a combination of Shutter1#Position and Power1#State triggers. 736 | 737 | 738 | https://tasmota.github.io/docs/_media/berry_short_manual.pdf 739 | https://berry.readthedocs.io/en/latest/source/en/Reference.html 740 | 741 | -# 742 | 743 | if tasShutters 744 | 745 | 746 | def sendCommand(idx, cmd, cal) 747 | # cal: nil or 'Calibrate' 748 | # print('--------- Somfy', idx, cmd) 749 | somfy_cmd(0, 0, 0, {"Idx": idx, "Button": cmd} ) 750 | 751 | if false 752 | # optional diagnostic mqtt messages 753 | import string 754 | import mqtt 755 | var logStr = string.format("%s,%d,%s,%s", 756 | tasmota.time_str(tasmota.rtc()['local']), 757 | idx+0, cmd, cal ? cal : '') 758 | 759 | mqtt.publish('shutterlog/log', logStr) 760 | end 761 | end 762 | 763 | #- 764 | "Shutter is moving" can be determined from: 765 | Relays: There is and up and a down relay. If either is ON, the shutter is moving. If both are OFF it is stopped. 766 | Direction: 1:up, -1:down, 0:off. 767 | We will use Direction, because it is provided along with Position and Target by the Shutter1#Position trigger. 768 | -# 769 | 770 | var moveState = [0,0,0,0,0] # previous Direction state, so we can detect a change. (index 0 is not used) 771 | var cmndRcvd = [false, false, false, false, false] 772 | 773 | def shutterPos(v, trigger, msg) 774 | # triggered by Shutter1#Position 775 | # msg eg {"Shutter1":{"Position":0,"Direction":0,"Target":0,"Tilt":0}} 776 | 777 | #- 778 | We have entered this function because we have received a Shutter Position update, this could be triggered by: 779 | 1) Any Shutter-move command for this Shutter, and a motor needs to be turned on or off. 780 | Position != Target, and Direction != 0, and Direction has changed. We [probably] need to send a Somfy command. 781 | 2) Any Shutter-move command for this Shutter, even if it is already in the target position, and a motor does NOT need to be turned on or off. 782 | Position == Target, and Direction == 0. If we are on an end-stop, we would like to send a calibrate Somfy command. 783 | 3) This Shutter is moving, and we are getting position updates. 784 | Position != Target, and Direction != 0, and Direction has NOT changed. Not action required. 785 | 4) Another Shutter is moving; Tasmota sends updates on all Shutters when this is happening. 786 | Position == Target, and Direction == 0. No action required. 787 | Problem: It is very difficult to differentiate #4 from #2. 788 | -# 789 | 790 | var idx = int(trigger[7]) # character at position 7 is idx 791 | var s = msg["Shutter"..idx] 792 | # print("============", idx, s["Direction"], s["Target"], s["Position"]) 793 | 794 | if moveState[idx] != s["Direction"] 795 | # Tasmota started or stopped a shutter motor, so send a Somfy up, down, or stop command. 796 | if s["Direction"] > 0 797 | sendCommand(idx, 'up') 798 | elif s["Direction"] < 0 799 | sendCommand(idx, 'down') 800 | else 801 | # Send a 'stop', unless Tasmota thinks the blind is already at the end-stop. 802 | if s["Position"] > 0 && s["Position"] < 100 803 | sendCommand(idx, 'stop') 804 | end 805 | end 806 | moveState[idx] = s["Direction"] 807 | else 808 | #- 809 | This is a Position update with no change to motors. 810 | Either: We have received a move-to-position command, but Tasmota thinks the shutter 811 | is already at that position, so does not turn on any motors. 812 | In this case, and if it is at an end-stop, we would like to send a calibrate command. 813 | Or: Another shutter is moving, and we are getting position updates on all shutters, 814 | in which case we need to ignore it. 815 | -# 816 | 817 | if false 818 | # Method #1: Another shutter is moving, so don't send a calibrate. 819 | # Problem: It omits the calibrate if both shutters have a command. 820 | if s["Target"] == s["Position"] && (s["Position"] == 0 || s["Position"] == 100) 821 | # We are sitting at an end-stop, so we may want to send a calibrate-move. 822 | # If any [other] shutters are moving, then this is just a position update, NOT a move-to-position command. 823 | var anyMoving = false 824 | for m: moveState 825 | if m != 0 anyMoving = true end 826 | end 827 | if !anyMoving 828 | var dir = s["Position"] < 50 ? 'down' : 'up' 829 | sendCommand(idx, dir, 'Calibrate') 830 | end 831 | end 832 | else 833 | # Method #2: Send a calibrate if we received a command for this shutter. 834 | # Problem: This only works for mqtt commands, because I use an mqtt interposer to detect the command. 835 | if (s["Position"] == 0 || s["Position"] == 100) && cmndRcvd[idx] 836 | var dir = s["Position"] < 50 ? 'down' : 'up' 837 | sendCommand(idx, dir, 'Calibrate') 838 | end 839 | end 840 | 841 | end 842 | 843 | cmndRcvd[idx] = false 844 | end 845 | 846 | 847 | import mqtt 848 | import string 849 | 850 | def mqttIn(topic, x, payload) 851 | # Purpose: Set a flag in cmndRcvd[] if we get a ShutterMove command. Used above in calibrate logic. 852 | # All mqtt commands 853 | topic = string.tolower(topic) 854 | if string.find(topic, 'shutter') >= 0 855 | # All Shutter commands. 856 | # Now look for specific Shutter-Move commands. This might not be necessary. 857 | var shutterCommands = ['ShutterOpen', 'ShutterClose', 'ShutterPosition', 'ShutterChange', 'ShutterToggle', 858 | 'ShutterToggleDir', 'ShutterStop', 'ShutterStopOpen', 'ShutterStopClose', 'ShutterStopPosition', 859 | 'ShutterStopToggle', 'ShutterStopToggleDir' ] 860 | for c: shutterCommands 861 | if string.find(topic, string.tolower(c)) >= 0 862 | # print('------- Setting cmndRcvd for', c) 863 | cmndRcvd[ int(topic[-1]) ] = true # last char of topic is idx, eg ShutterClose1 864 | break 865 | end 866 | end 867 | 868 | end 869 | return false # 'return false' allows Tasmota to process the message, else it will be discarded. 870 | end 871 | 872 | 873 | mqtt.subscribe('cmnd/' + tasmota.cmd('Topic', true)['Topic'] + '/+', mqttIn) # Interpose all mqtt commands to this node. 874 | tasmota.add_rule("Shutter1#Position", shutterPos ) 875 | tasmota.add_rule("Shutter2#Position", shutterPos ) 876 | tasmota.add_rule("Shutter3#Position", shutterPos ) 877 | tasmota.add_rule("Shutter4#Position", shutterPos ) 878 | 879 | 880 | end 881 | --------------------------------------------------------------------------------