HEART Embedded Systems logoHEART

Smart thermostat simulator: a HEART-embedded system you can run, watch and ask to change things#

A demonstration only. It simulates a house, a smart thermostat, a gas boiler and the thermostat's HEART. It switches no real heating and must never be connected to a boiler or heat pump.

One Python file, thermostatSimulator.py, with only the standard library. Run it, register it on HEART Hub with the code it prints, and watch its health on your own private page while it lives a simulated life: day and night, cold and mild spells, sunshine, windows opened now and then, people coming and going, a boiler that cycles on and off. Faults come and go: strong sun overheating the room, a lost Wi-Fi link, a frozen room sensor, an impossible reading, a boiler that will not light, a boiler that short-cycles, and flat batteries in the wall unit.

Unlike the pacemaker simulator, this HEART also takes changes. The owner can change the setpoint, the night temperature and the mode through the local HEART API. Heating acts on the physical world, so every change waits for a person to press the thermostat's own button. A clearly labelled mock AI suggests ways to save energy, and its suggestions wait for the same yes. They never run on their own.

The case study behind it, step by step for a real product, is guides/Thermostat.md (Guide 5).

On a real device: Thermostat, for real shows the hardware we assume, where the HEART runs, the code to change, how to build and install it, the factory steps and what the owner sees. For illustration only.

FileWhat it is
thermostatSimulator.pyThe simulator: house, thermostat and boiler firmware, HEART with its gate, mock AI, local HEART API, HEART Hub reporting and approvals
test_e2e.pyEnd-to-end test against a monitor site on your own computer
README.mdThis file

1. Download and run#

You need Python 3.8 or newer. Nothing to install.

python3 thermostatSimulator.py --scenario all

It starts at once and prints three things:

  HEART Hub: register this simulated thermostat
    1. Sign in at https://heartcortex.ai/login with your email (a 6-digit code is sent to you).
    2. Open https://heartcortex.ai/register?id=5b1e0c93a7d2f418
    3. Enter the claim code  Q4M7T-ZX2KD-9RBVA-H3NPW
    4. Watch it at https://heartcortex.ai/device/5b1e0c93a7d2f418  (private to you until you share it)

10:50:09 HEART API on http://127.0.0.1:43278/heart/v1/pulse  (owner key for private views: ...)
10:50:24 room  20.3 C · target 20.0 (heat) · CALL · boiler ON  flow 61.8 C starts 1 · outdoor   5.4 C · battery 2.98 V · wifi up · HEART healthy

Every 15 seconds it prints one line of what the simulated thermostat knows. When something goes wrong, its HEART says so:

10:52:21 HEART critical system.main-function-lost: The boiler does not heat when the thermostat calls for heat.
         Called for heat for over 2 minutes, but the burner has not fired (the boiler reports a lockout). The house
         will get cold. Check the boiler's display and gas supply, or call the installer. No fix HEART can make: it
         never resets a boiler.
10:53:42 HEART resolved   system.main-function-lost: the boiler does not heat when the thermostat calls for heat

And when a change is waiting for a yes, the thermostat's "display" shows it with a short code:

10:51:10 THERMOSTAT DISPLAY  Night temperature: 20 to 18 °C at night?  code 01ED72  (press the button within 900 s: touch button.press)

Stop it with Ctrl-C. The device ID, claim code, key and the monitor site's signing key are kept in thermostat-monitor.json (readable only by you), so the same simulated device comes back next time. Delete that file to make a new device.

2. Register it and watch it on HEART Hub#

  1. Sign in. Open https://heartcortex.ai/login and enter your email address. A 6-digit code arrives; type it in. There are no passwords.
  2. Register. Open the register?id=... link the simulator printed, and enter the claim code. The claim code is the proof that you hold the device: it is shown only on the simulator's console, and the site holds only its hash.
  3. Watch. Your device's page, https://heartcortex.ai/device/<ID>, shows its status (healthy, degraded or failing) and its open and recent alerts, each with the plain-language meaning of its fault code. All your devices are at https://heartcortex.ai/devices.
  4. Share, if you want. The page is private until you make a share link or make it public.
  5. Silence. Stop the simulator: after three missed Pulse intervals (90 seconds) the page shows a Flatline, and you get one email. The site checks for silent devices every 5 minutes, so the email can take a few minutes more. (The staged Wi-Fi loss lasts 200 simulated seconds: at --speed 1 that is long enough for a Flatline, at --speed 5 it is not.)

Until you register it, the site stores nothing. After registering, it can take up to five minutes for the first report to arrive, or less as soon as something changes.

What the site sees. Only the public Pulse and public alerts, and it keeps only the status and timing and each alert's fault code, severity and times. Never a temperature, the boiler's state, the schedule, or whether anyone is home. The one exception is opt-in: see approvals on the site, below.

Approvals on the site (optional, off by default)#

With --monitor-approvals, the HEART also offers a waiting change to its owner on the monitor site, as the Python template's ask_owner() does (HEART Hub, Section 7.1). The site sees only a title and a change in plain words, such as "Night temperature: 21 to 18 °C at night", never the hours of the night schedule or anything about who is home. The owner turns approvals on for the device on its page, presses Approve…, and types the code the site emails them. The device acts only after it has checked the site's Ed25519 signature on the decision with the key it pinned at its first hello, that the digest is the waiting Action's (API Standard 6.3), and that the decision is at most 300 seconds old. It records the approval as presence method remote-site.

Every change on a thermostat is physical, and remote-site proves only that the owner could read their inbox, not that anyone stood in the house. So physical changes are offered on the site only with a second flag, --monitor-approvals-physical (the owner's remoteSitePhysical setting, Part 6 Section 3.2). Without it, the site never sees a thermostat change, and the button is the only way to say yes. Resuming after Stop is never offered on the site.

Note for heartcortex.ai. Approvals need the site to have its signing key. The live heartcortex.ai must be redeployed with SITE_SIGNING_KEY set before approvals work there; until then the owner's page says approvals are not available, and everything else works. The local test site (heart-monitor/test/serve.mjs) has a test key, and test_e2e.py uses it.

3. The staged faults#

With --scenario, each fault starts at a set time, lasts a while, and is then "fixed by the installer", so you can watch an alert open, escalate and resolve. Times are in simulated seconds: at --speed 1 they are real seconds.

ScenarioStartsLastsWhat you see
sun30 s2 minperformance.overshoot (warning): strong sun drives the room more than 1.5 °C above its target. HEART cannot cool; it says so
link1 min200 sconnectivity.link-lost (warning): the hub loses Wi-Fi. Heating goes on as set; no reports until it is back
batteryat onceto the endpower.battery-low (warning) within two minutes: the wall unit's AA cells, aged 1500 times faster
stuck280 s3 minsensors.stuck (error): the room reading freezes, so the thermostat controls to a stale value
range470 s80 ssensors.out-of-range (error): the sensor reads -40 °C. The firmware ignores it and keeps the last good reading
boiler560 s400 ssystem.main-function-lost (critical): called for heat, the boiler does not light. The demo opens a window for the first two minutes, so heat is soon needed
cycling980 s400 sactuators.short-cycling (warning): with air in the radiators the flame cuts out after 20 s; the firmware's anti-short-cycle timer restarts it 90 s later, 4 starts in 10 minutes
ai5 minThe mock AI makes its first suggestion early, and asks again 100 s after a suggestion lapses (otherwise after 30 minutes, then 6 hours)
allAll of the above. At --speed 5 it takes about 4.5 minutes

Without --scenario, the house still has random weather, sun, open windows and Wi-Fi losses (every 3 to 9 hours), as in life. A long run with no scenario raises only those.

4. Options#

OptionDefaultMeaning
--scenariononesun, link, battery, stuck, range, boiler, cycling, ai (comma-separated) or all
--speed1Simulated seconds per real second. --speed 5 runs the staged faults in about 4.5 minutes
--approval-timeout900Real seconds a change waits for a yes before it expires (approvalTimeoutSec, at least 60; the standard's default is 24 hours)
--monitor-approvalsoffOffer waiting changes to the owner on the monitor site (signed decisions, remote-site)
--monitor-approvals-physicaloffAlso offer physical changes there (remoteSitePhysical). Needs --monitor-approvals
--monitor-urlhttps://heartcortex.aiAny HEART Hub site, including your own. http only for 127.0.0.1
--no-monitorReport to no site: local only
--state-filethermostat-monitor.jsonWhere the device ID, claim code, key and pinned site key are kept
--port43278The local HEART API (falls back to 7685; 0 for none)
--seedrandomRepeat the same simulated life
--durationuntil Ctrl-CStop after this many real seconds

5. The local HEART API#

The simulator serves the HEART API on your own computer, as a real thermostat's hub would:

curl http://127.0.0.1:43278/.well-known/heart-card.json         # the Card: what it is
curl http://127.0.0.1:43278/heart/v1/pulse                       # public health
curl http://127.0.0.1:43278/heart/v1/arrhythmias                 # public alerts: codes only
curl http://127.0.0.1:43278/heart/v1/properties                  # public values: battery status only
curl http://127.0.0.1:43278/heart/v1/tending/autonomy            # max 3, mode suggest; max 2, mode observe after Stop
KEY=...   # the owner key it printed
A="Authorization: Bearer $KEY"
curl -H "$A" http://127.0.0.1:43278/heart/v1/properties          # all values, internal and private
curl -H "$A" http://127.0.0.1:43278/heart/v1/arrhythmias         # alerts with explanations, cause and confidence
curl -H "$A" http://127.0.0.1:43278/heart/v1/actions             # every change asked for, and what became of it
curl -H "$A" http://127.0.0.1:43278/heart/v1/rhythm              # private history: actions, occupancy, windows

Changing things. Every change needs the owner key, and every one acts on the heating, so it is physical:

P=http://127.0.0.1:43278/heart/v1/properties/org.heartaispec.demo.thermostat
curl -X PUT -H "$A" -d '{"value": 21}' $P.setpoint             # 202: an Action waits; the display shows a code
curl -X POST -H "$A" -d '{"decision":"approve"}' http://127.0.0.1:43278/heart/v1/actions/<id>/decision
                                                                 # 403 presence-required: a key is not a person
touch button.press                                               # a press on the thermostat: it runs
curl -X PUT -H "$A" -d '{"value": "eco"}' $P.mode              # heat, eco (setpoint - 3 °C) or off (frost protection only)
curl -X PUT -H "$A" -d '{"value": 18}' $P.night_setpoint       # 23:00 to 06:00; null to turn it off
curl -X POST -H "$A" -d '{"decision":"deny"}' http://127.0.0.1:43278/heart/v1/actions/<id>/decision   # no proof needed
curl -X POST -H "$A" http://127.0.0.1:43278/heart/v1/tending/stop   # Stop: at once; or touch button.hold
curl -X PUT -H "$A" -d '{"max":3}' http://127.0.0.1:43278/heart/v1/tending/autonomy   # resume: waits for the button
RequestAnswer
A setpoint or night temperature outside 10 to 28 °C, or a mode other than heat, eco, off422 out-of-range. Frost protection (5 °C) stays on in every mode, whatever is set
frost_protection, max_setpoint, or anything the firmware or boiler owns409 conflict: they are read-only, and HEART never overrides them
The same setting again within 5 simulated minutes, or any change while the room sensor's reading is not good409 guard-failed. The guards are the product's, checked when asked and again just before the change is applied, whoever asked
A valid change202 with an Action, awaiting-approval, until the button is pressed (it runs), it is denied or cancelled, Stop is pressed (cancelled), a newer request replaces it, or --approval-timeout passes (expired, nothing changes)
approve with the key, or any presence field in a request403 presence-required or 403 presence-invalid. Only the button (or a signed site decision, if allowed) proves a person
PUT /heart/v1/tending/autonomy with a max other than 0 to 2 or 3400 invalid-request: this HEART promises level 3 (mode suggest) and never acts on its own

Autonomy. A person sets the ceiling max; the current level is the lower of max and the promised level 3, and the mode follows from it: 3 is suggest, 2 or below is observe. Stop sets max to 2, and so does PUT /heart/v1/tending/autonomy with a max of 2 or below, at once. Raising it back to 3 is destructive and waits for the button. The owner's Pulse shows autonomy with max, current, and limitedBy: stopped while max is below 3.

Only one change waits at a time, so one press can never approve the wrong thing. The mock AI (MockAI in the file, plain rules over the private occupancy log, no model and no network) waits until nothing else is waiting, then offers its suggestion as an ai Action with onBehalfOf: owner. Because it is physical and started by an AI, the gate treats it as destructive: it always waits for a person, whatever is configured, and after Stop it does not suggest at all.

Every message it serves is a valid HEART message: test_e2e.py checks each one against the official schemas. Two simplifications: the simulator honours an Idempotency-Key header but does not insist on one, as API Standard 5 requires of a real system; and the API is plain HTTP on loopback only.

6. How the code is built#

House ──► Thermostat (firmware: control loop, limits) ──► Boiler (its own firmware)
             ▲  │ read                                       │ read
   apply()   │  ▼                                             ▼
   (clamps)  └── Heart ◄─── MockAI (suggests only)
                  │  watches, gates changes, tells a person
                  ├─► local HEART API    (127.0.0.1; owner key; button for physical changes)
                  └─► HEART Hub      (outbound only; approvals only with the flags)
PartIn the fileIn a real product
HouseOutdoor temperature (day and night, spells), the room's thermal mass and radiators, sun, windows, occupancy, each on its own random timerThe real home
ThermostatWall unit sensor, batteries and Wi-Fi hub; the control loop (hysteresis around the target); frost protection, the 28 °C maximum; guard() and apply()The thermostat's own firmware. Never part of HEART
BoilerFires when called, within its anti-short-cycle timers; can fail to light, or cut its flame for low flowThe boiler and its certified controls. Never part of HEART
HeartRULES turn readings into alerts with fault codes; escalate() tells a person; the gate turns change requests into Actions that wait for presence; Stop; the RhythmThe code you add, on the thermostat's hub
MockAISuggests a lower night temperature. Labelled a mock: a few rules, no modelA model behind the Reasoning Gateway, or your own rules; its output still goes through the gate as ai
monitor()Hello, reports, nextSec, 404 means hello again, 401 stops; offers waiting changes and checks signed decisions only with the flagsThe same
handler()The HEART API: Card, Pulse, alerts, properties, actions, autonomy, Stop, RhythmThe same, behind TLS on the home network

HEART is not in the control loop (Part 6, Section 9): the firmware decides each second whether to call for heat. HEART changes only the targets the loop works to, only through Thermostat.apply(), which keeps the firmware's limits, and only after a person's yes.

7. What code to add to make a real product HEART-embedded#

For a real thermostat, HEART runs on the hub: the part with Wi-Fi, beside the boiler-control firmware, never inside it. Pick a starting point, then add four things.

Starting points

Your thermostatStart from
A microcontroller with Wi-Fi (most thermostats)The ESP32 Full starter heart-templates/templates/esp32-full: its signal table, owner key, physical writes that wait for the device's button, Stop by a long press, and Monitor reporting with signed approvals. Its host/port_linux.c uses the same button.press file as this simulator
An embedded Linux hubThe Rust Core heartd (heart-core-rs/crates/heartd) for HEART Full, with guards and interlocks in its configuration; heart-core-rs/examples/ac-controller is a close relative (see guides/AC-Temperature-Controller.md). Or this file's Heart, monitor() and handler()
A cloud service or app beside the thermostatThe Python template heart-templates/templates/python (ask_owner() for site approvals) or heart-templates/templates/javascript-node

The four things you add

  1. Read the thermostat. Replace House, Thermostat and Boiler with reads of values your firmware already has: room reading, target, call for heat, the boiler's OpenTherm status (firing, flow temperature, fault flags), battery voltage, Wi-Fi state.

``python class Readings: """CHANGE: fill these from your firmware. Read only.""" def refresh(self): f = your_firmware.status() # your code self.raw, self.reading, self.call = f.room_raw, f.room_filtered, f.call_for_heat self.firing, self.flow, self.lockout = f.ot_flame_on, f.ot_flow_temp, f.ot_fault self.months, self.wifi_up = f.battery_days / 30.44, f.wifi_connected ``

  1. Your rules, thresholds and guards. Edit RULES and EXPLAIN with your product's limits, keeping the standard fault codes (Part 3). Keep the guards (guard()) and limits (apply()) in the firmware, where HEART cannot remove them: HEART asks, the firmware decides.
  2. Your presence and escalation routes. Wire the "button" to a real press on the wall unit (and show the 6-character code on its display), and Stop to a long press. Change escalate() to notify the owner's app, and to show the problem on the thermostat itself, with no internet needed.
  3. Your privacy table. Go through PROPS and give every value its level (Step 3 of the case study). Occupancy, windows and the schedule are private; only status and fault codes are public.

Then keep monitor() as it is, store the device ID, claim code, key and pinned site key in the hub's secure storage, and print the claim code and a QR code of https://<your monitor site>/register?id=<ID> on a label in the box. Serve the API over TLS, run the HEART-Rating client (heart-rating/client/heart_rating.py) and Part 9's tests, and claim what passed. The simulator's Card promises Autonomy Level A1: it demonstrates the A3 gate ("suggests and waits for a yes"), but Part 2 Section 8 says a claim of A3 needs HEART Full, which this one-file simulator does not implement in full. Case study, Step 10.

8. Test it#

From the project's top folder, with Node 20+ and Python 3 with pip install --user "jsonschema[format]>=4.18" referencing:

python3 thermostatSimulator/test_e2e.py

It starts a HEART Hub site on this computer (port 8802), runs the simulator with every staged fault at 5x speed (API on port 43302) with approvals on the site allowed, signs in, registers the device through the site's own form, and checks for five minutes that:

  • every message is valid against the official schemas, and public views hold only public data;
  • out-of-range setpoints are refused 422, the firmware's limits 403, and a second change too soon 409 guard-failed;
  • a setpoint change waits: approving with the key is refused 403 presence-required, it stays waiting with the setpoint unchanged, and runs after touch button.press;
  • a waiting change can be denied without the button;
  • the mock AI's suggestion is refused with the key, then expires, and nothing changes; its next one runs only after the owner approves it on the site, with an emailed code, and the device checks the signature (remote-site);
  • Stop cancels a waiting change at once, and resuming needs the button;
  • every staged fault reaches the owner's page with its fault code, no temperature, occupancy or schedule reaches the site, and the page stays private.

Licence: Apache-2.0. Part of HEART, the open standard at heartcortex.ai.