Last updated: 2026-10-10
ClothesCast is a daily weather-insight app for Android. This policy describes what data the app handles, where it goes, and what control you have over it.
Geocoder API to look up the city name shown next to
the date. If you search for a place name (from the Settings
location picker), the text you type is also sent to Open-Meteo
so it can suggest matching places.
That is the only user-content data the app sends off your device
by default.has_events boolean — is
published as a retained MQTT message to a broker you configure —
typically the Mosquitto add-on running on your own Home Assistant. The
broker host and credentials are entirely yours; nothing ClothesCast
sends goes to a service the developer operates. See “Smart Home / Home
Assistant bridge” below for what’s in the payload.ClothesCast is an open-source Android app developed by Mikel Ward. The source code is at https://github.com/mikelward/clothescast.
ACCESS_COARSE_LOCATION).Geocoder
API on Play Services devices — receives the coarse coordinate so
we can look up the city name (e.g. “London”) and the country code
(used to pre-select holidays in your country). On AOSP / non-Play
devices Geocoder reports no backend and we skip the lookup;
the Today header falls back to a date-only label.None of these services receives an account, device identifier, or any other payload beyond the items listed above.
CalendarContract only when you grant
READ_CALENDAR and enable the calendar extras setting.has_events — no
titles, times, or locations) is published to your own broker. See “Smart
Home / Home Assistant bridge” below.Where it goes: Detected event titles — including contact names on birthday events — stay on your device. They are shown only in the themed banner on your screen: never read aloud through online TTS, never sent to Gemini, and never written to Firebase Analytics or Crashlytics.
One narrow exception, and only if you use the online (Gemini) voice: on a day with a detected birthday, the spoken forecast opens with a generic “Happy birthday” — a fixed phrase from the app’s own translations, with no name, no title, and no time. So the request carries the fact that some birthday falls today; it carries nothing about whose, or what the event was called. Detected public holidays from your calendar get no greeting at all — those titles are never spoken. On the device voice nothing leaves the phone either way.
READ_CALENDAR, prompted by
the toggle itself. Revoking permission later from system Settings
doesn’t auto-flip the toggle off; the reader simply returns no events
until permission is restored.cacheDir/diag.log
(plus one rotated predecessor cacheDir/diag.log.1, capped at
~200 KB each). Lines may include the rendered insight prose
(“Insight delivered for …”) and device-resolved coordinates (“Using
device-resolved location at lat, lon”). At each launch the log also
records why ClothesCast’s previous runs ended — Android’s own
reason (a crash, an out-of-memory reclaim, an app update, the system
stopping it), the exit code or signal the process ended on, how
important Android considered the app at that moment, when it
happened, and the system’s own
short description of it — together with
when ClothesCast was installed and last updated. That is the app’s
account of itself rather than anything about you: an insight that never
arrived looks the same whether the fetch failed or the process was
killed before it ran, and without this there is nothing afterwards to
tell those apart. Android’s description text is written by the system
and can name the component that stopped the app — for an update, the
installer. If the app crashes, the stack trace is written to the
same on-device log. The local bug-report payload includes the newest
lines of that log that fit a report short enough to paste whole, plus
your current settings (schedule, units, TTS choice, clothes rules) and
the most recently rendered insight prose.---- process start <version> ---- marker line at each launch lets
readers see where the current process began.now/timestamp topic carries the epoch-millis time of the most
recent publish so a consumer can tell a fresh forecast from a retained
one. A has_events topic carries a single boolean — true/false for
whether that window has a located calendar event — so an automation can
tell a deliberately-quiet evening (when “notify only on event nights”
keeps the phone silent) from an outage. It’s the only calendar-derived
field here, and it carries no event titles, times, or locations. The
payload includes any calendar-event extras clause that fires
(e.g. “Bring a jacket for your concert tonight”) when you have the
calendar extras enabled, because that clause is part of the rendered
sentence. No coordinates, no API keys, no settings values, no device
identifiers travel with any of these messages, with one exception you
choose: the optional Name on the Smart Home page.clothescast/alexs) while you leave the topic field empty. Leave it blank
and none of this happens. It is stored on your device and goes to the
broker and display you configured — and, because a bug report lists your
MQTT topic prefix and whether it comes from the name, into any bug report
you choose to send while the prefix is name-derived._home-assistant._tcp. and _mqtt._tcp.) over your local
network multicast group so it can offer to pre-fill the broker
host. Nothing about you or your forecast is included in the queries
— they’re indistinguishable from any other mDNS browser on your
network — and discovery stops as soon as you leave the page or pick
a result. No discovery happens unless you tap the button.Online TTS (the Gemini voice) has two paths.
a1B2c3… — that is not linked to your name,
email, phone number, device, or location, and is never used for
advertising or tracking across apps. It exists only so the proxy
can count how many free clips this app install has used today. On
the shared path your data is handled by ClothesCast and Google
servers.Authorization header, which the
proxy verifies to read the identifier — the client cannot forge or
swap it), a Firebase App Check attestation token (proves the request
came from a genuine install — verified in-memory and discarded), and
the model name. During the current rollout the app also sends a
Firebase Installation ID for backward compatibility (so older proxy
versions still recognize the install); it is likewise an anonymous,
random identifier with no personal information, and is dropped once
the rollout completes. No coordinates, calendar metadata as fields,
user account, advertising ID, or hardware / device identifiers are
added.quota/<your anonymous ID>
with a first-use timestamp, a successful-call counter, and a
most-recent-use timestamp, capped at 5 successful clips per UTC day.
Used to enforce the free daily limit and for capacity planning. Not
used for advertising or cross-app tracking. Your anonymous
identifier resets if you reinstall the app or use “Clear data” in
Android Settings.ClothesCast talks to these services on your behalf. Their own privacy policies apply to anything they receive:
| Service | What we send | When |
|---|---|---|
| Open-Meteo | Coarse coordinate (forecast); your typed search text when you use the Settings location picker | Always for forecast; only when you search for a place name |
Google’s geocoding service (via Android’s Geocoder on Play Services devices), governed by Google’s Privacy Policy |
Coarse coordinate | Always on Play Services devices (city / country lookup for the Today header); skipped on AOSP devices |
| Google Weather API (Google Maps Platform), governed by Google’s Privacy Policy | Coarse coordinate and your own Gemini API key | Only if you enable the “Google” forecaster on the Forecasters page and have set your own Gemini key |
| Google Firebase Authentication | An App Check attestation, to mint and refresh an anonymous identifier (no name, email, or password) | First use of the shared-key path, then periodic token refreshes |
| ClothesCast TTS proxy (Cloud Function, ClothesCast-operated) | The short rendered insight sentence, an anonymous app identifier (as a signed Firebase ID token; plus a Firebase Installation ID during the current rollout), a Firebase App Check token, and the Gemini model name | Online TTS with the shared key (default) |
| Google Gemini API | The short rendered insight sentence (forwarded by the proxy, or sent directly when you use your own key) | Online TTS, either path |
| Your self-hosted MQTT broker (e.g. Mosquitto inside Home Assistant) | The short rendered insight sentence; the outfit-card image, TTS audio, and muxed MP4 on sibling topics; an update timestamp; and a calendar-derived has_events boolean (no event titles, times, or locations) — all as retained MQTT messages |
Only if you opt in to the Smart Home bridge and configure a broker |
| Analytics / crash-reporting service (e.g. Firebase Crashlytics + Google Analytics for Firebase) | Aggregate usage events and crash diagnostics — see “Analytics and crash reporting” below for what’s in and out | Only if you turn on crash and usage reporting in Settings → Privacy, plus the one upgrade exception described there |
These providers act as service providers fulfilling a single request and returning the result. When the ClothesCast TTS proxy is in the path, it adds the anonymous identifier and App Check token described above; when you use your own Gemini key, the request goes straight to Google and no anonymous identifier is attached.
Note on Gemini API: request inputs are not retained for training by default. See the provider’s policy linked above for the authoritative terms.
READ_CALENDAR in Android Settings, or turn off
“Use calendar events” in ClothesCast’s Settings.To spot bugs and decide which features are worth keeping, ClothesCast can send two kinds of payload to a third-party reporting service (e.g. Firebase Crashlytics + Google Analytics for Firebase, or equivalent).
This is off until you turn it on, in Settings → Privacy. Both SDKs are started with collection disabled, before any of the app’s own code runs, so on a fresh install nothing is sent and nothing is queued to be sent.
Being exact about one thing, because “off” does not mean the same for both: analytics collects nothing at all, but the crash reporter still writes a crash to your phone even while it is off — turning collection off stops it sending, not catching. Such a report is never sent, and it is discarded the moment you first turn the switch on rather than being released. The goal, once you do turn it on, is to inform product decisions, not to identify you.
What’s sent, once it is on:
open_meteo_forecast, google_weather), the HTTP status code, an outcome bucket
(success / http_error / timeout / network_error /
other_error), and the request latency in milliseconds. No URL
parameters, no request or response bodies — the endpoint identifier
is a fixed string from a short enum, not a captured URL. Used to spot
rate-limit hits and transient server issues in aggregate. Failures
that look like “device offline / airplane mode” are filtered out
before the event is sent so the stream reflects real provider issues
rather than the user’s local radio state.today / tonight) and two delay numbers in milliseconds — alarm
fire delay (how late the OS released the alarm after the scheduled
trigger, an indicator of Doze deferral) and total delay (how late
the notification reached the system, which also folds in WorkManager
waits for connectivity). No timestamps, no location, no calendar or
insight content. Used to spot regressions in delivery punctuality.
Powered-off / airplane-mode misses simply don’t appear in the
stream — the event only fires when a notification is actually
posted, so unfired alarms aren’t reported.today / tonight), an outcome bucket (success, forecast_error,
no_location, cancelled, or other_error), and the wall-clock
latency from worker start to terminal result in milliseconds. No
forecast content, no insight prose, no location coordinates, no error
messages. Used to spot regressions in delivery reliability (“what
percentage of scheduled refreshes actually completed today?”).
WorkManager retries between transient failures (no connectivity, 5xx
from upstream) aren’t reported individually — only the terminal
outcome the user actually sees.all_defaults flag;
per-category integer Celsius deltas from the default threshold for the
temperature rules (sweater / jacket / coat / gloves / shorts),
clamped to ±5°C (so e.g. moving the jacket threshold from 10°C down to
8°C reports -2); and, for the two rain-gear rules (umbrella,
rain jacket), a signed percentage-point delta of their rain-probability
gate from the default, rounded to the nearest 10 and clamped to ±50 (so
raising the umbrella gate from 20% to 60% reports +40). A rain-gear
rule whose only change is its weather-code floor (drizzle / rain / off)
still shows up in the customised-categories list, but the floor value
itself is never sent. No raw thresholds, no weather-code values, no
user-added rule items.What’s not sent — these are hard limits, not “best-effort”:
It is off until you turn it on, and you can turn it back off. Settings → Privacy has the switch. Leave it alone and nothing is sent — with one exception, if you are updating from a version where reporting was on: the reporting libraries start before any of the app’s own code, so a report they were already holding — or an event they record themselves in that moment — can still go before the app turns them off. The app does that on its first launch after the update, and they start off from then on — unless that launch ends before the app gets that far, in which case the next one tries again. A fresh install is never in this position. The Changelog entry for 2026-08-28 describes it in full.
Turn it on and then off again and the app stops collecting, resets the analytics identifier so a later re-enable starts a new stream rather than resuming the old one, and asks the crash reporter to discard the reports it is still holding.
Two things worth being exact about. If the app crashes while reporting is off, the reporting library still writes that crash to your phone — it is never sent, and it is discarded the moment you first turn the switch on, rather than being released. And within a period you had switched on, anything already queued can’t be recalled, and a crash or event landing in the moment you flip the switch is handed over before the new setting reaches the library; either of those may still go.
The reporting service receives only what’s described above and is bound by its own privacy policy (linked from the third-party services table above once a specific provider is chosen). ClothesCast is open source, so you can audit exactly what’s instrumented at https://github.com/mikelward/clothescast.
ClothesCast is not directed at children under 13 and does not knowingly collect personal data from them.
If this policy changes, the updated version is committed to https://github.com/mikelward/clothescast/blob/main/PRIVACY.md and the “Last updated” date at the top reflects the change. A short summary of each material change is added to the Changelog below, and material changes are also noted in the app’s release notes.
The full revision history of this file is viewable on GitHub: View all changes to PRIVACY.md →
Open an issue at https://github.com/mikelward/clothescast/issues or email the address listed on the Play Store listing.
2026-10-10 — Added the optional Smart Home Name. When set, it appears in the outfit-card image title, the Home Assistant device name and (while the topic field is empty) the MQTT topic prefix, and so reaches your own broker and smart display. Blank by default.
2026-08-28 — Crash and usage reporting is now off until you turn it on. It used to be on by default, with a one-time banner telling you how to switch it off. Both SDKs now start with collection disabled, before any of the app’s own code runs, so on a fresh install nothing is sent and nothing is queued to be sent. An existing install that never made a choice is treated as not having consented, so reporting stops there too until it is turned on. The Today banner is now an invitation rather than a notice.
Existing installs are migrated, not grandfathered. An install that had never made a choice is treated as not having agreed: on its first launch after the update the app turns both reporting SDKs off and discards whatever they were holding, rather than leaving them as they were. That step is necessary rather than belt-and-braces — the crash reporter keeps its own copy of the collection setting, and it outranks the app’s new default.
One exception, and only if you had reporting on before: the reporting libraries start before any of the app’s own code, so for the moment between them starting and the app turning them off, a report they were already holding — or an event they record themselves in that moment — can still go. Nothing the app itself records reaches that window: everything that produces usage events waits until the stored choice has been applied to both libraries. That moment is the first launch after the update, and they start off from then on — but a launch that ends before the app reaches that step leaves them on, so the next launch opens the same moment again and turns them off there. There is no such window on a fresh install, where they have never been on.
A crash captured before you first turn it on is discarded, not sent. Discarding held reports is new in this release, in both directions: previously the switch set the two collection flags and nothing else, so turning reporting off left whatever was already captured on the phone. Now turning it off discards what is still held, and turning it on for the first time does too — because switching the reporter to “don’t send” never stopped it writing crashes to the phone.
Turning it off after a period with it on also resets the analytics identifier; previously the switch set the collection flags and nothing more, and those take effect only from the next launch. One limit remains and applies only to a period you had switched on: a crash report or usage event already queued during that period cannot be recalled.
has_events boolean (true/false) on each window’s topic
(and the now mirror): whether that forecast window has a located
calendar event, so a Home Assistant automation can tell a deliberately-
quiet evening (when “notify only on event nights” keeps the phone silent)
from an outage. This is a new calendar-derived field crossing the device
boundary — but only to your own broker, only when you have both the
calendar extras and the bridge enabled, and it carries no event titles,
times, or locations, just the single boolean. See the updated “Smart Home
/ Home Assistant bridge” and “Calendar events” sections above.clothes_rules_snapshot analytics event gained one aggregate param,
rain_jacket_delta_pct: the rain jacket’s rain-probability gate as a
signed percentage-point delta from its 50% default, bucketed exactly like
umbrella_delta_pct (rounded to the nearest 10, clamped to ±50, MISSING
if deleted). No raw thresholds, no weather-code values, no calendar /
location / insight content — same hard limits as the rest of the event.clothes_rules_snapshot analytics event gained
one aggregate param, umbrella_delta_pct: the user’s umbrella
rain-probability gate as a signed percentage-point delta from the 30%
default, rounded to the nearest 10 and clamped to ±50 (e.g. "+30",
"-20", MISSING if the rule was deleted). Same hard limits as the
rest of the event — no raw thresholds, no calendar / location / insight
content.installs/<install ID> to quota/<anonymous ID>. To obtain the
identifier the app now signs in anonymously to Firebase Authentication
(a Google service) — no name, email, or password, and no user-facing
account. See “Online TTS: shared key by default, your own key on
request” and the third-party services table above.daily_refresh (slot + outcome bucket + latency, one per terminal
result of the scheduled fetch + notify pipeline; WorkManager retries
between transient failures aren’t individually reported),
settings_snapshot (the resolved values of the non-voice user
settings — units, delivery mode, theme / palette / default-bottom,
the four schedule booleans, day counts, and schedule times bucketed
to the hour), and clothes_rules_snapshot (counts of customised
defaults and extra rules, sorted list of customised categories, plus
per-category integer Celsius delta from the default threshold,
clamped to ±5°C). No raw thresholds, no exact local times, no
calendar / location / insight content; same hard limits in
“Analytics and crash reporting” apply.cacheDir/diag.log (with one rotated predecessor, ~200 KB each)
instead of living only in a process-memory ring buffer. This lets
errors logged by background workers reach a bug report filed after
the OS later kills the process. The categories of data the diag log
may contain (rendered insight prose, device-resolved coordinates,
error / warning text) are unchanged — they were already shown in the
bug-report payload from the in-memory buffer; what changes is that
they now live on disk between launches, similar to the existing
crash file. Nothing new leaves the device; the bug-report share-sheet
flow is unchanged.app/google-services.json).