The control screen - monikapurpl3/breeze GitHub Wiki
One unit per screen — swipe left/right to switch, or use the arrows either side of the dots below. Each unit fills the screen, tinted to the active mode, and scrolls only with large text (see Accessibility).
- Temperature — a big readout with a stepped slider (0.5° steps) and − / + buttons on each side for single steps. Hold one to keep stepping; the new target is sent once, when you let go.
-
Indoor / outdoor —
I: 27.0 °C / O: 33.0 °Cwhen the unit has an outdoor probe, above a bar showing how the three temperatures relate. See the temperature bar below. - Mode — a colourful segmented picker: auto · cool · dry · heat · fan.
- Fan — a Low→High slider that detaches to Auto.
- Flap — one big pill split in two, vertical and horizontal, each toggled on its own: neither, either, or both.
- Eco / Turbo — large, colourful switches.
- Power — a big switch in the header: faintly red when off, faintly green when on, so the unit's state is never in doubt.
- The hourglass, just left of the power switch — a sleep timer, and with Breeze Core 4.2.0 a scheduled start as well.
The app works with TalkBack, Switch Access, Voice Access, large text and "Remove animations":
- Every control says what it is and how it is set. Power, eco, turbo and each flap half are switches that say whether they are on. The mode buttons and fan Auto say which one is chosen. The sliders read in degrees and in words ("Fan speed, medium"), not as a percentage. The − and + are "Lower the target temperature" and "Raise the target temperature". Each one is a single item, so Voice Access's "tap Power" lands on the switch.
-
Changes are spoken without moving focus:
- the new target temperature, whoever changed it;
- the unit refusing a change, and why (the message stays until you dismiss it while TalkBack is on);
- the server or a unit becoming unreachable;
- which unit you swiped to ("Unit 2 of 3, Bedroom").
- Pairing is spoken too: the code letter by letter, a warning 15 seconds before it runs out, "Code expired", and the first load once it is approved.
- Moving between units does not need a swipe: the arrows either side of the dots go to the previous and next unit. Each unit's name is a heading.
-
What you can see and touch:
- text in the mode colours is darkened, or lightened in the dark theme, to at least 4.5:1 contrast with what is behind it;
- the targets are at least 48 dp;
- from Android's "Large" font size up, the control screen scrolls rather than cutting off the bottom.
- Feel: a refused change has the two-beat failure vibration.
The pairing code still expires after 60 seconds, which the server decides.
These are checked by tests in the repository's test/ folder.
Every change is optimistic with haptic feedback: it reflects instantly, then reconciles with the server. The screen never flickers while refreshing — state merges in place, and a small indicator appears only while you have triggered a command. That indicator sits next to the status dot, in a slot reserved whether or not it is visible: it used to sit where the hourglass now is, and appearing and vanishing mid-command shifted the unit's name sideways.
On Breeze Core ≥ 3.0.0 the app receives live updates over SSE, so the server pushes changes — including ones made by a schedule or another client — and the phone stops polling. Against an older server, or if the stream drops, it falls back to a 5-second poll and retries. An offline banner appears with backed-off polling when the server is unreachable.
The app reopens on the unit you last viewed, remembered by unit rather than by position, so it survives adding, removing or reordering units.
Feedback is named by meaning rather than intensity, so it stays consistent as things get added:
| when | |
|---|---|
| tick | a notch moved — temperature ±, a slider detent, swiping to another unit |
| select | a setting chosen — mode, fan, a flap half, eco/turbo |
| toggle | power on/off, deliberately the heaviest: the one action with a physical result you might be across the room from |
| success / failure | pairing accepted, or a rejected key — failure is two beats, so it is distinguishable without looking |
The temperature slider ticks only when the value crosses a notch, not on every pixel of the drag.
The hourglass beside the power switch: tap it, pick how long the unit should stay on, and the server switches it off. It keeps its promise with the phone asleep, flat, or in another country — the app only places the order.
Presets are 15, 30, 45, 60, 90 and 120 minutes, plus a custom box (1 minute to 24 hours). While one is pending the hourglass fills in and shows the time left ("42m"), and tapping it again offers to change or cancel.
Two details that are deliberate rather than incidental:
-
It asks the server for minutes, never for a time of day, and counts down
from the
seconds_remainingthe server reports. Your phone's clock is used only to measure elapsed time, never to decide what time it is. A phone in a different timezone from its server, or with a clock that is simply wrong, still gets the right result — and this app has been bitten by trusting the phone's clock before (see Security). - Setting a timer replaces that unit's existing one. Asking for "off in 30" when one is already running means you changed your mind, not that you want two competing promises about the same unit.
The hourglass only appears when the server supports it (Breeze Core 3.2.0 or
newer, advertising the sleep_timer feature), rather than appearing everywhere
and failing. Server-side
behaviour — what happens if the unit is unreachable, or the server was rebooting
when the timer came due — is in
Timers.
Breeze 2.3.0, with Breeze Core 4.2.0 or newer.
The same sheet has a second half, Turn on later: pick a day — today, tomorrow, or up to 30 days ahead — and a time, and the server switches the unit on then, in the mode and at the temperature it last had. "Off tonight, on before I'm home" is one of each.
-
The day and the time are the server's. The app asks for "in 3 days at
07:30" and the server works out the moment, for the same reason the sleep timer
asks for minutes. For most people that is their own clock. A server in a
container without
TZset runs on UTC, and then 07:30 means 07:30 UTC. - A unit has one of each. A new start replaces the unit's start and leaves its sleep timer alone, and the other way round.
- While one waits, an alarm clock sits beside the hourglass, with the day or time when there is room for it: "18:05" today, "Tue" this week, "29 Sep" beyond. The unit's name comes first, so with both timers pending the start shows as the alarm clock alone, beside the countdown. The sheet says it in full — "Turning on Tue 29 Sep at 07:30" — opens on that day and time, so changing one keeps the other, and offers to cancel it.
- A time already gone today is refused, and the app shows the server's reason.
- If the server was down at the moment and comes back more than 15 minutes late, the start is dropped rather than switching the unit on hours late.
It only appears when the server advertises the timer_at feature; with an older
server the sheet is exactly what it was. Server side:
Timers.
Whenever the app is waiting on the server — at launch, or connecting to a unit — it shows a cat fact. They are bundled, not fetched: an app whose whole point is that nothing leaves your network would look absurd calling a cat-fact API, and it would fail on a LAN with no route out anyway.
Your units report an outdoor temperature and the app used to throw it away. The bar could be described as three separate behaviours, which is how such things drift apart, so it is implemented as one rule:
The warmest of indoor / outdoor / target sets the top of the scale. The other two are drawn from the floor as overlapping fills — the warmer of them lighter, the cooler darker on top.
- Cooling (outdoor 33, indoor 27, target 24) — outdoor is the ceiling, indoor the lighter fill, target the darkest.
- Heating (outdoor 5, indoor 20, target 23) — the target becomes the ceiling and the outdoor reading the darkest fill.
- Indoor below target — the shades swap by themselves, because warmer is always lighter. No special case.
A temperature sensor has many ways to lie, and each of these is covered by a test:
- A unit without an outdoor probe may report a sentinel rather than null, so anything outside −50…80 °C is treated as missing. A bar scaled to 255 °C renders as an empty sliver with no clue why.
- NaN and infinities are dropped the same way.
- Below-zero outdoors extend the scale floor beneath 0 °C rather than clamping the reading to nothing, which would look like a dead sensor every winter.
- Fewer than two real readings is not a relationship: the numbers still show, the bar does not.
- Two readings draw a single full-strength fill; the pale shade only makes sense with something darker on it.
- Identical readings still get a 1 °C span, so nothing divides by zero.
- Genuine extremes are kept — 45 °C on a sun-baked outdoor unit and −30 °C in a real winter are weather, not faults.
Geometry is computed in Celsius whatever the display unit, so °F users see identical proportions.