Fetch today's weather from Tempest and Ambient Weather stations and send it via iMessage
Install
npx skillscat add rolandcanyon-cmd/home-consciousness/morning-weather Install via the SkillsCat registry.
Morning Weather Report
Fetch current weather and forecast from Tempest station, plus indoor/outdoor temperature, pool temperature and battery status from the Ambient Weather REST API. Send a formatted morning greeting via iMessage.
Steps
Look up the Tempest station URL from FunkyGibbon via the
kittenkongMCP tools (source of truth): callsearch_entitieswith query "tempest" and readstation_urlfrom the matching device's content.- There is deliberately NO REST fallback. Everything talks to FunkyGibbon through MCP only — the graph REST endpoint (
/api/v1/graph/search) was removed in the-goodies v0.8.0 (2026-09-13) and must never be called directly by this skill, even as a fallback. An earlier version of this file had a "query the API directly" fallback step; that was a design violation (operator correction, 2026-09-27) and has been removed, not just fixed. - Root cause of the 2026-09-23 to 2026-09-27 silent-report outage:
kittenkongwas configured in.claude/settings.jsonbut never actually added to.mcp.json— the file Claude Code actually reads for this project's MCP servers — so the tool never existed in any session, including this job's. Fixed 2026-09-27 by adding it to.mcp.json(verified the server starts cleanly with itsFUNKYGIBBON_TOKENenv var). Ifsearch_entitiesis ever unavailable again, that means the MCP wiring broke again — do not paper over it with a REST call or a hardcoded URL; surface the failure plainly in the report and stop after step 9 (e.g. "couldn't reach FunkyGibbon for the station lookup — MCP tool unavailable, check.mcp.json") so the wiring gap gets caught immediately instead of silently degrading.
- There is deliberately NO REST fallback. Everything talks to FunkyGibbon through MCP only — the graph REST endpoint (
Extract current outdoor weather data from Tempest using browser_snapshot:
- Current temperature (e.g., "56°")
- Feels like temperature (e.g., "Feels Like 56°")
- Condition (Partly Cloudy, Clear, etc.)
- Humidity percentage (e.g., "87% Humidity")
- Wind speed and direction (e.g., "W 2 mph")
Extract today's forecast from Tempest:
- Look for "Today" in the forecast cards
- Today's high and low temperatures
- Condition forecast (e.g., "Partly Cloudy")
- Precipitation chance (percentage)
Get Ambient Weather readings via the REST API (NOT the browser):
node .claude/scripts/ambient-weather.mjs --jsonThe script reads
ambient_api_key/ambient_app_keyfrom the encrypted vault, retries the API's ~1-req/sec rate limit, and never prints credentials. It returns:devices[].readings.indoorTempF,.indoorHumidity,.indoorFeelsLikeF— indoor consoledevices[].readings.outdoorTempF,.outdoorHumidity,.outdoorFeelsLikeF— outdoor air (API slottemp1f)devices[].readings.poolTempF— the pool (API slottemp2f)- top-level
poolTempFandpoolSensorOffline— see step 5 lowBatteries[]— only non-empty when a battery is actually LOWpm25Category— air-quality alarm field, sourced from the PurpleAir Flex (not the Ambient station), see step 6.
Ambient does not return the dashboard's custom sensor labels, so the slot→name mapping lives in
SENSOR_LABELSin the script.NEVER navigate to ambientweather.net/dashboard with Playwright. That path is permanently broken: the isolated browser profile has no saved login, and the macOS Passwords app is unreadable by any CLI. See
[[known-macos-passwords-app-unreadable]].Pool: report it only when
poolSensorOfflineis false. When the sensor's battery dies, slot 2 vanishes from the API entirely andpoolSensorOfflinegoes true — then say so plainly ("pool sensor offline") or omit the line. Never substitute the outdoor sensor. Slot 1 is outdoor air; reporting it as the pool would be a fabricated reading. (Sanity check: the pool probe reports temperature only. If a temp reading carries humidity, it is NOT the pool.)Air quality: report it every day, not just when it's bad. The old "Roland Canyon PM2.5" Ambient station was drifted/faulty (operator-confirmed 2026-07-08) and was dropped as an air-quality source entirely (CMT-009, 2026-07-13). It has been replaced by a PurpleAir Flex, read directly off its own LAN JSON endpoint — no cloud dependency.
ambient-weather.mjscross-checks its two laser channels (A/B) against each other, then corroborates against a regional reference (Open-Meteo) before settingpm25Category.Report air quality whenever
pm25Categoryis non-null — Good included (as of 2026-07-15, operator asked to see it as a standing line, not just when it's bad). Ifpm25Suspectis present instead (channel disagreement or drift detected), orpm25Unavailableis present (PurpleAir Flex unreachable), do NOT report an air-quality line — the fault is visible in the JSON but stays out of the user-facing message.Battery status: mention ONLY if
lowBatteries[]is non-empty. Do not say "all batteries OK" in the message. Entries tagged(suspect device)come from the faulty PM2.5 station — don't alarm on those.Note: the Ambient station also reports outdoor temp/humidity, so it can cross-check or stand in for Tempest if the Tempest page fails to load.
Format a friendly morning message with the weather data
Send via iMessage using the standard relay script (never
imsg send— that binary/command does not exist on this system):cat <<'EOF' | .claude/scripts/imessage-reply.sh "+14084424360" MESSAGE EOFThis step is MANDATORY even in a degraded run. If step 1-7 partially or fully failed, still compose and send whatever is available, explicitly naming what's missing (per the existing "if the Ambient script fails, send anyway" rule below) — never let the job exit having sent nothing. A silent no-op is worse than a message saying "couldn't reach the Tempest station this morning."
Output Format
🌤️ House Weather - [Day, Mon DD]
🏠 Indoor: [indoorTempF]°
🌡️ Outdoor: [Outdoor Temperature]° (Feels like [Feels Like]°)
💧 Humidity: [Outdoor Humidity]
Today's Forecast:
High [High]° / Low [Low]°
[Precipitation]% chance of rain
[🏊 Pool: [poolTempF]° ← ONLY when poolSensorOffline is false. NEVER use outdoorTempF here.]
😷 Air quality: PM2.5 [pm25] — [pm25Category] ← ALWAYS when pm25Category is non-null (Good included, as of 2026-07-15); omit only on pm25Suspect/pm25Unavailable
[⚠️ Low battery: [lowBatteries] ← ONLY when lowBatteries is non-empty]
Good morning!Lines in [brackets] are conditional — omit the whole line when the condition isn't met. The air-quality line now appears every day whenever pm25Category is non-null, including "Good" (operator request 2026-07-15; PurpleAir Flex is the trusted source as of 2026-07-13). The pool line appears whenever poolSensorOffline is false. Never fill either gap with another sensor's reading.
If indoorTempF is missing from the Ambient reading, say so plainly (e.g. "70% humidity (temp reading unavailable this cycle)") rather than fabricating or silently dropping the line — this has been happening since 2026-07-12 (indoor humidity keeps reporting, indoor temp does not); see [[known-ambient-indoor-temp-sensor-dropout]].
Notes
- The Tempest page is JavaScript-rendered, so you need to wait for it to load
- Ambient Weather: use
node .claude/scripts/ambient-weather.mjs --json, never the browser. Credentials live in the encrypted vault; the dashboard login cannot be automated (see step 4). - Use the Playwright MCP browser tools (browser_navigate, browser_snapshot) for the Tempest page only
- Keep the message concise and friendly
- This is called by the morning-weather-report job at 7am daily
- If the Ambient script fails, send the report anyway with the Tempest data and say plainly which readings are missing — do not silently drop them
- Data sources:
- Tempest (browser): outdoor weather, forecast, wind, conditions
- Ambient Weather (REST API): indoor temp/humidity, OUTDOOR temp/humidity (slot 1), pool temp (slot 2 — absent when its battery dies), low-battery alerts.
- PurpleAir Flex (local LAN JSON, read inside
ambient-weather.mjs): PM2.5 air quality — replaced the faulty Ambient PM2.5 station (CMT-009, 2026-07-13).