├── 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 | 
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 | | CC1101 |
62 | ESP32 |
63 | Modules |
64 |
65 |
66 | | 3V3 |
67 | 3V3 |
68 |
69 |
70 | |
71 |
72 |
73 | |
74 |
75 | | SCK | SSPI SCLK |
76 | | MISO | SSPI MISO |
77 | | MOSI | SSPI MOSI |
78 | | CSN | SSPI CS |
79 | | GDO0 | IRsend |
80 | | GDO2 | not used |
81 | | GND | GND |
82 |
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 |
--------------------------------------------------------------------------------