Jev for Home Assistant
September 20, 2026 ยท View on GitHub
Jev for Home Assistant
Ask TypeSafe Jev questions about your house and get numbers back. Jev is a decision model rather than a chat model, so it answers a typed question with a probability, a choice or a score, and this integration turns each answer into an entity you can automate on.
Not affiliated with TypeSafe. The API client is jevclient.

What it does
- Questions become sensors: a probability, one of your options with its
distribution, or a number that can land between levels. Add them in the UI,
or in
configuration.yaml, or both. - Four actions answer inside an automation and return a response variable:
jev.noul,jev.choice,jev.scoreandjev.ask. - Point a question at entities, devices, areas, floors or labels in the normal picker and the state is built for you, so no template is needed.
- A conversation agent for Assist, so spoken commands are routed by the same model and counted against the same budget.
- Reports what it spends: calls, input tokens and estimated cost per day, plus a daily token budget that halts evaluation when it trips.
- Fifteen worked examples, four of them pairing Jev with an LLM.
automation:
- alias: Remind about the washing
triggers:
- trigger: state
entity_id: binary_sensor.jev_laundry_forgotten
to: "on"
for: "00:10:00"
actions:
- action: notify.mobile_app
data:
message: The washing is done and still in the machine.
Installation
Requires Home Assistant 2026.9 or newer and an API key from typesafe.ai.
HACS
Not in the HACS default list yet, so add it as a custom repository once. hacs/default#11052 is queued; when it merges, steps 1 and 2 go away.
- HACS, then the three dot menu, then Custom repositories.
- Paste
https://github.com/AboveColin/HA-Jev, set Type to Integration, Add. - Search HACS for Jev, then Download.
- Restart Home Assistant.
Manual
Copy custom_components/jev from the
latest release into your
config/custom_components/ directory and restart. HACS will not update a copy
installed this way.
Configuration
Settings, Devices and services, Add integration, then Jev (TypeSafe). It asks for an API key and the address to send it to, and both are checked before the entry is created. The address already holds the TypeSafe API, so a key is all most people fill in.
| Option | Where | Default | Description |
|---|---|---|---|
| API key | config flow | none | Your TypeSafe key |
| API address | config flow | https://api.typesafe.ai | Where the requests go. Leave it alone unless you run something of your own that speaks the same API |
| Daily input token budget | options | 0 | Stops evaluating for the day once spent. 0 means no limit |
| Price per million input tokens | options | 0.042 | Only affects the estimated cost sensor |
| Fall back to this agent | options | none | Where unrouted sentences go. Empty means the agent says it did not understand |
| Act only above this confidence | options | 0.6 | Below it, the sentence goes to the fallback instead |
| Allow whole-house commands | options | off | Commands naming no room or device. Turning everything off is always allowed |
Use Reconfigure to replace the key or the address later, which keeps your entities and history.
Pointing it somewhere else
The address is checked before the entry is created, the same way the key is, so a
wrong one fails there rather than later. Anything that answers POST /v1/systemone
the way TypeSafe does will do: a proxy that holds the key, caches answers or meters
what is spent across more than just Home Assistant. A path is kept as a prefix, so
http://gateway.local:8093/jev becomes http://gateway.local:8093/jev/v1/systemone.
The key is sent as a bearer header. Over http that puts it on the wire in clear,
where anything on the same network can read it, so the log says so once per setup
unless the address is loopback. Leaving the field empty goes back to TypeSafe.
Actions
- action: jev.noul
response_variable: laundry
target:
entity_id: sensor.washing_machine_power
data:
instructions: Is the laundry finished but still sitting in the machine?
background: >-
This machine draws under 5 W when idle and over 300 W while a programme runs.
threshold: 0.7
- if: "{{ laundry.is_true }}"
then:
- action: notify.mobile_app
data: { message: The washing is done and still in the machine. }
The same thing in the automation editor, and what a run of it looks like:
![]() | ![]() |
The right-hand one is example 15. Six questions go in one request and five are thrown away, the action targets entities instead of building a template, and nothing acts until the confidence clears a bar. Its trace on a real instance, API call included:

| Action | You give it | You get back |
|---|---|---|
jev.noul | a yes/no question | noul 0 to 1, is_true against your threshold |
jev.choice | options:, 2 to 255 | choice, probabilities, confidence |
jev.score | levels:, 2 to 10, lowest first | score, normalized, nearest_level, legend, probabilities, confidence |
jev.ask | any mix, under your own keys | the same, under answers |
All four take a template in state, or an object, or a list. They also take
background: for standing facts about how to read the state, which is
worth more attached to the question than to the state.
They send whatever you target, whether or not it is exposed to Assist. An
automation names its entities on purpose, so the Assist exposure list is not
consulted here. It is consulted for the conversation agent below. Watch that with
include_attributes: true on a device_tracker, which puts coordinates in the
request.
Questions
Settings, Devices and services, Jev, then Add question. Pick what kind of answer you want back:

Name it, ask one thing, and say what it should look at. The schedule and the rest sit in a collapsed Advanced section, so a first question is four fields.

Before it saves, it shows you what it will send and what that answers right now:

That last screen is worth the click. A question that reads a perfect state and still answers 0.5 is the common disappointment, and this is where you find that out rather than after the sensor exists. The trial costs one request, which the footer reports: 350 input tokens and $0.000015 in the shot above.
It also names which other questions it will share a request with. Questions that look at the same thing on the same schedule are sent as one call, because the API takes one state per request. You never declare that grouping, and this is where you see it.
Two worked examples
A washing machine that has finished but not been emptied. The readings are 1.4 W,
the door shut, and 14 minutes since the programme ended, and the background field
says what those numbers mean:

A cable modem where one reading sits near its limit. Downstream SNR of 31.2 dB is under the healthy 33, upstream power of 50.4 dBmV is near the 51 ceiling, and there are 1,184 uncorrected errors:

Worth reading that second one closely. It answers degraded at 0.59 with marginal
right behind at 0.40, and a confidence of 0.46. That is the model saying the data is
genuinely ambiguous rather than pretending otherwise, and it is why the actions
return confidence at all: an automation can require 0.8 before it wakes anyone.
Both are sample data on a throwaway instance, not a real house.
Questions in YAML
The same thing in a file, which keeps working and is not deprecated:
jev:
- name: Laundry
scan_interval: 300
entities:
- sensor.washing_machine_power
- binary_sensor.laundry_door
questions:
- name: Laundry forgotten
type: noul
instructions: Is the laundry finished but still sitting in the machine?
background: >-
This machine draws under 5 W when idle and over 300 W while a programme runs.
threshold: 0.7
- name: Nudge urgency
type: score
instructions: How urgently should someone be reminded?
criteria: [Not at all, When convenient, Right now]
| Key | Required | Description |
|---|---|---|
name | yes | Names the context and prefixes its entities |
entities | one of these two | Entities, devices, areas, floors or labels to read |
state | one of these two | Text or a template, alone or as a note beside the entities |
scan_interval | no | Seconds between evaluations, minimum 30, default 300 |
trigger_entities | no | Wake on these instead of on whatever entities names |
include_attributes | no | Send every attribute of the picked entities, off by default |
questions | yes | Each with name, type, instructions, and criteria for choice and score |
A context is one request, so keep related questions together. It is evaluated on
scan_interval, or when an entity it watches changes, debounced by 5 seconds. Adding
threshold: to a noul also creates a binary sensor to trigger on.
Voice
The integration adds a conversation agent. Settings, Voice assistants, pick your pipeline, set Conversation agent to Jev.

It sends one request per sentence, describing only the entities you exposed to Assist, and runs Home Assistant's own intents with what comes back. It turns things on and off, toggles them, sets a light's brightness and answers what something is set to. Anything else, anything phrased as two commands, and anything it is not confident about goes to the fallback agent whole, with nothing done first.
Locks are not among them and are never described to the model. Home Assistant reads turn_on on a lock as lock and turn_off as unlock, which is the opposite way round from how the command is spoken, and a probability with no reasoning should not be deciding whether a door opens. Lock sentences go to the fallback agent.
Against the built-in sentence matcher, it understands a command phrased a way nobody wrote a template for, and it returns a confidence the router can refuse to act on. Against an LLM agent, it is cheaper and it stops on its own: a command works out at about $0.0001 with 20 entities exposed and $0.0007 at the 150 entity cap, derived from the measured token cost per entity, and every one counts against the same daily budget as the sensors. A satellite that mishears a wake word all night trips that budget instead of running up a bill.
Brightness comes out of a regex, not out of a question, because Jev judges and does
not calculate. 40 percent, 40% and 40 procent all work.
Commands naming no room and no device are refused unless you allow them, except turning everything off, whose worst case is a dark house.
Examples
| 01 laundry reminder | one question, one threshold, one binary sensor |
| 02 alert triage | three questions in one call, three notification paths |
| 03 doorbell triage | a choice on an intercom transcript |
| 04 situation layer | named situations other automations trigger on |
| 05 confidence gating | act, ask, or stay quiet |
| 06 composite score | several scores combined with your own weights |
| 07 Jev gates the LLM | a cheap typed decision in front of an expensive call |
| 08 cascade | low confidence escalates to a reasoning model |
| 09 guardrail | the LLM writes, Jev checks it against the source |
| 10 extract then verify | the LLM pulls fields, Jev verifies each one |
| 11 post and parcels | one attention queue across several channels |
| 12 energy window | where to keep arithmetic and where to ask |
| 13 voice commands | a command router, 12 questions per request |
| 14 conversation agent | watching what the agent spends, and routing text Assist never saw |
The LLM examples use ai_task.generate_data, so they work with Google Generative AI,
OpenAI, Anthropic or a local Ollama. The voice command router follows TypeSafe's own
smart home demo and builds its device
options from your entity registry, so the answer is an entity_id you can act on.
Measurements
docs/measurements.md has what was measured against the live API: what an entity costs in tokens, why batching is nearly free, real latency from Europe against the published figure, and the two findings that changed this code.
Known limitations
- Answers carry no reasoning, so there is nothing to audit afterwards.
- Confidence has no published calibration evidence. Treat 0.9 as higher than 0.6 until you have measured it on your own questions.
- Slower from Europe than the published 70 to 500 ms. Fine for a doorbell, too slow for a tight loop.
- Not for safety decisions. A probability with no explanation should not hold a lock, a heater or a smoke alarm.
- The conversation agent handles on, off, toggle, brightness and state questions. Locks, climate setpoints, anything needing words written and anything phrased as two commands go to the fallback agent.
- Diagnostics include the last 20 sentences the agent routed. Read the file before pasting it into a public issue.
Troubleshooting
Turn on debug logging first. It prints every state sent, which is usually the answer:
logger:
logs:
custom_components.jev: debug
| Symptom | Cause |
|---|---|
| An answer barely moves with the world | The state does not say what you assumed, or it holds a number the model is being asked to compare |
| Answers sit near 0.5 with low confidence | The question measures more than one thing. Split it |
| Entities unavailable, budget sensor on | The daily budget stopped evaluation |
| Entities unavailable, budget sensor off | Look for one line saying TypeSafe is not answering |
| Setup fails with "TypeSafe did not answer" | Connectivity, not configuration. Home Assistant retries |
| Voice commands all go to the fallback | Check the traces in diagnostics. Each one records the reason |
| Voice acts on the wrong device | The names and areas in the entity registry are what the model reads |
| An error names a limit | It names your number too. 2 to 255 options, 2 to 10 levels, 250 entities |
Contributing
Issues and pull requests welcome.
Python 3.14. The pinned pytest-homeassistant-custom-component requires it, and CI
runs the same version, so 3.13 fails at install with "No matching distribution
found".
pip install -r requirements-test.txt
pytest
204 tests run the integration inside a real Home Assistant with the API client
replaced, so the suite spends nothing. quality_scale.yaml tracks this against Home
Assistant's quality scale, and mypy --strict runs in CI.
Changelog
See the release history.

