@@ -339,9 +339,10 @@ def processSensorData(self, jdata, when):
339339 # instead of appending, but frequent value changes can still cause unbounded growth.
340340 cur_value_float = float (cur_value )
341341 for obj in objects_in_sensor :
342- if sensor_id in obj .chain_data .env_sensor_state :
343- # Update exposure incrementally
344- state = obj .chain_data .env_sensor_state [sensor_id ]
342+ with obj .chain_data ._lock :
343+ if sensor_id in obj .chain_data .env_sensor_state :
344+ # Update exposure incrementally
345+ state = obj .chain_data .env_sensor_state [sensor_id ]
345346 last_time = state ['exposure' ]['last_time' ]
346347 last_value = state ['exposure' ]['last_value' ]
347348
@@ -352,38 +353,38 @@ def processSensorData(self, jdata, when):
352353 avg_value = (last_value + cur_value_float ) / 2.0
353354 state ['exposure' ]['total' ] += avg_value * dt
354355
355- state ['exposure' ]['last_time' ] = timestamp_epoch
356- state ['exposure' ]['last_value' ] = cur_value_float
357-
358- # Update readings array: append if value changed, update timestamp if same
359- if 'readings' not in state :
360- state ['readings' ] = []
361- if state ['readings' ] and state ['readings' ][- 1 ][1 ] == cur_value_float :
362- # Value unchanged - update timestamp
363- state ['readings' ][- 1 ] = (timestamp_str , cur_value_float )
356+ state ['exposure' ]['last_time' ] = timestamp_epoch
357+ state ['exposure' ]['last_value' ] = cur_value_float
358+
359+ # Update readings array: append if value changed, update timestamp if same
360+ if 'readings' not in state :
361+ state ['readings' ] = []
362+ if state ['readings' ] and state ['readings' ][- 1 ][1 ] == cur_value_float :
363+ # Value unchanged - update timestamp
364+ state ['readings' ][- 1 ] = (timestamp_str , cur_value_float )
365+ else :
366+ # Value changed - append new reading
367+ state ['readings' ].append ((timestamp_str , cur_value_float ))
364368 else :
365- # Value changed - append new reading
366- state ['readings' ].append ((timestamp_str , cur_value_float ))
367- else :
368- # First reading - initialize from entry time (or first_seen for scene-wide sensors)
369- entry_str = obj .chain_data .regions .get (sensor_id , {}).get ('entered' )
370- if not entry_str :
371- # No entry time (scene-wide sensor or pre-existing object) - use first_seen
372- entry_str = get_iso_time (obj .first_seen )
373-
374- entry_epoch = get_epoch_time (entry_str )
375- dt = timestamp_epoch - entry_epoch
376- # Only calculate initial exposure if time moved forward
377- initial_exposure = cur_value_float * dt if dt > 0 else 0.0
378-
379- obj .chain_data .env_sensor_state [sensor_id ] = {
380- 'readings' : [(timestamp_str , cur_value_float )],
381- 'exposure' : {
382- 'total' : initial_exposure ,
383- 'last_time' : timestamp_epoch ,
384- 'last_value' : cur_value_float
369+ # First reading - initialize from entry time (or first_seen for scene-wide sensors)
370+ entry_str = obj .chain_data .regions .get (sensor_id , {}).get ('entered' )
371+ if not entry_str :
372+ # No entry time (scene-wide sensor or pre-existing object) - use first_seen
373+ entry_str = get_iso_time (obj .first_seen )
374+
375+ entry_epoch = get_epoch_time (entry_str )
376+ dt = timestamp_epoch - entry_epoch
377+ # Only calculate initial exposure if time moved forward
378+ initial_exposure = cur_value_float * dt if dt > 0 else 0.0
379+
380+ obj .chain_data .env_sensor_state [sensor_id ] = {
381+ 'readings' : [(timestamp_str , cur_value_float )],
382+ 'exposure' : {
383+ 'total' : initial_exposure ,
384+ 'last_time' : timestamp_epoch ,
385+ 'last_value' : cur_value_float
386+ }
385387 }
386- }
387388
388389 elif sensor .singleton_type == "attribute" :
389390 # Event history tracking - append discrete events (or update timestamp if value unchanged)
@@ -392,14 +393,15 @@ def processSensorData(self, jdata, when):
392393 # Convert to string for consistent type comparison (attributes can be non-numeric)
393394 cur_value_str = str (cur_value )
394395 for obj in objects_in_sensor :
395- if sensor_id not in obj .chain_data .attr_sensor_events :
396- obj .chain_data .attr_sensor_events [sensor_id ] = []
397-
398- events = obj .chain_data .attr_sensor_events [sensor_id ]
399- if events and events [- 1 ][1 ] == cur_value_str :
400- # Value unchanged - update timestamp of last event instead of appending
401- events [- 1 ] = (timestamp_str , cur_value_str )
402- else :
396+ with obj .chain_data ._lock :
397+ if sensor_id not in obj .chain_data .attr_sensor_events :
398+ obj .chain_data .attr_sensor_events [sensor_id ] = []
399+
400+ events = obj .chain_data .attr_sensor_events [sensor_id ]
401+ if events and events [- 1 ][1 ] == cur_value_str :
402+ # Value unchanged - update timestamp of last event instead of appending
403+ events [- 1 ] = (timestamp_str , cur_value_str )
404+ else :
403405 # Value changed - append new event
404406 events .append ((timestamp_str , cur_value_str ))
405407
@@ -617,37 +619,39 @@ def _updateRegionEvents(self, detectionType, regions, now, now_str, curObjects):
617619 if region .singleton_type == "environmental" :
618620 # Use 'now' directly (already epoch time) to avoid precision loss from string conversion
619621
620- if hasattr (region , 'value' ) and hasattr (region , 'lastWhen' ):
621- # Sensor has cached value - initialize with it
622- ts_str = get_iso_time (region .lastWhen )
623- last_value = float (region .value ) if region .value is not None else None
624-
625- # Start exposure accumulation from entry time, not sensor reading time
626- # Exposure = value * (now - entry_time)
627- obj .chain_data .env_sensor_state [key ] = {
628- 'readings' : [(ts_str , region .value )],
629- 'exposure' : {
630- 'total' : 0.0 ,
631- 'last_time' : now ,
632- 'last_value' : last_value
622+ with obj .chain_data ._lock :
623+ if hasattr (region , 'value' ) and hasattr (region , 'lastWhen' ):
624+ # Sensor has cached value - initialize with it
625+ ts_str = get_iso_time (region .lastWhen )
626+ last_value = float (region .value ) if region .value is not None else None
627+
628+ # Start exposure accumulation from entry time, not sensor reading time
629+ # Exposure = value * (now - entry_time)
630+ obj .chain_data .env_sensor_state [key ] = {
631+ 'readings' : [(ts_str , region .value )],
632+ 'exposure' : {
633+ 'total' : 0.0 ,
634+ 'last_time' : now ,
635+ 'last_value' : last_value
636+ }
633637 }
634- }
635- else :
636- # No cached value yet
637- obj . chain_data . env_sensor_state [ key ] = {
638- 'readings ' : [],
639- 'exposure ' : {
640- 'total ' : 0.0 ,
641- 'last_time ' : now ,
642- 'last_value' : None
638+ else :
639+ # No cached value yet
640+ obj . chain_data . env_sensor_state [ key ] = {
641+ 'readings' : [],
642+ 'exposure ' : {
643+ 'total ' : 0.0 ,
644+ 'last_time ' : now ,
645+ 'last_value ' : None
646+ }
643647 }
644- }
645648
646649 elif region .singleton_type == "attribute" :
647650 # Attribute sensors only tag objects present when MQTT arrives
648651 # Do NOT initialize with cached values (those belong to other objects)
649- if key not in obj .chain_data .attr_sensor_events :
650- obj .chain_data .attr_sensor_events [key ] = []
652+ with obj .chain_data ._lock :
653+ if key not in obj .chain_data .attr_sensor_events :
654+ obj .chain_data .attr_sensor_events [key ] = []
651655
652656 if (len (new ) or len (old )) and now - region .when > DEBOUNCE_DELAY :
653657 log .debug ("REGION EVENT" , key , now_str , regionObjects , len (objects ))
@@ -685,18 +689,19 @@ def _updateRegionEvents(self, detectionType, regions, now, now_str, curObjects):
685689 # Always clean up exited objects, even if debounce prevented event emission
686690 for obj in regionObjects :
687691 if obj .gid in old :
688- obj .chain_data .regions .pop (key , None )
692+ with obj .chain_data ._lock :
693+ obj .chain_data .regions .pop (key , None )
689694
690- # Clean up sensor tracking on exit
691- if region .singleton_type is not None :
692- obj .chain_data .active_sensors .discard (key )
695+ # Clean up sensor tracking on exit
696+ if region .singleton_type is not None :
697+ obj .chain_data .active_sensors .discard (key )
693698
694- # Environmental sensors: clear state on exit (data doesn't persist)
695- if region .singleton_type == "environmental" :
696- obj .chain_data .env_sensor_state .pop (key , None )
699+ # Environmental sensors: clear state on exit (data doesn't persist)
700+ if region .singleton_type == "environmental" :
701+ obj .chain_data .env_sensor_state .pop (key , None )
697702
698- # Attribute sensors: keep event history (data persists after exit)
699- # attr_sensor_events[key] intentionally not removed
703+ # Attribute sensors: keep event history (data persists after exit)
704+ # attr_sensor_events[key] intentionally not removed
700705
701706 return updated
702707
0 commit comments