Modul SMS Gateway nahrazuje Odoo IAP pro odesílání SMS. Místo cloudové služby používá fyzické Android telefony jako SMS brány. Každý telefon se spáruje s Odoo pomocí QR kódu a automaticky odesílající SMS z fronty.
| Komponenta | Popis |
|---|---|
sms_gateway |
Odoo modul - spravuje telefony, frontu, API endpointy |
sms-gateway-app |
React Native (Expo) aplikace pro Android |
sms.gateway.phone |
Model reprezentující jeden registrovaný telefon |
sms.sms |
Rozšířený o gateway_phone_id a gateway_state
|
sms, mass_mailing_sms,
phone_validation
qrcode (pro generování QR kódu)pip install qrcode[pil]
sms_gateway do adresáře Odoo addons:
cp -r extra/sms/sms_modules/sms_gateway /path/to/odoo/addons/
sms_gateway je vzájemně
exkluzivní s sms_httpsms. Pokud používáte sms_httpsms,
odinstalujte ho před instalací sms_gateway.
Stáhněte nejnovější release APK z
GitHub Releases.
Na telefonu otevřete stažený .apk soubor a povolte instalaci z neznámých zdrojů.
SEND_SMS a READ_SMS, která vyžadují speciální review.
Sideloading APK z GitHub Releases je zamýšlený způsob distribuce.
Připojte telefon přes USB (se zapnutým USB laděním). Z kořene modulu
sms-gateway (extra/sms):
# Nejnovější release
./scripts/install-release.sh
# Konkrétní tag
./scripts/install-release.sh v1.3.0
# Stáhnout znovu i když je v cache
./scripts/install-release.sh --force
Stáhne app-release.apk z GitHub release (cache podle tagu ve složce
.release-cache/), nainstaluje přes adb install -r a udělí
oprávnění pro SMS limit. Vyžaduje gh (přihlášený) a adb —
žádný build toolchain není potřeba.
gh v PATH
Připojte telefon přes USB. Z kořene modulu sms-gateway (extra/sms):
./scripts/setup.sh # celý flow: závislosti, prebuild, build, instalace
./scripts/setup.sh --clean # prebuild s --clean (po změně app.json / nativního kódu)
./scripts/setup.sh --no-install # jen build, přeskočit ADB instalaci
Skript bootstrapuje nvm + Node 20 (nainstaluje, pokud chybí), inicializuje submodul
sms-gateway-app, najde Android SDK, nainstaluje závislosti, provede prebuild,
sestaví release APK, nainstaluje ho přes ADB a udělí WRITE_SECURE_SETTINGS.
git submodule update --init --recursive # stáhnout zdroje sms-gateway-app
cd sms-gateway-app
# Instalace závislostí
npm install
# Prebuild nativního projektu (nutné pro nativní moduly)
npx expo prebuild
# Spuštění na připojeném telefonu
npx expo run:android
# NEBO build release APK přes EAS
eas build --profile production-apk --platform android
gh release create v1.x.x ./build/*.apk \
--repo Varyshop/sms-gateway-app \
--title "v1.x.x" \
--notes "Release notes"
| Oprávnění | Účel |
|---|---|
SEND_SMS |
Odesílání SMS bez otevření aplikace |
RECEIVE_SMS |
Čtení příchozích SMS (detekce STOP) |
READ_PHONE_STATE |
Čtení čísel SIM karet |
READ_PHONE_NUMBERS |
Čtení telefonního čísla |
CAMERA |
Skenování QR kódu |
| Pole | Popis | Příklad |
|---|---|---|
| Name | Název telefonu | Telefon 1 - O2 |
| Phone Number | Hlavní číslo (mezinárodní formát) | +420777123456 |
| Phone Number 2 | Druhé číslo (dual SIM, volitelné) | +420608987654 |
| Daily Limit | Max SMS za den | 500 |
| SMS per Minute | Max SMS za minutu (rate limit) | 100 |
| Heartbeat Timeout | Po kolika minutách bez heartbeatu = offline | 5 |
{
"type": "sms_gateway",
"url": "https://vas-odoo-server.cz",
"api_key": "nahodny_bezpecnostni_token"
}
Ahoj {{ object.name }}! Mate 20% slevu na vse.
Nakupte na https://www.example.cz/akce
STOP pro odhlaseni
https://vas-server.cz/r/ABC123/s/42). Původní odhlašovací
odkazy se nahradí textem STOP pro odhlášení.
Použijte Odoo domain filtr pro výběr příjemců:
| Příklad filtru | Popis |
|---|---|
[("category_id", "in", [10])] |
Kontakty s kategorií ID 10 |
[("country_id.code", "=", "CZ")] |
České kontakty |
[("customer_rank", ">", 0)] |
Pouze zákazníky |
[("opt_out", "=", False)] |
Pouze ti, co se neodhlásili |
Každý gateway telefon může mít nastaven Partner Domain Filter. Tento filtr určuje, které SMS se budou přiřazovat danému telefonu na základě partnera příjemce.
sms.sms._send() přiřazuje SMS do fronty, hledá
dostupné telefony
domain_filter se ověří, zda
partner příjemce odpovídá filtru
| Scénář | Domain Filter | Vysvětlení |
|---|---|---|
| Telefon pro VIP zákazníky | [("category_id", "in", [10])] |
Pouze zákazníci s kategorií "VIP" (ID 10) |
| Telefon pro Prahu | [("state_id.name", "=", "Praha")] |
Pouze zákazníci z Prahy |
| Telefon pro všechny | (prázdné) | Přijímá SMS pro všechny partnery |
| Telefon pro firmy | [("is_company", "=", True)] |
Pouze firmy, ne fyzické osoby |
failure_type='sms_server'). Doporučujeme mít alespoň jeden
telefon bez filtru jako fallback.
sms.sms záznamy ve stavu outgoing
_send() přiřadí každé SMS gateway telefonu a změní stav na
pending
Pokud se SMS nevytvořily automaticky:
Kampaň lze pozastavit bez zrušení:
Všechny URL v SMS těle jsou automaticky převáděny na krátké trackovací odkazy:
| Fáze | Příklad |
|---|---|
| Původní text | Nakupte na https://www.example.cz/akce |
| Po konverzi | Nakupte na https://vas-server.cz/r/ABC123 |
| Po přiřazení SMS ID | Nakupte na https://vas-server.cz/r/ABC123/s/42 |
link.tracker záznam s krátkým kódem a UTM parametry
/s/{sms_id} pro identifikaci konkrétního příjemce
mailing.trace (který příjemce klikl)https://www.example.cz/akce?utm_campaign=Q1_2025&utm_medium=sms&utm_source=kampan
Automaticky přidávané při přesměrování:
| Parametr | Hodnota | Zdroj |
|---|---|---|
utm_campaign |
Název kampaně | mailing.mailing.campaign_id |
utm_medium |
"sms" | mailing.mailing.medium_id |
utm_source |
Název zdroje | mailing.mailing.source_id |
Systém podporuje dva způsoby odhlášení z SMS kampaní:
/sms-gateway/inbound
phone.blacklist
Odoo standardně generuje odhlašovací URL ve formátu
/sms/{mailing_id}/{trace_code}. Náš modul tyto URL
nahrazuje textem "STOP pro odhlášení", protože:
Odoo používá model phone.blacklist z modulu
phone_validation:
+420777123456)
SMS Marketing > Configuration > Phone Blacklist
| Metrika | Popis | Jak se měří |
|---|---|---|
| Sent | Počet úspěšně odeslaných SMS | sms.sms ve stavu sent |
| Delivered | Doručené SMS | = sent + opened + replied |
| Clicked | Příjemci co klikli na odkaz | mailing.trace s links_click_datetime |
| Click Rate | Procento kliknutí | (unikátní kliknutí / celkem odesláno) × 100 |
| Bounced | Neplatná čísla | mailing.trace ve stavu bounce |
| Failed | Chyba odeslání | mailing.trace ve stavu error |
Každý odkaz v SMS má vlastní link.tracker záznam:
| Limit | Kde se nastavuje | Popis |
|---|---|---|
| Daily Limit | sms.gateway.phone |
Max SMS za den na telefon. Počítadlo sent_today se
resetuje o půlnoci.
|
| SMS per Minute | sms.gateway.phone |
Max SMS za minutu. Aplikace počítá delay mezi SMS:
60000 / rate_limit ms.
|
| Android SMS limit | OS Android | Android standardně omezuje na ~30 SMS za 30 minut. Může být změněn v nastavení. |
rate_limit na každém gateway telefonu
(např. 100 SMS/min)
60000 / rate_limit milisekund
daily_limit, není mu přiřazena žádná
další SMS
adb shell settings put global sms_outgoing_check_max_count 10000
Systém podporuje telefony se dvěma SIM kartami:
phone_number a
phone_number_2
SimManager modulu
Všechny endpointy vyžadují hlavičku X-API-Key s platným API
klíčem gateway telefonu.
Odeslání heartbeatu a získání stavu fronty.
// Request
{
"phone_numbers": ["+420777123456", "+420608987654"],
"battery_level": 85,
"signal_strength": -70
}
// Response
{
"success": true,
"pending_count": {"+420777123456": 12, "+420608987654": 5},
"rate_limit": 100
}
Získání SMS čekajících na odeslání.
// Request
{"phone_numbers": ["+420777123456"], "limit": 20}
// Response
{
"success": true,
"sms_list": [
{
"id": 123,
"phone_number": "+420999888777",
"message": "Text SMS zpravy...",
"uuid": "abc-123-def",
"gateway_phone_number": "+420777123456"
}
]
}
Potvrzení stavu odeslání SMS.
// Request - uspech
{"status": "sent"}
// Request - chyba
{"status": "error", "error_message": "Permission denied"}
// Request - probihajici
{"status": "sending"}
// Response
{"success": true}
Nahlášení příchozí SMS (pro STOP detekci). Zpráva se uloží do modelu sms.gateway.inbound a pokud obsahuje STOP, číslo se přidá na blacklist.
// Request
{
"from_number": "+420999888777",
"message": "STOP",
"to_number": "+420777123456"
}
// Response
{
"success": true,
"blacklisted": true,
"partner_found": true
}
Hromadné nahlášení více příchozích SMS najednou. Od v1.2.0 aplikace posílá všechny přijaté
SMS (nejen STOP) na server. Server provádí deduplikaci podle kombinace from_number + to_number + message + received_at,
takže opakované odeslání stejných zpráv (např. při rescan inboxu) nevytvoří duplicity.
// Request
{
"messages": [
{
"from_number": "+420999888777",
"message": "STOP",
"to_number": "+420777123456",
"received_at": "2026-03-22T10:30:00Z"
},
{
"from_number": "+420111222333",
"message": "Dekuji za informaci",
"to_number": "+420777123456",
"received_at": "2026-03-22T11:15:00Z"
}
]
}
// Response
{
"success": true,
"processed": 2,
"duplicates_skipped": 0
}
Všechny příchozí SMS se ukládají do modelu sms.gateway.inbound:
| Pole | Typ | Popis |
|---|---|---|
from_number |
Char | Číslo odesílatele (E.164) |
to_number |
Char | Číslo gateway telefonu, na které SMS přišla |
message |
Text | Obsah SMS zprávy |
received_at |
Datetime | Čas příjmu zprávy na telefonu |
partner_id |
Many2one | Nalezený partner podle čísla odesílatele (může být prázdné) |
is_stop |
Boolean | Zda zpráva obsahuje klíčové slovo STOP |
is_blacklisted |
Boolean | Zda je číslo odesílatele na blacklistu |
gateway_phone_id |
Many2one | Odkaz na sms.gateway.phone |
Statistiky gateway telefonu.
// Response
{
"success": true,
"phones": [
{
"id": 1,
"name": "Telefon 1 - O2",
"phone_number": "+420777123456",
"phone_number_2": null,
"state": "online",
"sent_today": 142,
"daily_limit": 500,
"sent_total": 4582,
"pending_count": 23,
"rate_limit": 100
}
]
}
Registrace FCM tokenu pro push notifikace. Telefon pošle svůj FCM token po spárování nebo když se token obnoví.
// Request
{
"fcm_token": "dK3xP9...dlouhy_firebase_token"
}
// Response - uspech
{
"success": true,
"message": "FCM token registered"
}
// Response - FCM neni povoleno
{
"success": true,
"message": "FCM not enabled, token ignored"
}
Hromadné potvrzení stavu více SMS najednou. Efektivnější než jednotlivé /confirm/{sms_id} volání.
// Request
{
"results": [
{"sms_id": 123, "status": "sent"},
{"sms_id": 124, "status": "sent"},
{"sms_id": 125, "status": "error", "error_message": "Number unreachable"}
]
}
// Response
{
"success": true,
"processed": 3
}
Od verze v18.0.2.2.0 modul obsahuje modely pro SMS marketing přímo z mobilní aplikace. Admin definuje segmenty a šablony v Odoo, operátor je používá z appky bez přístupu do backendu.
Definuje skupinu příjemců (např. „Neobjednali 3 měsíce“).
| Pole | Typ | Popis |
|---|---|---|
name | Char | Název segmentu (překladatelný) |
code | Char, unique | Slug (no_order_3m, one_order_only, new_customers_30d) |
domain_filter | Char | Deklarativní Odoo doména, např. [("country_id.code", "=", "CZ")]. Pokud je nastavena, použije se místo code-based logiky. |
sequence | Integer | Pořadí v seznamu |
active | Boolean | Aktivní/neaktivní |
description | Text | Popis zobrazený v appce |
| Code | Název | Logika |
|---|---|---|
no_order_3m | Neobjednali 3 měsíce | SQL: partneři s objednávkou, ale žádnou za 90 dní |
one_order_only | Jediná objednávka | SQL: partneři s přesně 1 potvrzenou objednávkou |
new_customers_30d | Noví zákazníci (30 dní) | Domain: [("create_date", ">=", today - 30d)] |
| Metoda | Popis |
|---|---|
_get_domain() |
Vrátí základní Odoo domain segmentu. Použije domain_filter nebo dispatchuje na _domain_{code}(). |
_get_full_domain(phone, exclude_contacted_days) |
Single source of truth — skládá kompletní domain: segment + blacklist + phone filter + exclusion kontaktovaných. |
_get_storable_domain(phone, exclude_contacted_days) |
Vrací domain vhodnou pro uložení do mailing_domain. Deklarativní segmenty se uloží jako čitelná doména; SQL segmenty se pre-resolví na ('id', 'in', [...]). |
_resolve_recipient_ids(phone, exclude_days, limit) |
Resolví domain na seznam partner ID. Používá se pro počty a preview. |
_get_recipient_count(phone, exclude_days) |
Spočítá počet odpovídajících partnerů. |
_get_exclusion_domain(days) |
Vrátí deklarativní domain leaf ('stats_last_sms_days', '>', days) vylučující partnery kontaktované v posledních N dnech. Pokud je days <= 0, vrací prázdný seznam (filtr ignorován). Od v18.0.2.3.0 je plně deklarativní — žádný SQL na mailing_trace. |
_get_storable_domain()
kombinuje 3 deklarativní filtry do jedné uložené domény:
domain_filterdomain_filter z sms.gateway.phone('stats_last_sms_days', '>', exclude_contacted_days) ze šablonymailing.mailing.mailing_domain jako pure declarative Odoo domain —
Odoo ji znovu vyhodnocuje při každém odeslání, bez runtime SQL dotazů.
Exclusion leaf využívá computed field res.partner.stats_last_sms_days
s vlastním _search_stats_last_sms_days() handlerem (v18.0.2.4.0+),
který pro operátory >/>= vrací OR větev:
partneři se stats row splňující datum nebo partneři bez stats row
(('stats_id', '=', False)). Tím je zajištěno, že brand-new partneři
bez jakékoliv SMS historie splňují podmínku „nekontaktován v posledních N dnech“.
Předchozí fields.Integer(related='stats_id.last_sms_sent_days') varianta
NEfungovala, protože Odoo převáděl search na LEFT JOIN přes O2m, který NULL řádky
filtroval ven (viz changelog v18.0.2.4.0).
SMS šablona přiřazena ke gateway telefonu.
| Pole | Typ | Popis |
|---|---|---|
name | Char | Název šablony |
body | Text | Text SMS (může obsahovat {{object.name}}) |
phone_id | Many2one → sms.gateway.phone | Přiřazený telefon |
segment_ids | Many2many → sms.marketing.segment | Povolené segmenty |
default_limit | Integer (100) | Výchozí návrh počtu příjemců |
max_limit | Integer (500) | Hard cap na počet příjemců |
exclude_contacted_days | Integer (0) | Vyloučit partnery kontaktované v posledních N dnech |
| Pole | Typ | Popis |
|---|---|---|
gateway_phone_forced_id | Many2one → sms.gateway.phone | Vynucený telefon pro všechny SMS v kampani |
recipient_limit | Integer | Max počet příjemců |
marketing_template_id | Many2one → sms.marketing.template | Zpětný odkaz na šablonu |
created_from_app | Boolean | Vytvořeno z mobilní appky |
paused | Boolean | Pozastavená kampaň (neodesílá se, ale SMS jsou ve frontě) |
Endpointy pro mobilní appku umožňující vytvořit a sledovat SMS kampaně
bez přístupu do Odoo backendu. Všechny endpointy: POST,
auth přes X-API-Key, JSON body/response.
Vrátí šablony přiřazené k telefonu identifikovanému API klíčem.
// Response
{
"success": true,
"templates": [
{
"id": 1,
"name": "Akce tento tyden",
"body": "Ahoj {{object.name}}! Mate slevu 20%...",
"segments": [
{"id": 1, "name": "Neobjednali 3 mesice", "code": "no_order_3m"},
{"id": 2, "name": "Jedina objednavka", "code": "one_order_only"}
],
"default_limit": 100,
"max_limit": 500,
"exclude_contacted_days": 14
}
]
}
Pro každý segment spočítá počet odpovídajících příjemců (včetně phone domain filtru a exclusion).
// Request
{"template_id": 1}
// Response
{
"success": true,
"filters": [
{
"id": 1,
"code": "no_order_3m",
"name": "Neobjednali 3 mesice",
"description": "Partneri s objednavkou ale zadnou za 90 dni",
"recipient_count": 342
}
]
}
Náhled kampaně — počet příjemců a renderovaný text s ukázkovým partnerem.
// Request
{"template_id": 1, "segment_id": 1, "limit": 200}
// Response
{
"success": true,
"recipient_count": 200,
"preview_text": "Ahoj Jan Novak! Mate slevu 20%...",
"template_name": "Akce tento tyden",
"segment_name": "Neobjednali 3 mesice"
}
Vytvoří mailing.mailing s plným trackingem. Přijímá volitelný custom_body (úprava textu z appky) a send_now přepínač.
// Request
{
"template_id": 1,
"segment_id": 1,
"limit": 200,
"custom_body": "Upraveny text SMS...",
"send_now": true
}
// Response
{
"success": true,
"campaign_id": 42,
"recipient_count": 200,
"state": "sending"
}
send_now=false, kampaň se vytvoří ve stavu
in_queue s paused=true. SMS jsou ve frontě, ale neodesílají se
dokud operátor nestiskne „Odeslat ihned“ v appce (endpoint assign-sim).
domain_filter
se do mailing_domain uloží čitelná doména (např. [("country_id.code", "=", "CZ"), ...]).
Pro SQL segmenty se pre-resolví na kompaktní [("id", "in", [1, 2, 3, ...])].
Přiřadí gateway telefon a SIM k SMS v kampani. Používá stejnou logiku jako akce „Send via Gateway“ v Odoo.
// Request — jedina SIM (nebo auto-prirazeni bez SIM cisla)
{
"campaign_id": 42,
"mode": "single",
"sim_number": "+420777123456"
}
// Request — rozdeleni mezi vice SIM
{
"campaign_id": 42,
"mode": "split",
"sim_numbers": ["+420777123456", "+420608987654"]
}
// Response
{
"success": true,
"assigned": 200,
"message": "Assigned 200 SMS to phone"
}
outgoing/error (nepřiřazené) a pending bez SIMsms_provider='gateway', gateway_phone_id, gateway_sim_numbersplit rozděluje SMS round-robin mezi všechny SIMstate='sending', paused=FalseSeznam kampaní vytvořených z appky pro daný telefon.
// Response
{
"success": true,
"campaigns": [
{
"id": 42,
"name": "Akce tento tyden - 04.04.2026 10:30",
"state": "done",
"date_created": "2026-04-04 10:30:00",
"total": 200,
"sent": 195,
"pending": 0,
"error": 5,
"clicked": 42,
"total_clicks": 67,
"order_count": 8,
"revenue": 12400.0,
"optout": 3
}
]
}
Detail kampaně s marketingovými statistikami.
// Response
{
"success": true,
"id": 42,
"name": "Akce tento tyden - 04.04.2026 10:30",
"state": "done",
"total": 200,
"sent": 195,
"pending": 0,
"error": 5,
"clicked": 42,
"total_clicks": 67,
"order_count": 8,
"revenue": 12400.0,
"optout": 3,
"created_at": "2026-04-04 10:30:00"
}
| Pole | Popis | Zdroj |
|---|---|---|
clicked | Unikátní příjemci co klikli | mailing.trace s links_click_datetime |
total_clicks | Celkový počet kliknutí (včetně opakovaných) | link.tracker.click pres UTM campaign |
order_count | Počet objednávek z kampaně | sale.order s campaign_id = UTM kampan |
revenue | Celkový příjem z objednávek | Součet amount_total z matchujících objednávek |
optout | Počet odhlášených (STOP) | mailing.trace ve stavu cancel |
| Problém | Příčina | Řešení |
|---|---|---|
| Telefon je stále Offline | Heartbeat nedochází na server |
|
| SMS se neodesílají | Aplikace nemá oprávnění SEND_SMS | Otevře nastavení Android > Aplikace > SMS Gateway > Oprávnění > povolit SMS |
| SMS ve frontě ale žádný telefon nepřijímá | Žádný telefon neodpovídá domain_filter | Zkontrolujte domain_filter na gateway telefonech. Alespoň jeden by měl být bez filtru. |
| Všechny SMS mají stav error | Žádný gateway telefon není online | Ujistěte se že aplikace běží, je spárovaná a posílá heartbeaty |
| Daily limit dosažený příliš brzo | Počítadlo sent_today | Zvyšte daily_limit nebo přidejte další telefony. Reset je o půlnoci (cron). |
| Android blokuje SMS po 30 zprávách | Hardwarový limit Androidu |
Použijte ADB příkaz:
adb shell settings put global sms_outgoing_check_max_count
10000
|
| STOP nepřidá číslo na blacklist | Aplikace nemá oprávnění RECEIVE_SMS | Povolte oprávnění RECEIVE_SMS v nastavení Android |
| Odkazy v SMS nefungují | Špatná konfigurace web.base.url | V Odoo nastavte správně System Parameters > web.base.url (musí být veřejně dostupné) |
| Kampaň se neoznačí jako Done | Některé SMS stále pending | Zkontrolujte, že všechny SMS jsou ve stavu sent nebo error. Možná je kampaň pozastavená. |
| SMS se neodesílají při zamknuté obrazovce | Android ukončí aplikaci kvůli optimalizaci baterie |
|
| STOP SMS chybí v blacklistu | InboundSmsWorker selhal nebo není síť |
|
| Příchozí SMS nedochází na server | InboundSmsWorker používal špatnou autentifikaci (Authorization: Bearer místo X-API-Key) |
|
| FCM push nedochází | FCM není správně nakonfigurováno |
|
# Test heartbeat
curl -X POST https://vas-server.cz/sms-gateway/heartbeat \
-H "Content-Type: application/json" \
-H "X-API-Key: VAS_API_KLIC" \
-d '{"phone_numbers": ["+420777123456"]}'
# Overeni pending SMS
curl -X POST https://vas-server.cz/sms-gateway/pending \
-H "Content-Type: application/json" \
-H "X-API-Key: VAS_API_KLIC" \
-d '{"phone_numbers": ["+420777123456"], "limit": 5}'
# Test inbound STOP
curl -X POST https://vas-server.cz/sms-gateway/inbound \
-H "Content-Type: application/json" \
-H "X-API-Key: VAS_API_KLIC" \
-d '{"from_number": "+420999888777", "message": "STOP", "to_number": "+420777123456"}'
Od verze v18.0.2.0.0 modul podporuje Firebase Cloud Messaging (FCM) pro okamžité probuzení telefonu při nových SMS ve frontě. Místo čekání na další polling interval telefon obdrží push notifikaci a okamžitě si vyzvedne čekající SMS.
| System Parameter | Typ | Popis |
|---|---|---|
sms_gateway.fcm_enabled |
Boolean | Zapne/vypne FCM push notifikace. Výchozí: False |
sms_gateway.fcm_credentials_json |
Text (JSON) | Inline obsah Firebase service account JSON souboru. Má přednost před fcm_credentials_path. |
sms_gateway.fcm_credentials_path |
Text (cesta) | Absolutní cesta k Firebase service account JSON souboru na serveru. Použije se pokud fcm_credentials_json není nastaven. |
pip install firebase-admin
Knihovna firebase-admin je automaticky nainstalována při instalaci modulu
(deklarována v external_dependencies manifestu). Pokud instalace selže
(např. chybí systémové knihovny), FCM funkce se tiše deaktivuje a telefony
pokračují v polling režimu.
tools/fcm_service.py
Hlavní funkce send_fcm_wake(phone) odešle FCM data message na registrovaný
telefon. Data message (na rozdíl od notification message) probudí aplikaci i na pozadí
bez zobrazení notifikace uživateli.
# Format FCM data message
{
"type": "sms_pending",
"phone_id": "7",
"timestamp": "1710751200"
}
| Pole | Popis |
|---|---|
type |
Vždy "sms_pending" — aplikace podle typu rozhodne akci |
phone_id |
ID záznamu sms.gateway.phone v Odoo |
timestamp |
Unix timestamp odeslání — pro deduplication na straně aplikace |
fcm_token na sms.gateway.phone) pokračují v polling režimu
bez jakékoliv změny chování. FCM je čistě aditivní vylepšení — žádný existující
telefon nepřestane fungovat.
Android agresivně omezuje pozadí aplikací kvůli úspoře baterie. Tato sekce dokumentuje strategie použité v SMS Gateway aplikaci pro spolehlivý provoz i při zamknuté obrazovce a Doze režimu.
Pro pravidelný polling (heartbeat + vyzvedávání SMS) aplikace používá
AlarmManager s přesným buzením namísto
ScheduledExecutorService nebo Handler.postDelayed().
| Přístup | Chování v Doze | MIUI/HarmonyOS |
|---|---|---|
ScheduledExecutorService |
Zastaven po ~1 min v Doze | Agresivně ukončen |
Handler.postDelayed |
Zastaven po ~1 min v Doze | Agresivně ukončen |
AlarmManager.setExactAndAllowWhileIdle |
Probudí zařízení z Doze | Funguje i na MIUI (s výjimkou baterie) |
SCHEDULE_EXACT_ALARM nebo USE_EXACT_ALARM.
Aplikace o něj žádá automaticky při prvním spuštění.
Zpracování příchozích SMS (detekce STOP) používá WorkManager
(InboundSmsWorker) namísto přímých HTTP volání z BroadcastReceiveru.
Důvody:
NetworkType.CONNECTED)
Aplikace používá PowerManager.WakeLock na dvou místech
pro zabránění uspání CPU během kritických operací:
| Komponenta | WakeLock tag | Trvání | Účel |
|---|---|---|---|
FcmMessageHandler |
sms-gateway:fcm-wake |
Max 60 sekund | Drží CPU vzhůru během poll + odeslání SMS po FCM wake |
SmsBroadcastReceiver |
sms-gateway:inbound-sms |
Max 30 sekund | Drží CPU během zpracování příchozí SMS a enqueue do WorkManager |
Foreground service notifikace používá kanál s IMPORTANCE_HIGH.
To zajišťuje, že Android nezabije službu ani při nedostatku paměti. Uživatel
vidí trvalou notifikaci ve stavovém řádku (požadavek Androidu pro foreground
service).
Při prvním spuštění aplikace žádá uživatele o výjimku z optimalizace baterie
(REQUEST_IGNORE_BATTERY_OPTIMIZATIONS). Bez této výjimky může
Android:
Aplikace od v1.2.0 obsahuje nativní metodu SmsModule.rescanInbox(days),
která přečte SMS inbox za posledních N dní (výchozí: 30) a všechny nalezené zprávy
odešle na server přes endpoint /sms-gateway/inbound-batch.
Tato funkce je dostupná uživateli přes tlačítko Znovu prohledat přijaté SMS v Nastavení > Služba. Server provádí deduplikaci, takže je bezpečné spustit rescan opakovaně.
// React Native volani
import { NativeModules } from 'react-native';
const messages = await NativeModules.SmsModule.rescanInbox(30);
// messages: Array<{from: string, body: string, date: number}>
// Nasledne se zpravy odeslou pres inbound-batch endpoint
Pro zvýšení nativního Android limitu na počet odchozích SMS je potřeba udělit
aplikaci oprávnění WRITE_SECURE_SETTINGS. Toto oprávnění nelze
udělit z GUI telefonu — vyžaduje ADB připojení:
# Primo pres ADB
adb shell pm grant com.varyshop.smsgatewayapp android.permission.WRITE_SECURE_SETTINGS
# Nebo pri buildu ze zdrojaku
yarn grant-permission
I s FCM může dojít k situaci, kdy push notifikace nedorazí (např. dočasný výpadek Google služeb). Proto aplikace udržuje heartbeat safety net:
pending_count pro každé číslopending_count > 0, aplikace okamžitě spustí poll cyklus/sms-gateway/pending nyní kontroluje zbývající denní/měsíční kapacitu telefonu před vydáním SMS. Pokud je limit vyčerpán, aktivní kampaně se automaticky pausnou a odpověď obsahuje limit_reached: truePOST /campaign/pause/<id> a POST /campaign/resume/<id>. Paused kampaň zůstává ve stavu sending s paused=True — její SMS se nevyzvedávají v pending endpointu (filtr m.paused = false v pickup query). Resume nastaví paused=False a kampaň pokračujePOST /campaign/archive/<id> nastaví active=False. V appce dostupné pouze v pokročilém módu pro dokončené kampaněin_queue, sending a paused kampaně. Přidány přepínače „Dokončené“ a „Archivované“ (archivované jen v pokročilém módu). API parametry include_done a include_archived v /campaign/listin_queue — dříve se znovu objevovalo po opuštění a návratu na obrazovkustats_last_sms_days (na res.partner i res.partner.stats) nyní vrací -1 pokud partner nemá last_sms_sent_date. Dříve vracelo 0, což UI nerozlišovalo od „kontaktován dnes“ — operátor nevěděl který je který. Hodnota -1 dává jednoznačný signál v form view i v exportech. Search sémantika zůstává identická (> N stále zahrnuje nikdy kontaktované partnery přes OR větev, takže kampaně s exclude_contacted_days > 0 zahrnou brand-new partnery a vyloučí jen ty kontaktované v posledních N dnech).days=0, nikdy kontaktován → days=-1, před 3 dny kontaktován → days=3. Search leaf ('stats_last_sms_days', '>', 30): today match=0, never match=1, 3d_ago match=0 — přesně podle záměru (kampaň zahrne nikdy-kontaktované + staré, vyloučí dnes + nedávné).res.partner.stats_last_sms_days byla related field přes O2m stats_id, takže Odoo při search převáděl doménu na LEFT JOIN res_partner_stats a NULL řádky (partneři bez stats row) vypadávali. Kampaň s exclude_contacted_days=30 tak nedostala ani jednoho nikdy-nekontaktovaného partnera — přesný opak zamýšleného chování.
_search_stats_last_sms_days handler>/>= vrací OR větev: partneři se stats row splňující datum nebo partneři bez stats row (('stats_id', '=', False))('stats_last_sms_days','>',30) vrátilo 5 026 partnerů, po fixu 14 467 (9 441 nikdy kontaktovaných + 5 026 starých)EXPLAIN ANALYZE: 8.98 ms execution (limit=20), parciální index mailing_trace_sms_res_id_write_date_idx je index-only scan, 20 lookupů místo sekvenčního skenu — potvrzuje proveditelnost na cílových 100k+/10k+ scale targetech._update_last_sms_sent zpracoval 14 750 partnerů za 1.19 s přes execute_values bulk UPDATE.sync_attempts (0–2 vždy, 3–5 ~30s, 6–9 ~5min, 10+ ~30min)POST /sms-gateway/reconcile — vrací already_confirmed_ids, stuck_ids, not_found_ids pro synchronizaci při startu appkyunsynced_count: Server loguje varování pokud nesynchronizovaných SMS > 10AtomicBoolean isPollingActive chrání před překrývajícími se poll cyklySELECT ... FOR UPDATE v _update_gateway_status zabraňuje TOCTOU racesentReceiver: getOrPut → null-check (ignoruje pozdní callbacky po sweepu)sweepStaleTrackers vyžaduje striktní rovnost sentParts == totalParts && failedParts == 0_cron_reset_stuck_gateway_sms resetuje SMS uvízlé > 30 min zpět na pending'failed' → 'error' (valid Odoo selection)_get_exclusion_domain() nyní vrací
('stats_last_sms_days', '>', days) místo SQL dotazu nad mailing_trace.
Exclusion je součástí uloženého mailing_domain a Odoo ho aplikuje v jednom search dotazu
spolu se segmentem a phone filtrem — odpadají obří ('id', 'not in', [...]) klauzule
a runtime SQL v mailing.mailing._get_recipients()
/campaign/filters nyní pro každý segment zobrazuje
počet příjemců už po odečtení exclude_contacted_days (díky deklarativní leaf se propaguje automaticky)
_search_last_sms_sent_days pro operátory >/>= nyní zahrnuje partnery bez SMS historie (last_sms_sent_date IS NULL)res.partner.stats.last_sms_sent_date se nyní aktualizuje v reálném čase při každém úspěšném odeslání SMS (_touch_last_sms_sent volaný z sms.sms._update_gateway_status). Dříve se pole aktualizovalo jen nočním cronem, což způsobovalo duplicitní SMS stejným příjemcům v rámci jednoho dne — exclusion filtr viděl zastaralé hodnoty_update_last_sms_sent nyní čítá z mailing_trace stavy ('pending', 'sent', 'open', 'reply') místo pouze 'sent' (v Odoo 18 'sent' znamená „doručeno“, nikoliv „odesláno“)/sms-gateway/pending nyní těsně před vydáním SMS telefonu canceluje záznamy, jejichž příjemce byl mezitím kontaktován jinou kampaní v rámci exclude_contacted_days okna. Řeší race condition: kampaň A vytvořena v 10:00 s 10 000 příjemci, před dokončením odesílání B pošle SMS některým z týchž příjemců v 11:00. Kontrola je optimalizována pro velké databáze (100k+ kontaktů, 10k+ na kampaň):
limit SMS (typicky 20) seřazených ORDER BY id ASC — stejně jako pickup query. Nekontroluje celou kampaň, pouze další batchmailing_trace s filtrem mt.mass_mailing_id != s.mailing_id (ne self)mailing_trace_sms_res_id_write_date_idx vytvořený v post_init_hook na sloupcích (res_id, write_date) s WHERE klauzulí trace_type='sms' AND trace_status IN ('pending','sent','open','reply') — EXISTS je index-only scanlimit index lookupů (<1 ms) místo sekvenčního skenu celého mailing_tracefailure_type='sms_duplicate' — viditelné v Odoo UIRecompute Last SMS Sent na modelu res.partner (dostupná z list/form view) — manuální přepočet last_sms_sent_date z mailing_trace. Paralelně k existující akci Recompute Order Statsstats_last_sms_sent a stats_last_sms_days, takže je vidět proč je partner v/mimo exclusion filtrcampaign/status/campaign/list nyní počítá trace_status IN ('pending', 'sent', 'open', 'reply') stejně jako Odoo UIsend_now=true už nezobrazujesms.marketing.segment s deklarativními i SQL-based doménami (3 výchozí segmenty)sms.marketing.template přiřazené ke gateway telefonům('id', 'in', [...])sms.gateway.phone.domain_filter se automaticky kombinuje s doménou segmentu v _get_full_domain()mailing_traceemail_from NOT NULL chyba při vytváření SMS kampaněgateway_phone_forced_id jako fallback v sms.sms._send()SMS Gateway v18.0.2.6.0 • VaryShop • 2026