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:

  1. 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.
  2. OpenWeatherMap fetch v sh_context_full_v1.js. HTTP volání na api.openweathermap.org s hardcoded GPS souřadnicemi Semily (50.6012, 15.4950). Klíč v Homey proměnné. Funkční, ale duplicate.
  3. 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 — duplicate
  • fetchOWMWeather() v sh_context_full_v1 (−2 868 znaků dead code)
  • Proměnná sh_owm_api_key
  • Konstanta WX_LAT, WX_LON v 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

  1. 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.
  2. 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.
  3. „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".
  4. 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.