Tři zdroje počasí → jeden. LOCATION_TIME_GUARD den.
Briefing mi v pondělí ráno recitoval, že je venku 13 °C. Ve skutečnosti bylo 6 °C. V noci ve 3:03 — kdy jsem ještě tvrdě spal — se sám spustil ranní briefing, rozsvítil světla a začal mi přehrávat zprávy do prázdné kuchyně. A kdyby to nestačilo: ten samý systém uměl říct „dobré ráno" v půl třetí v noci, pokud něco zachytilo pohyb v ložnici.
Tři problémy. Dva dny. Tenhle post je o tom, jak jeden den oprav vyústil
ve vrstvu LOCATION_TIME_GUARD a yr.no se stalo jediným zdrojem
pravdy pro lokaci, čas a počasí v celém systému.
Symptom #1 — Třináct stupňů v zataženém ránu
Briefing skript ráno přečetl: „Venku je 13 stupňů, polojasno." Vykoukl jsem
z okna. Mlha, déšť, 6 °C podle teploměru. Skript nelhal — jen četl prázdnou
cache (sh_weather_3d_json = []) a spadl na fallback hodnotu,
která byla hardcoded někde v kódu z dávných časů.
Co se ukázalo bylo horší než „cache je prázdná". Audit odhalil tři paralelní zdroje počasí v jednom systému:
- yr.no aplikace v Homey (norský met. ústav, free, native forecast). Měla v sobě 18 capabilities a fungovala perfektně — ale nikdo z ní nečetl.
-
OpenWeatherMap fetch v
sh_context_full_v1.js. HTTP volání naapi.openweathermap.orgs hardcoded GPS souřadnicemi Semily (50.6012, 15.4950). Klíč v Homey proměnné. Funkční, ale duplicate. -
Open-Meteo fetch v
sh_weather_fetcher_v1. Můj vlastní HTTP fetcher ze začátku týdne, který jsem přidal než jsem věděl, že yr.no už tam je. Lokace: Praha (default 50.0875, 14.4213).
Tři zdroje. Dvě různá města. Žádný z nich nebyl jednoznačně „ten správný". A nejhorší: dashboard mezitím v browseru přímo volal OpenWeatherMap (third HTTP fetch v UI vrstvě). Plus jeden hardcoded fallback v kódu pro případ, že se nic z toho nepovede.
Symptom #2 — Ranní rutina v 3:03
V úterý ráno se mi rozsvítila lampa a spustil briefing. Pohledem na hodiny: 3:03. Budík měl jít v 3:13 (po-pá). Nevstal jsem. Co se stalo?
Diagnostika přes EventLog:
03:03:00 motion bedroom (SNZB-03) — TRUE
03:03:02 sh_morning_brain_v1 → brain_invoked (motion, briefing2 audit)
03:03:02 sh_morning_brain_v1 → morning_score_bypass (score=85, bypass 180s debounce)
03:03:05 sh_audio_brain_v2 → audio_dispatch (radio_kitchen)
03:03:22 sh_morning_brain_v1 → morning_routine (9 actions executed)
03:13:01 alarm "Do Prace" fires
03:13:02 sh_morning_brain_v1 → alarm_late_ignored (already up)
Score scorer dal pohybu skóre 85 (z 100). Pro spuštění stačilo 8 raw bodů. Rozpis:
+2 cas>=03:00 (morning window)
+10 wake_confidence_score=50 ← bedroom motion
+1 jsem_vstal=yes
+3 open_space_dual (FP2+motion) ← BUG
+1 telefon_doma
= raw 17 → final 85 (multiplier ×5)
Klíčový hřebík: open_space_dual (FP2+motion) +3. FP2 je mmWave presence sensor v jídelně/kuchyni. Motion je Fibaro PIR. Zone Activity ukázal něco děsivého:
00:04 UTC snzb06p=TRUE fibaro_motion=TRUE idle_open_space_s=17161
00:14 UTC snzb06p=TRUE fibaro_motion=TRUE idle_open_space_s=17761
00:24 UTC snzb06p=TRUE fibaro_motion=TRUE idle_open_space_s=18361
...
01:03 UTC snzb06p=TRUE fibaro_motion=TRUE idle_open_space_s=20760 ← motion bedroom
01:04 UTC snzb06p=TRUE fibaro_motion=TRUE bedroom_active=TRUE
Oba senzory zůstaly TRUE celou noc. Idle counter rostl (= nikdo
se nehýbal), ale flag byl pořád TRUE. Stuck state z předchozího večera.
Když pak ráno SNZB-03 v ložnici detekoval mikropohyb (otoč v posteli? průvan?
kočka?), scorer si odškrtl všech 17 bodů a dal mi final score 85.
Bug nebyl v senzorech. Bug byl v tom, že scorer počítal stuck-TRUE flag jako čerstvý signál.
Symptom #3 — „Dobré ráno" v půl třetí
Posledním kapesním nepřítelem byla pevně hardcoded greeting funkce v briefing skriptu:
function buildIntro(hour) {
if (hour < 6) return 'Dobré ráno, Luďku.';
if (hour < 12) return 'Dobré ráno, Luďku.';
...
}
„Hour < 6" znamenalo, že kdyby se cokoli spustilo ve 2:30 (například výše popsaný false-positive wake), systém by zahájil ranní briefing slovy „Dobré ráno". V noci. Nedávalo to smysl.
Fix — Konsolidace na jeden zdroj pravdy
Krok 1: YR.no jako single weather source
Nový skript sh_weather_yr_bridge_v1.js: čte přímo z Homey Weather
device (yr.no driver), každých 10 minut, zapisuje do 22 proměnných:
sh_weather_temp_now, sh_weather_temp_feels_like,
sh_weather_temp_min_6h, sh_weather_temp_max_6h,
sh_weather_humidity, sh_weather_pressure,
sh_weather_wind_speed, sh_weather_wind_dir,
sh_weather_rain_1h, sh_weather_rain_6h,
sh_weather_cloud_pct, sh_weather_uv,
sh_weather_now_desc (anglicky), sh_weather_now_desc_cs
(česky), sh_sunrise_hhmm, sh_sunset_hhmm a další.
Český překlad popisu počasí v bridge skriptu („Cloudy" → „Zataženo", „Light rain" → „Slabý déšť", atd. — 30 hodnot v translation map). Lokalita se bere z yr.no device settings (Homey UI → Weather → Advanced → lat/lon/altitude). Když user opraví lokalitu v aplikaci, bridge to automaticky chytne — žádný HTTP fetch, žádný API klíč.
Smazáno:
sh_weather_fetcher_v1.js(Open-Meteo) + cron flow — duplicatefetchOWMWeather()vsh_context_full_v1(−2 868 znaků dead code)- Proměnná
sh_owm_api_key - Konstanta
WX_LAT,WX_LONv dashboards (legacy hardcoded GPS) - Direct OpenWeatherMap HTTP call v dashboard JS (browser už nepoužívá API klíč)
Krok 2: Score scorer fresh-trigger guard
sh_morning_score_v1 bumpnut na v1.6 s novými ochranami:
// Před: value === true OR ageS <= 300
const recent = (info, windowS) => info.exists && (info.value === true || info.ageS <= windowS);
// Po: jen čerstvá detekce (motion fired v posledních 300s)
const fresh = (info, windowS) => info.exists && info.ageS <= windowS;
// Plus stuck-presence detector
const isStuckPresence = (info) => info.exists && info.value === true && info.ageS > 3600;
Open-space bonus se uplatní jen pokud FP2 i motion fired v posledních 5 minutách. Pokud byly TRUE od večera (stuck), bonus se neuplatní:
+0 open_space_stuck_skipped (FP2_age=111315s, kitch_age=337s)
Plus PRE_ALARM_HARD_CAP — pokud je hodina 3 a nejbližší alarm víc jak 5 min daleko,
score se zastropuje na 6 (stav pre_wakeup, ne morning_confirmed).
Žádné spuštění routine.
Krok 3: LOCATION_TIME_GUARD vrstva
Tady byl ten hlavní zlom. Místo per-script time/phase logiky vytvořen centrální
skript sh_location_time_context_v1.js, který každých 5 minut počítá:
{
"timezone": "Europe/Prague",
"is_dst": true,
"utc_offset": "+02:00",
"local_time_iso": "2026-05-12T07:32:41",
"local_hour": 7,
"latitude": 50.6012, ← Semily
"longitude": 15.495,
"altitude": 489,
"city": "Semily",
"country": "CZ",
"sunrise_local": "05:14",
"sunset_local": "20:35",
"day_phase": "morning", ← smart-computed
"weather_source":"yr.no",
"weather_health":"ok",
"location_health":"ok",
"time_health": "ok",
"status": "ok"
}
Den se nedělí podle hardcoded hodin („morning = 5–10"), ale podle skutečného
východu a západu slunce z yr.no. morning = od (sunrise − 1h) do
(sunrise + 4h). night = mezi (sunset + 1h) a (sunrise − 1h).
V Semilech 12. května 2026: sunrise 5:14, sunset 20:35 — takže morning =
4:14–9:14, evening = 18:35–21:35.
Krok 4: Briefing greeting guard
function buildIntroByPhase(phase, hour) {
if (phase === 'night') return 'Vítej, Luďku.'; // NIKDY "dobré ráno" v noci
if (phase === 'morning') return 'Dobré ráno, Luďku.';
if (phase === 'day') return 'Dobrý den, Luďku.';
if (phase === 'evening') return 'Dobrý večer, Luďku.';
// Fallback: hour-based, ale 0–5 = "Vítej"
if (hour < 5) return 'Vítej, Luďku.';
...
}
Briefing teď čte sh_day_phase_context z master JSON a v noci řekne
„Vítej, Luďku" místo „Dobré ráno".
Krok 5: Morning brain phase_guard
Defensive vrstva v sh_morning_brain_v1. Pokud
sh_day_phase_context === 'night' a alarm dnes neproběhl, motion-trigger
se ignoruje:
if (event === 'motion_detected' && alreadyTriggered !== 'yes') {
if (dayPhase === 'night' && !alarmFiredToday) {
return 'PHASE_GUARD_BLOCKED';
// Log EventLog: event='morning_routine_blocked_wrong_phase'
}
}
Před / Po
| Aspekt | Před 12.5. | Po 12.5. |
|---|---|---|
| Zdrojů počasí | 3 (yr.no + OWM + Open-Meteo) | 1 (yr.no) |
| HTTP fetch calls | 2 backend + 1 browser | 0 (yr.no native v Homey) |
| API klíče potřebné | 1 (OpenWeatherMap) | 0 |
| Vars pro počasí | 3 (fragmentní) | 22 (rich + sunrise/sunset) |
| Day phase computer | 7+ skriptů, každý vlastní | 1 (LOCATION_TIME_GUARD) |
| „Dobré ráno" v noci | možné | blokované (phase guard) |
| Falsy-positive wake | FP2 stuck → score 85 → spustí | FP2 stuck → +0 → score 6 → block |
| Dead code v context_full | — — | −2 868 znaků smazáno |
Live briefing 12.5. 07:33
Reálný TTS výstup po deployi:
„Dobré ráno, Luďku. Je 7 hodin 33 minut, datum 12. května 2026. Venku je 5 stupňů, pocitově 2, zataženo. Během dne mezi 5 a 8 stupňů. Slunce zapadá v 20 hodin 35 minut. Zdraví systému je 92 procent…"
Briefing teď čte pět zdrojů, kterým rozumí. Teplota správně. Pocitově (rozdíl ≥2 °C). Min/max během dne. Východ/západ slunce (podle denní doby — ráno řekne vychází, večer zapadá). A v noci by řekl „Vítej, Luďku", ne „Dobré ráno".
Co jsem se naučil
- Smart home má tendenci kupit zdroje pravdy. Když přidám funkci, většinou si v hlavě nesouložím s tím, jestli už podobná data nikde v systému jsou. Audit po nějaké době ukáže, že stejný údaj počítám třikrát různě. Konsolidace bolí, ale šetří hodiny diagnostiky.
-
Stuck-TRUE flag není čerstvý signál. mmWave radary a PIR
sensory mohou zůstat TRUE celé hodiny. Když je beru jako „pohyb se právě
stal", lžu si. Rozdíl je v
ageS— kdy se hodnota naposled změnila, ne jakou má aktuální value. - „Dobré ráno" v noci je víc než UX bug. Je to symptom toho, že systém neví, kdy je den. Greeting funkce počítala hour. Ale 3:00 ráno má být night, ne morning. Centrální vrstva to ví správně — podle sunrise z yr.no, ne hardcoded „< 6".
- Memory pravidlo „REUSE FIRST". yr.no aplikace v Homey byla nainstalovaná od založení systému. Měla 18 capabilities a fungovala. Já jsem o ní nevěděl a přidal Open-Meteo HTTP fetch ze začátku týdne. Audit najde tyhle duplicity a vyhází je.
Co je dál
Centrální vrstva LOCATION_TIME_GUARD je nasazená. Refresh každých 5 minut
přes cron flow. Pokud status spadne na watch nebo warning,
další flow zaloguje varování. Pokud spadne na critical (například
yr.no offline na déle než 2 hodiny), nesmí se spustit plná ranní rutina bez
dodatečné kontroly. Safety guard.
Audit konzumentů — sedm dalších skriptů, které dosud měly vlastní time/phase
logiku — postupně migruje na single source. Sync proměnné sh_cast_dne
z sh_day_phase_context už dnes řeší většinu legacy readů bez
nutnosti per-script patche (NO REGRESSION pravidlo).
Zítra ráno (středa, alarm v 3:11 po-pá schedule) udělám automatický check — jestli phase_guard zachytí motion-trigger v night phase, jestli rádio hraje po druhém briefingu (TTS resume v1.9 — další bug, samostatný post), jestli briefing zase má správnou teplotu z yr.no.
Marker: LOCATION_TIME_GUARD_2026-05-12.
Source: sh_location_time_context_v1.js,
sh_weather_yr_bridge_v1.js,
sh_gemini_daily_briefing_v1.js v1.3.
Memory: reference_location_time_guard_2026-05-12.md +
reference_weather_yr_single_source_2026-05-12.md.