About making inexpensive models smarter by providing tools and context. (local models, gpt-5-mini, gpt-4.1-mini, gpt-4o-mini ...)

So here we go: Short term history access.
To be able to request the recorder values of all entities (as long as you didn’t exclude them manually).
Default persistence duration until this values get purged is 10 days.

Some examples:

  • When was the bathroom window opened the last time
  • How long was it opened
  • Has there been motion in the front yard today?
  • When did <name> leave the house?
  • How long did I watch TV today?

This one was a little bit more difficult, as there is no action in HA to retrieve this data.
So, you have to use the HA API with external tools like Python or Node-Red to retrieve the history data and get it back into HA.

As I use Node-RED for all my automations anyway, the setup looks like this:

  • The “tool” (HA script for the LLM) calls a generic script I posted here to send data to a Node-RED flow and wait for the returned data
  • This generic script sends an event to Node-RED and waits for a response with a different event.
  • Node-RED processes the parameters it received, calls the history API and sends the answer with the event type that the script is waiting for.
  • The generic script hands the answer back to the LLM script

This allows questions about a lot history states that aren’t in the aggregated statistics we accessed in the other history script.

The tool provides a list of state changes for the given time range.
It also provides the total count of state changes, the count and duration for each state that the entity had in the timespan as well as the first and last state value.

Here are the steps to add it to your installation:

  • First add my generic “Call a Node-RED flow and wait for the response” script from here
    Only the base script, not the samples (Node-RED flow and sample caller script).
    Do not expose this helper script to assist.

  • Import this Node-Red flow, which does the real work by collecting the history data and calculating the durations and counts for the single states:

    [{"id":"81cd8bd1866eb2a7","type":"group","z":"bfa4729855e61f82","style":{"label":false,"stroke":"none","fill":"#d1d1d1","fill-opacity":"0.5"},"nodes":["17a1219784662b39","d1864a3f26d6e6e0","72dafcbec8fb0f5f","2848b06d0cd60a3c","d6158a71fb770c01","eef83e5afa4e8a59","258eb4dd654d0e0a","20232be9215a10be","0d3f6bf42798bc6f","90b0d467703f69f5","044047795e2600eb"],"x":14,"y":19,"w":1052,"h":262},{"id":"17a1219784662b39","type":"api-get-history","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","name":"Read history","server":"ef6aa0b.3fe4a6","version":1,"startDate":"","endDate":"","entityId":"","entityIdType":"regex","useRelativeTime":false,"relativeTime":"","flatten":false,"outputType":"array","outputLocationType":"msg","outputLocation":"payload","x":730,"y":60,"wires":[["eef83e5afa4e8a59"]]},{"id":"d1864a3f26d6e6e0","type":"function","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","name":"Format Incoming Data","func":"const event = msg.payload.event;\nmsg.channel= event.channel;\nmsg.request_id= event.request_id;\n\nconst startDate = msg.start_time = event.payload.start_time;\nconst endDate = msg.end_time = event.payload.end_time || new Date().toLocaleString('sv-SE').replace('T', ' ');\n\nconst entities = event.payload.entity_ids.split(',').map(item => item.trim());\nconst escapedSpecialChars = entities.map(str => str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'));\nconst regexString = `^(?:${escapedSpecialChars.join('|')})$`;\n\nmsg.limit = event.payload.limit;\n\nmsg.payload = {\n    entityId: regexString,\n    startDate,\n    endDate\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":60,"wires":[["17a1219784662b39"]]},{"id":"72dafcbec8fb0f5f","type":"server-events","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","name":"","server":"ef6aa0b.3fe4a6","version":3,"exposeAsEntityConfig":"","eventType":"nodered_request.trigger","eventData":"","waitForRunning":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"$outputData(\"eventData\").event_type","valueType":"jsonata"}],"x":170,"y":120,"wires":[["2848b06d0cd60a3c"]]},{"id":"2848b06d0cd60a3c","type":"switch","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","name":"Are we the receiver for the event?","property":"payload.event.channel","propertyType":"msg","rules":[{"t":"eq","v":"entity-history-raw-state-log","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":280,"y":180,"wires":[["0d3f6bf42798bc6f"]]},{"id":"d6158a71fb770c01","type":"ha-fire-event","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","name":"","server":"ef6aa0b.3fe4a6","version":0,"event":"nodered_request.response","data":"","dataType":"jsonata","x":900,"y":180,"wires":[[]]},{"id":"eef83e5afa4e8a59","type":"function","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","name":"Create output Mapping","func":"// Used below for converting from 2025-07-30T22:00:00+00:00 \n// to an local time converted string in the format 2025-07-30 22:00:00\nfunction formatLocalTime(isoString) {\n    const d = new Date(isoString);\n    const y = d.getFullYear();\n    const m = String(d.getMonth() + 1).padStart(2, '0');\n    const day = String(d.getDate()).padStart(2, '0');\n    const h = String(d.getHours()).padStart(2, '0');\n    const min = String(d.getMinutes()).padStart(2, '0');\n    const s = String(d.getSeconds()).padStart(2, '0');\n    return `${y}-${m}-${day} ${h}:${min}:${s}`;\n}\n\n// Parse local time strings like 2025-07-30 22:00:00 as LOCAL time to epoch ms\nfunction parseLocalDateTime(str) {\n    const [datePart, timePart] = str.split(' ');\n    const [Y, M, D] = datePart.split('-').map(Number);\n    const [h, m, s] = timePart.split(':').map(Number);\n    return new Date(Y, M - 1, D, h, m, s || 0).getTime();\n}\n\nconst windowStart = parseLocalDateTime(msg.start_time);\nconst windowEnd = parseLocalDateTime(msg.end_time);\nconst result = {};\n\nfor (const entityLog of msg.payload) {\n    const entityId = entityLog[0].entity_id;\n    result[entityId] = {\n        friendly_name: entityLog[0].attributes.friendly_name || \"\",\n        newest_log_entries_truncated_by_limit: [],\n        complete_log_count_within_timespan: entityLog.length,\n        state_values_within_timespan: {\n            state_at_start: entityLog[0].state,\n            state_at_end: entityLog[entityLog.length - 1].state,\n            states: {}\n        }\n    };\n\n    for (const logEntry of entityLog.slice().reverse()) {\n        const state = logEntry.state;\n\n        if (result[entityId].newest_log_entries_truncated_by_limit.length < msg.limit) {\n            result[entityId].newest_log_entries_truncated_by_limit.push({ [formatLocalTime(logEntry.last_changed)]: logEntry.state });\n        }\n\n        if (!result[entityId].state_values_within_timespan.states[state]) {\n            result[entityId].state_values_within_timespan.states[state] = { count: 1 };\n        } else {\n            result[entityId].state_values_within_timespan.states[state].count += 1;\n        }\n    }\n\n    const statesMap = result[entityId].state_values_within_timespan.states;\n\n    if (windowEnd > windowStart) {\n        let prevTimestamp = windowStart;\n        let prevState = entityLog[0].state;\n\n        for (let i = 0; i < entityLog.length; i++) {\n            const stateChangeTimestamp = new Date(entityLog[i].last_changed).getTime();\n            const segmentEnd = Math.min(stateChangeTimestamp, windowEnd);\n\n            if (segmentEnd > prevTimestamp) {\n                if (!statesMap[prevState]) statesMap[prevState] = { count: 0, duration_sum_in_seconds: 0 };\n                if (typeof statesMap[prevState].duration_sum_in_seconds !== 'number') statesMap[prevState].duration_sum_in_seconds = 0;\n                statesMap[prevState].duration_sum_in_seconds += Math.floor((segmentEnd - prevTimestamp) / 1000); // seconds\n            }\n\n            if (stateChangeTimestamp >= windowEnd) {\n                prevTimestamp = windowEnd;\n                break;\n            }\n\n            prevTimestamp = Math.max(stateChangeTimestamp, windowStart);\n            prevState = entityLog[i].state;\n        }\n\n        if (windowEnd > prevTimestamp) {\n            if (!statesMap[prevState]) statesMap[prevState] = { count: 0, duration_sum_in_seconds: 0 };\n            if (typeof statesMap[prevState].duration_sum_in_seconds !== 'number') statesMap[prevState].duration_sum_in_seconds = 0;\n            statesMap[prevState].duration_sum_in_seconds += Math.floor((windowEnd - prevTimestamp) / 1000);\n        }\n    }\n}\n\nmsg.payload = result;\ndelete msg.start_time;\ndelete msg.end_time;\ndelete msg.limit;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":630,"y":120,"wires":[["20232be9215a10be"]]},{"id":"258eb4dd654d0e0a","type":"comment","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","name":"Entity History - Raw State Log","info":"","x":160,"y":60,"wires":[]},{"id":"20232be9215a10be","type":"function","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","name":"Format Response","func":"msg = { \n    payload : {\n        data: {\n            channel: msg.channel,\n            request_id: msg.request_id,\n            result: msg.payload,\n            hint: \"Remember you are bad at calculating or determining of min / max values. Use the calculator if you can't directly use the provided values!\"\n        } \n    }\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":850,"y":120,"wires":[["044047795e2600eb"]]},{"id":"0d3f6bf42798bc6f","type":"function","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","name":"Do the Provided Entity IDs exist?","func":"const event = msg.payload.event;\nconst entities = event.payload.entity_ids.split(',').map(item => item.trim());\n\nconst states = global.get('homeassistant').homeAssistant.states;\n\nconst missing = entities.filter(id => !(id in states));\n\nif (missing.length > 0) {\n    // Du brauchst keine Details – optional kannst du missing dranhängen\n    msg.error = 'Mindestens eine entity_id existiert nicht.';\n    // msg.missing = missing; // falls du doch debuggen willst\n    return [null, msg];\n}\n\nreturn [msg, null];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":240,"wires":[["d1864a3f26d6e6e0"],["90b0d467703f69f5"]],"outputLabels":["Real","Fake"]},{"id":"90b0d467703f69f5","type":"function","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","name":"Format Error","func":"const event = msg.payload.event;\nmsg.channel = event.channel;\nmsg.request_id = event.request_id;\n\nmsg = {\n    payload: {\n        data: {\n            channel: msg.channel,\n            request_id: msg.request_id,\n            result: null,\n            error: {\n                code: 'entity_not_found',\n                message: 'You provided some not existing entity ids. Use only names returned from \"Entitiy Index\" or that you looked up in the data provided to you.'\n            }\n        }\n    }\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":610,"y":240,"wires":[["044047795e2600eb"]]},{"id":"044047795e2600eb","type":"junction","z":"bfa4729855e61f82","g":"81cd8bd1866eb2a7","x":720,"y":180,"wires":[["d6158a71fb770c01"]]},{"id":"ef6aa0b.3fe4a6","type":"server","name":"Home Assistant","addon":true}]
    

  • Add the final script that connect the parts. This one has to be exposed to assist:
alias: Entity History - RAW State Log
icon: mdi:chart-timeline
description: >
  Access historical state values.

  The return value for a timespan is an object with:  - first state  - last
  state - count of state entries - count per state value like "on" or "off" -
  summed up duration for each state value like "on" or "off" where the entity
  was in this state - Raw state values as time -> state-value mapping log. Use
  limit parameter for lot of state changes or long periods


  About the tool an the correct way to use it:


  Supply exact and existing entity_ids. Find them with the tool "Entity Index".

  Required:
    - entity_ids: String with an entity_id name, or a comma seperated list like "sensor.temp_1,
  sensor.temp_2"
    - start_time: Time string in local time like '2025-07-30 14:00:00'
    - limit: limit the output of raw log entries to avoid too large database queries and context size

  Optional:
    - end_time: Time string in local time like '2025-07-30 14:00:00'. If not provided, current time is used.

  Output on success:
   - result: 
     - <entity_name_1>: 
       - friendly_name: string
       - newest_log_entries_truncated_by_limit: array of { date-string: new-state-string }
       - complete_log_count_within_timespan: number
       - state_values_within_timespan:
         - state_at_start: state value
         - state_at_end: state value
         - states:
           - state_value_1:
             - count: number
             - duration_sum_in_seconds: number
           - ...
      - ...
  - error: null

  Output on error:
    - result: null
    - error: 
      - code: string
      - message: string


  Attention: 

  Always provide dates in local time like '2025-07-30 14:00:00'.  Returned dates
  and times are in same format and local time.

  Hint:
    - To only search for the state at a specific moment, set start and end date to same value.

  Examples. Use this tool to get information like:
    - When was bath window opened last time
    - How long is front door already open
    - Last time motion was detected in garden
    - How often was front door opened today
    - How long (summed up) was TV turned on today
mode: parallel
fields:
  entity_ids:
    name: Entity IDs (comma separated)
    description: >-
      comma-separated string of a single or multiple entity_id names (e.g.
      "sensor.roomname_temperature". MANDATORY!
    required: true
    selector:
      text: {}
  start_time:
    name: Start time
    description: >-
      Set the start date / time of your search. Always provide dates in
      localtime like '2025-07-30 14:00:00'
    required: true
    selector:
      text: {}
  end_time:
    name: End time
    description: >-
      Set the end date / time of your search. If not provided, the current time
      is used. Always provide dates in localtime like '2025-07-30 14:00:00'
    required: false
    selector:
      text: {}
  limit:
    name: Limit
    description: >-
      Set the maximum RAW state values you want to receive. If you only need the
      last state change, set limit to 2. Otherwise, set the limit as needed (but
      not lower than 2).
    required: true
    selector:
      number:
        min: 0
        max: 50
        mode: slider
sequence:
  - action: logbook.log
    data:
      name: "ENTITY HISTORY - RAW STATE LOG:"
      message: >-
        ids={{ entity_ids }}, start={{ start_time }}, end={{ end_time }},
        limit={{ limit }}
      entity_id: "{{ this.entity_id }}"
  - action: script.node_red_request
    data:
      channel: entity-history-raw-state-log
      payload:
        entity_ids: "{{\_entity_ids }}"
        start_time: "{{ start_time }}"
        end_time: "{{ end_time }}"
        limit: "{{ limit }}"
      timeout: 5
    response_variable: response
  - variables:
      result: "{{ response | default(None) }}"
  - stop: ""
    response_variable: result

If you use this script, you might want to add also some information to the prompt of your LLM.

Here’s my combined prompt for both history tools (which means you can delete the prompt part from the aggregated history statistics tool and replace it with this one):

There are 2 tools for checking history values of the entities:

  • “Entity History - Aggregated Statistics”
    This tool can retreive aggregated values but no RAW sate values.
    The aggregation has the benefit that the stored data is smaller and can be kept around for years.
    Also searching for results is way faster.
    Another benefit is, that you can request mean, min, max values e.g. of a temperature sensor without the need to calculate them yourself.
    The option “change” for the aggregation type parameter allows you to get the increment within a timespan of a monotonic increasing counter.
    Prefer this tool whenever you need to find a min or max value of a numeric entity like power (W) or temperature within a timespan, or the change/increment of monotonic increasing counters like energy (kWh) sensors.
  • “Entity History - RAW State Log”
    This tool has access to the RAW state values of entities from the past and can present them as a list of timestamp → state mappings.
    But it has only a limited time of persistance as the RAW data need a lot storage and search queries take more time.
    The Benefit is that you have access to the history of a lot more entities and not just numeric ones.
    Like “on” or “off” states of lights or opened windows.
    The exact timestamps of state changes returned from this tool also allow you to check how long an entity is already in a given state.
    It also returns the count of state changes per state and the summed up duration that the entity was in this state in the requested timespan.

Hints for both history tools:

  • You really need the EXACT and REAL entity_id of the devices to get the historical values.
  • The tools allow you to request values for multiple entities as once, so you need less tool calls.
  • Energy (kWh) sensors are cumulative, monotonic increasing counter.
    NEVER reply with their current state when asked about the energy used/produced in a specific timespan.
    Use the “Entitiy History - Aggregated Statistics” tool with the aggregation type “change” to get the deviation between start-time and end-time
1 Like