Loading Now

A Local LLM Suggests, I Tap, the Thermostat Obeys

Flat editorial illustration: a round thermostat dial beside a small robot holding a speech-bubble suggestion, a fingertip about to tap a button.

I don’t let an LLM control my thermostat. I let it nag me, one button at a time, and the arrangement works better than either full manual or full auto ever did.


the contract

Every hour during waking hours, a local Ollama model gets a small packet of facts: indoor temperature, outdoor temperature, who’s home, solar generation, time of day. Its entire allowed vocabulary is two shapes:

HOLD :: <reason>
SUGGEST :: <setpoint> :: <reason>

HOLD means silence. No notification, no log entry I’ll ever read, nothing. SUGGEST becomes a phone notification with the reason and a button: Set 78°. Tap it and a handler applies the setpoint. Ignore it and nothing happens, and the next hour starts fresh.

That’s the whole system. The design position: AI in the suggestion loop, human in the action loop. The model gets to be smart; only I get to move the equipment.

why not let it act

Climate control is the tempting first target for “let the AI run it,” and the failure mode is miserable: you notice the house is wrong hours later, then reverse-engineer a decision no one wrote down. An unattended wrong setpoint costs comfort, money, and with an elderly relative or a nursery, more than that.

The suggestion loop keeps the useful part, a tireless watcher that notices “it cooled off outside, you could open up and coast.” I skip the hard part on purpose: teaching a model my family’s comfort by trial and error, with the family as the test subjects.

Two knobs enforce it. The button is the only path from model output to climate.set_temperature, and the handler clamps to a sanity range (60–85°F) even then. A parse failure or a setpoint outside the range means the suggestion drops, silence. The model can’t cook anyone. It can only be ignored, which it often is, and that’s the system working.

why local

The packet leaves nothing to the cloud: an hourly heartbeat of who’s home and what the house is doing stays on my hardware. A small local model handles this fine, because the task is small: read eight numbers, apply common sense, emit one line in a fixed grammar. No reasoning marathon, no context window, no subscription. The GPU that runs my other local jobs absorbs it without noticing.

The strict output grammar is doing quiet heavy lifting here. Small models ramble when you let them. Two allowed shapes and a parser that drops anything else turns “small model rambles sometimes” from a bug into a non-event.

the config

Sanitized. Thermostat and sensor names are stand-ins.

- id: climate_suggestion_hourly
  alias: "Climate — hourly LLM suggestion"
  trigger:
    - platform: time_pattern
      hours: "/1"
  condition:
    - condition: time
      after: "06:00:00"
      before: "22:00:00"
  action:
    - service: ai_task.generate_data
      data:
        task_name: climate_suggest
        instructions: >
          You suggest thermostat setpoints. Reply with EXACTLY one line:
          either  HOLD :: <short reason>
          or      SUGGEST :: <setpoint °F, integer> :: <short reason>
          Suggest only when a change would clearly improve comfort or cost.
          Facts: inside {{ states('sensor.inside_temp') }}°F,
          outside {{ states('sensor.outside_temp') }}°F,
          thermostat set to {{ state_attr('climate.main','temperature') }}°F,
          mode {{ states('climate.main') }},
          occupancy: {{ states('zone.home') }} people home,
          solar: {{ states('sensor.solar_power') }} W,
          time: {{ now().strftime('%H:%M') }}.
      response_variable: verdict
    - variables:
        parts: "{{ (verdict.data | default('')).split('::') | map('trim') | list }}"
    - condition: template
      value_template: >
        {{ parts | count == 3 and parts[0] == 'SUGGEST'
           and parts[1] | int(0) >= 60 and parts[1] | int(0) <= 85 }}
    - service: notify.my_phone
      data:
        title: "🌡️ Suggestion: {{ parts[1] }}°"
        message: "{{ parts[2] }}"
        data:
          actions:
            - action: "SET_TEMP_{{ parts[1] }}"
              title: "Set {{ parts[1] }}°"

- id: climate_suggestion_apply
  alias: "Climate — apply suggested setpoint"
  trigger:
    - platform: event
      event_type: mobile_app_notification_action
  condition:
    - condition: template
      value_template: "{{ trigger.event.data.action.startswith('SET_TEMP_') }}"
  action:
    - variables:
        setpoint: "{{ trigger.event.data.action.split('_')[-1] | int }}"
    - condition: template
      value_template: "{{ setpoint >= 60 and setpoint <= 85 }}"
    - service: climate.set_temperature
      target:
        entity_id: climate.main
      data:
        temperature: "{{ setpoint }}"
    - service: notify.my_phone
      data:
        message: "Thermostat set to {{ setpoint }}°. 🌡️"
  mode: single

The range check appears twice, once before the notification and again in the handler. The handler must not trust its input even though I wrote the thing that sends it. Notification actions are strings, and strings that fire service calls on your HVAC deserve validation on both ends.

a month of living with it

Most hours: HOLD, silence. Two or three times a day it pipes up, and about a third of those earn a tap, evening coast-downs mostly, plus the occasional “solar is peaking, pre-cool now and coast through the expensive hours,” which I would not have thought to do at 2pm on a Tuesday.

The taps I skip teach me the model’s blind spots. It can’t know a kid is home sick or that the oven is about to run for two hours. In the suggestion loop those gaps cost one swipe-to-dismiss. In an action loop they’d cost an evening of a wrong house and a family asking pointed questions about my hobby.


Cheapest AI-in-the-home lesson I know: give the model a strict grammar, a narrow lane, and no hands. Rig it so the worst possible outcome is a notification you ignore, then let it surprise you inside those walls.

My name is Skylar Pearce, I have been working as a System Administror since 2013 as well some side consulting work. During my career I have worked with everything from Active Directory and vCenter to configuring routers and switches and phone systems, documenting and scripting my way through the whole thing. I have a Security+ certification and am currently working on my PenTest+. Throughout my career I have gained almost all of my knowledge from blogs like this. It is now time for me to pay it back. Over time I have gathered scripts and tricks over the years that I will share on this site. A lot of the posts here will be mainly reference posts, some will be full on how to’s. I am happy to go into more depth on any other topics I go over here, just make a comment on a post. I will do my best to post once a day on weekdays but as I run out of ideas it may slow down. My WordPress skills are still growing so the site will likely get better over time as I learn. You can reach me at contact@allthesystems.com or on LinkedIn

Post Comment