Repository navigation
Expand file tree
/
Copy pathcamera.py
More file actions
152 lines (121 loc) · 5.59 KB
/
Copy pathcamera.py
File metadata and controls
152 lines (121 loc) · 5.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""CameraImageScanner app: trigger and manage image processing scans for configured cameras.
Main features:
- Trigger image_processing scans when configured sensors (e.g., motion) change.
- Manage scan scheduling, throttling and automatic stop when inactivity is detected.
Key configuration keys:
- image_processor: entity_id of an image_processing configuration to call via service.
- sensors: list of binary_sensor ids that should trigger scans.
Example:
```yaml
camera_scanner:
module: camera
class: CameraImageScanner
image_processor: image_processing.my_person_detector
sensors:
- binary_sensor.motion_front_door
```
See module docstring and inline examples for usage.
"""
import appdaemon.plugins.hass.hassapi as hass
class CameraImageScanner(hass.Hass):
def initialize(self):
"""Initialize scanner: read args and register motion sensor listeners.
Side effects:
- Registers listeners on sensors to start/stop image scanning.
- Starts a periodic run_every timer to manage processing cadence.
"""
self.log("Hello from CameraImageScanner")
self._processing_handle = None
self._processing_count = 0
self._processing_max_count = 10000
# setup sane defaults
self._image_processor = self.args.get("image_processor", None)
self._sensors = self.args.get("sensors", [])
self._sensor_handles = {}
for sensor in self._sensors:
self.listen_state(self.trigger_start_image_scan_callback, sensor, new="on", old="off")
self.listen_state(self.trigger_stop_image_scan_callback, sensor, new="off", old="on")
def trigger_start_image_scan_callback(self, entity, attribute, old, new, kwargs):
"""Callback invoked when a sensor transitions to 'on' to start processing.
Args:
entity (str): entity id that changed.
attribute (str): attribute that changed.
old (str): previous state.
new (str): new state.
kwargs (dict): additional AppDaemon kwargs.
"""
self.log(
f"Callback trigger_image_scan_callback from {entity}:{attribute} {old}->{new}")
self.start_image_processing()
def trigger_stop_image_scan_callback(self, entity, attribute, old, new, kwargs):
"""Callback invoked when a sensor transitions to 'off' to possibly stop processing.
The method checks whether other sensors are still 'on' and only stops
if none remain active.
"""
self.log(
f"Callback trigger_stop_image_scan_callback from {entity}:{attribute} {old}->{new}")
if(self.count_on_sensors() > 0):
self.log(f"Ignoring status {new} of {entity} because {self.count_on_sensors()} sensors are still in status on")
return
self.stop_image_processing()
def count_sensors(self, state):
"""Count sensors in the configured `_sensors` list that match `state`.
Args:
state (str): state to compare (e.g., 'on' or 'off').
Returns:
int: number of matching sensors.
"""
count = 0
for sensor in self._sensors:
if self.get_state(sensor) == state:
count = count + 1
return count
def count_on_sensors(self):
"""Return count of configured sensors currently in state 'on'."""
return self.count_sensors("on")
def count_off_sensors(self):
"""Return count of configured sensors currently in state 'off'."""
return self.count_sensors("off")
def get_next_run_in_sec(self):
"""Return a dynamic retry interval (seconds) based on processing count.
The implementation uses step thresholds to gradually shorten the interval
when the processing count is low, allowing faster re-checks.
"""
seconds = 1
if(self._processing_count < 1000):
seconds = 60
if(self._processing_count < 500):
seconds = 15
if(self._processing_count < 200):
seconds = 10
if(self._processing_count < 100):
seconds = 5
if(self._processing_count < 60):
seconds = 2
return seconds
def process_image(self, kwargs):
"""Invoke the image_processing scan service for the configured processor.
This method increments an internal counter and reschedules itself
according to `get_next_run_in_sec` until the configured max count is reached.
"""
self.call_service(
"image_processing/scan", entity_id=self._image_processor)
self._processing_count += 1
self.log(f"Image processing count {self._processing_count}")
if self._processing_count < self._processing_max_count:
self._processing_handle = self.run_in(self.process_image, self.get_next_run_in_sec())
def start_image_processing(self):
"""Start a periodic image processing run sequence.
Stops any existing handle, resets counters and schedules the first run.
"""
self.stop_image_processing()
self._processing_count = 0
self.log("Starting image processing")
self._processing_handle = self.run_in(self.process_image, self.get_next_run_in_sec())
def stop_image_processing(self):
"""Stop any active image processing timer and mark processing as finished."""
if self._processing_handle is not None:
self.log("Stopping image processing")
self.cancel_timer(self._processing_handle)
self._processing_count = self._processing_max_count
self._processing_handle = None