Area Occupancy Probability Calculation Explained¶
This document details the process used by the area_occupancy
custom component to calculate the probability of an area being occupied, including how historical data influences priors and how probability decays over time.
Core Concepts¶
- Occupancy Probability: The final output value (0.0 to 1.0, typically represented 0-100%) indicating the calculated likelihood that the area is currently occupied. This is the value exposed by the main probability sensor.
- Undecayed Probability: The probability calculated based only on the current state of sensors and the learned priors, before any decay is applied.
- Prior Probability (Learned): This isn't a single value but refers to the
PriorState
object stored by the component. It contains: - Entity Priors: For each configured sensor, stores:
P(Active | Occupied)
: The learned probability that this sensor is active given the primary sensor indicates occupancy.P(Active | Not Occupied)
: The learned probability that this sensor is active given the primary sensor indicates non-occupancy.Prior
: The simple historical probability of this specific sensor being active over the analysis period.
- Type Priors: Aggregated averages of the
Entity Priors
grouped by sensor type (motion, media, light, etc.). - Overall Prior: A baseline probability calculated from the
Type Priors
and their configuredWeights
. This represents a starting point before considering current sensor states. - Analysis Period: The number of days (
history_period
) used for the historical analysis. - Weights: User-configured values (0.0 to 1.0) assigned to each type of sensor (motion, media, light, etc.), determining their relative influence on the probability calculation.
- Active States: User-configured states for specific sensor types that are considered to indicate activity (e.g.,
playing
for media players,on
for motion sensors,closed
for certain door configurations). - Decay: An exponential decrease in the occupancy probability over time when no sensors are reporting an "active" state. This simulates the diminishing certainty of occupancy as time passes without new evidence.
- Threshold: A user-configured value (0-100%). If the final Occupancy Probability meets or exceeds this threshold, the main binary sensor state becomes
on
(Occupied).
Data Flow and Components¶
- Coordinator (
coordinator.py
): The central orchestrator. - Holds the current state (
self.data
:ProbabilityState
) and learned priors (self.prior_state
:PriorState
). - Listens for state changes of configured entities.
- Triggers probability calculations.
- Manages the decay process and timer.
- Schedules and triggers periodic prior updates.
- Interacts with storage.
- Notifies HA of updates.
- Probability Calculator (
calculate_prob.py
): Calculates the current, undecayed probability based on current sensor states and learned priors (PriorState
). - Prior Calculator (
calculate_prior.py
): Calculates the learned priors (PriorState
) based on historical data analysis. - Decay Handler (
decay_handler.py
): Applies the exponential decay logic to the calculated probability. - Probabilities (
probabilities.py
): Holds configuration data (weights, active states) and provides methods to access learned/default prior values from thePriorState
. - Storage (
storage.py
): Handles saving and loading the learnedPriorState
object to/from the.storage/area_occupancy.storage
file. - Service (
service.py
): Provides thearea_occupancy.run_analysis
service to manually trigger prior recalculation. - Home Assistant Core: Provides entity states, history data (
recorder
), event listeners, and timers.
Process Walkthrough¶
1. Initialization / Startup¶
- Coordinator Starts: The
AreaOccupancyCoordinator
is initialized for a configured area. - Load Config: Merges configuration from
data
andoptions
of theConfigEntry
. - Load Stored Priors: Attempts to load the
PriorState
object for this area from.storage/area_occupancy.storage
usingStorageManager
.- If successful,
self.prior_state
is populated with learned values andself._last_prior_update
timestamp is loaded. - If unsuccessful (no file, error),
self.prior_state
is initialized with default values fromconst.py
viaProbabilities
.
- If successful,
- Initialize States: Gets the current state of all configured sensors from
hass.states.get
and populatesself.data.current_states
. - Setup Listeners: Starts listening for state changes (
async_track_state_change_event
) for all configured sensors. - Initial Prior Check: Determines if priors need to be calculated immediately (based on whether they were loaded, are complete, and are recent enough compared to
self.prior_update_interval
). If needed, callsupdate_learned_priors
. - Schedule Prior Updates: Schedules the next periodic prior update using
_schedule_next_prior_update
(async_track_point_in_time
for the start of the next hour). - Initial Refresh: Performs an initial calculation (
async_refresh
->_async_update_data
) to establish the starting probability.
2. State Change Event¶
This is the most common trigger for recalculation.
sequenceDiagram
participant Sensor
participant HA Event Bus
participant Coordinator Listener
participant Coordinator State Lock
participant Coordinator (`_async_update_data`)
participant ProbabilityCalculator
participant DecayHandler
participant HA UI/Sensors
Sensor->>HA Event Bus: State changes (e.g., motion 'on')
HA Event Bus->>Coordinator Listener: Event received (`async_state_changed_listener`)
Coordinator Listener->>Coordinator State Lock: Acquire Lock
Coordinator Listener->>Coordinator State Lock: Update `self.data.current_states`
Coordinator State Lock-->>Coordinator Listener: Release Lock
Coordinator Listener->>Coordinator (`_async_update_data`): Trigger calculation task
Note over Coordinator (`_async_update_data`): Capture `current_states` snapshot
Coordinator (`_async_update_data`)->>ProbabilityCalculator: Calculate Undecayed Probability (using snapshot, `prior_state`)
ProbabilityCalculator-->>Coordinator (`_async_update_data`): Return Undecayed Probability
Coordinator (`_async_update_data`)->>DecayHandler: Apply Decay (using undecayed prob, previous prob, decay status)
DecayHandler-->>Coordinator (`_async_update_data`): Return Decayed Probability & Decay Status
Coordinator (`_async_update_data`)-->>Coordinator (`_async_update_data`): Update `self.data` (final prob, is_occupied, decay status etc.)
Coordinator (`_async_update_data`)-->>Coordinator (`_async_update_data`): Check Reset Condition (no active sensors AND decay stopped)
Coordinator (`_async_update_data`)-->>Coordinator (`_async_update_data`): Start/Stop Decay Timer if needed
Coordinator (`_async_update_data`)->>HA UI/Sensors: Notify Listeners (`async_set_updated_data`)
- Event Trigger: A configured sensor changes state.
- Listener Callback: The
async_state_changed_listener
incoordinator.py
receives the event. - State Update: It acquires an
asyncio.Lock
(self._state_lock
), updates the corresponding entry inself.data.current_states
with the new state and availability, and releases the lock. - Trigger Calculation: It schedules
_async_update_data
to run. - Snapshot & Previous State:
_async_update_data
takes a snapshot ofself.data.current_states
(to ensure the calculation uses a consistent view) and copies the oldcurrent_states
intoself.data.previous_states
. It also notes the probability before this calculation cycle (initial_prob
). - Calculate Undecayed Probability: Calls
ProbabilityCalculator.calculate_occupancy_probability
, passing the state snapshot and theself.prior_state
.- The calculator iterates through the sensors in the snapshot.
- For each sensor, it checks if its state is "active" using
self.probabilities.is_entity_active
. - If active, it retrieves the learned conditional probabilities (
P(T)
,P(F)
) for that entity fromself.prior_state.entity_priors
and the configured type weight fromself.probabilities
. - It combines these using a Bayesian-like approach (the exact formula isn't shown but likely involves weighting the evidence from active sensors against the overall prior).
- It returns the calculated undecayed probability.
- Apply Decay: Calls
DecayHandler.calculate_decay
, passing the newly calculated undecayed probability, theinitial_prob
(from before this cycle), and the current decay status (decaying
,decay_start_time
, etc.).- If decay is enabled and the probability hasn't increased, it calculates how much time has passed since decay started (or since the last update if decay is ongoing).
- It applies an exponential decay factor based on
DECAY_LAMBDA
and the configureddecay_window
. - It returns the potentially lower decayed probability and updated decay status flags/timestamps.
- Update Final State: Updates
self.data
with the final decayed probability, the correspondingis_occupied
state (based on the threshold), and the new decay status. - Reset Check: Checks if there are any active sensors in the snapshot and if the decay process is currently stopped (
decaying=False
). If both conditions are true (no activity, decay finished), it resets the probability toMIN_PROBABILITY
andis_occupied
toFalse
. - Manage Decay Timer: If
self.data.decaying
is true, it ensures the 5-second decay timer (async_track_time_interval
calling_async_do_decay_update
) is running. If false, it stops the timer. The timer callback simply triggers anotherasync_request_refresh
->_async_update_data
cycle to apply further decay. - Notify Listeners: Calls
self.async_set_updated_data(self.data)
to push the updated state to all sensors/UI elements associated with this coordinator.
3. Prior Calculation (Periodic / Manual)¶
This process runs independently to learn from history.
sequenceDiagram
participant Scheduler/Service
participant Coordinator (`update_learned_priors`)
participant PriorCalculator
participant Recorder (`history.get_significant_states`)
participant Coordinator Storage (`_async_save_prior_state_data`)
participant Storage Helper
alt Periodic Timer
Scheduler/Service->>Coordinator (`update_learned_priors`): Timer fires (`_handle_prior_update`)
else Manual Service Call
Scheduler/Service->>Coordinator (`update_learned_priors`): Service call received (`service.py`)
end
Coordinator (`update_learned_priors`)-->>Coordinator (`update_learned_priors`): Determine `history_period` (Service > Config > Default)
Coordinator (`update_learned_priors`)-->>Coordinator (`update_learned_priors`): Calculate `start_time`, `end_time`
Coordinator (`update_learned_priors`)->>PriorCalculator: Call `calculate_prior` for each sensor (with time range)
loop For Each Sensor
PriorCalculator->>Recorder: Fetch history (Primary Sensor, `start_time`, `end_time`) - Needs Full State
Recorder-->>PriorCalculator: Return Primary States
PriorCalculator->>Recorder: Fetch history (Current Sensor, `start_time`, `end_time`) - Needs Full State
Recorder-->>PriorCalculator: Return Current Sensor States
PriorCalculator->>Recorder: Fetch state before window (Current Sensor, `start_time - 1s`, `start_time`) - Needs Minimal State
Recorder-->>PriorCalculator: Return Prior State(s)
PriorCalculator-->>PriorCalculator: Convert states to Time Intervals (`_states_to_intervals`)
PriorCalculator-->>PriorCalculator: Calculate P(Entity Active | Primary ON) (`_calculate_conditional_probability_with_intervals`)
PriorCalculator-->>PriorCalculator: Calculate P(Entity Active | Primary OFF) (`_calculate_conditional_probability_with_intervals`)
PriorCalculator-->>PriorCalculator: Calculate Sensor Prior (Active Time / Total Time)
end
PriorCalculator-->>Coordinator (`update_learned_priors`): Return P(T), P(F), Prior for the sensor
Coordinator (`update_learned_priors`)-->>Coordinator (`update_learned_priors`): Update `self.prior_state.entity_priors`
Coordinator (`update_learned_priors`)-->>Coordinator (`update_learned_priors`): Calculate & Update `self.prior_state.type_priors` (averaging entity priors)
Coordinator (`update_learned_priors`)-->>Coordinator (`update_learned_priors`): Calculate & Update `self.prior_state.overall_prior`
Coordinator (`update_learned_priors`)->>Coordinator Storage (`_async_save_prior_state_data`): Save updated `prior_state`
Coordinator Storage (`_async_save_prior_state_data`)->>Storage Helper: Save data to `.storage/area_occupancy.storage`
Storage Helper-->>Coordinator Storage (`_async_save_prior_state_data`): Confirm Save
Coordinator Storage (`_async_save_prior_state_data`)-->>Coordinator (`update_learned_priors`): Confirm Save
Coordinator (`update_learned_priors`)-->>Coordinator (`update_learned_priors`): Update `_last_prior_update` timestamp
Coordinator (`update_learned_priors`)-->>Scheduler/Service: Reschedule next periodic update
- Trigger: Either the hourly timer fires (
_handle_prior_update
) or thearea_occupancy.run_analysis
service is called. - Determine Period: The
update_learned_priors
method determines thehistory_period
(days) to use, prioritizing the service call parameter, then the configured value, then the default. It calculates thestart_time
andend_time
for the history query. - Iterate Sensors: Loops through all configured sensors.
- Fetch History: For each sensor, calls
PriorCalculator.calculate_prior
. This method calls_get_states_from_recorder
(which usesrecorder.history.get_significant_states
) twice:- Once for the primary occupancy sensor over the full period. Requires full state objects (
minimal_response=False
) to getlast_changed
. - Once for the current sensor being analyzed over the full period. Requires full state objects.
- Once for the primary occupancy sensor over the full period. Requires full state objects (
- Fetch Prior State:
PriorCalculator._states_to_intervals
makes another call toget_significant_states
for a tiny (1-second) window before the mainstart_time
. This usesminimal_response=True
andsignificant_changes_only=False
to efficiently get the state value just before the analysis window begins. - Calculate Intervals:
_states_to_intervals
converts the list ofState
objects (from step 4 & 5) into a list ofStateInterval
dictionaries, each representing a period during which the sensor held a specific state. - Calculate Probabilities (Non-Primary):
- Calculates the simple prior for the current sensor (total active time / total time).
- Calls
_calculate_conditional_probability_with_intervals
twice: - Calculates P(Entity Active | Primary=ON) by finding the overlap duration between the entity's active intervals and the primary sensor's ON intervals, divided by the total duration the primary sensor was ON.
- Calculates P(Entity Active | Primary=OFF) similarly, using the primary sensor's OFF intervals.
- Calculate Probabilities (Primary): If the sensor is the primary, it calculates its simple prior (active time / total time) and assigns fixed high/low values for P(T) (0.9) and P(F) (0.1).
- Update
prior_state
: The results (P(T), P(F), Prior) for the successfully calculated sensor are stored inself.prior_state.entity_priors
. - Update Type Priors: After looping through all sensors,
_update_type_priors_from_entities
averages the newly learnedentity_priors
for each sensor type and updatesself.prior_state.type_priors
. - Update Overall Prior: Calculates the new
overall_prior
based on the updatedtype_priors
and configured weights, updatingself.prior_state.overall_prior
. - Save Priors: Calls
_async_save_prior_state_data
which usesStorageManager
to save the entire updatedself.prior_state
object to the JSON storage file (.storage/area_occupancy.storage
). - Update Timestamp: Sets
self._last_prior_update
to the current time. - Reschedule: Schedules the next hourly update.
Summary¶
The area_occupancy
component uses a two-pronged approach:
- Real-time Calculation: Responds instantly to sensor state changes, calculating an undecayed probability based on current states and learned priors, applies exponential decay if no activity is present, determines the final occupancy state based on a threshold, and updates HA sensors.
- Periodic Learning: Runs in the background (typically hourly or on demand) to analyze historical sensor data, comparing each sensor's activity against the primary sensor's state to learn conditional probabilities (Priors). These learned priors refine the accuracy of the real-time calculations.
This combination allows the component to react quickly while continuously improving its understanding of how different sensors correlate with actual occupancy in that specific area over time.