auto-trainer-core 2.0.1.dev349__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_trainer_core-2.0.1.dev349.dist-info/METADATA +19 -0
- auto_trainer_core-2.0.1.dev349.dist-info/RECORD +73 -0
- auto_trainer_core-2.0.1.dev349.dist-info/WHEEL +5 -0
- auto_trainer_core-2.0.1.dev349.dist-info/top_level.txt +1 -0
- autotrainer/core/__init__.py +319 -0
- autotrainer/core/analysis/__init__.py +6 -0
- autotrainer/core/analysis/alarm_monitor.py +484 -0
- autotrainer/core/analysis/audio_spectrum_data.py +27 -0
- autotrainer/core/analysis/audio_spectrum_monitor.py +134 -0
- autotrainer/core/analysis/auto_tunnel_fan_monitor.py +92 -0
- autotrainer/core/analysis/detector.py +179 -0
- autotrainer/core/analysis/external_doors_monitor.py +124 -0
- autotrainer/core/analysis/global_animal_presence_monitor.py +69 -0
- autotrainer/core/analysis/head_fix_measurement.py +15 -0
- autotrainer/core/analysis/headbar_pressure_monitor.py +158 -0
- autotrainer/core/analysis/load_cell_monitor.py +377 -0
- autotrainer/core/analysis/load_cell_tare_monitor.py +217 -0
- autotrainer/core/analysis/measurement_data.py +80 -0
- autotrainer/core/analysis/pellet_position_monitor.py +108 -0
- autotrainer/core/analysis/sensor_analysis.py +390 -0
- autotrainer/core/analysis/system_fault_monitor.py +81 -0
- autotrainer/core/analysis/system_maintenance_monitor.py +88 -0
- autotrainer/core/animal/__init__.py +1 -0
- autotrainer/core/animal/animal_subject.py +208 -0
- autotrainer/core/capture.py +25 -0
- autotrainer/core/configuration/__init__.py +88 -0
- autotrainer/core/configuration/alarm_configuration.py +55 -0
- autotrainer/core/configuration/animal_presence_configuration.py +17 -0
- autotrainer/core/configuration/behavior_configuration.py +323 -0
- autotrainer/core/configuration/camera_configuration.py +108 -0
- autotrainer/core/configuration/external_doors_monitor_configuration.py +9 -0
- autotrainer/core/configuration/hardware_configuration.py +30 -0
- autotrainer/core/configuration/inference_configuration.py +26 -0
- autotrainer/core/configuration/load_cell_config.py +61 -0
- autotrainer/core/configuration/persistence_configuration.py +29 -0
- autotrainer/core/configuration/presence_detection_configuration.py +9 -0
- autotrainer/core/configuration/system_configuration.py +211 -0
- autotrainer/core/configuration/system_fault_config.py +9 -0
- autotrainer/core/configuration/system_maintenance_config.py +17 -0
- autotrainer/core/diamond_triangle_config.py +150 -0
- autotrainer/core/event/__init__.py +54 -0
- autotrainer/core/event/api_event_kind.py +5 -0
- autotrainer/core/event/api_event_plugin.py +57 -0
- autotrainer/core/event/event_info.py +53 -0
- autotrainer/core/event/event_manager.py +296 -0
- autotrainer/core/event/event_manager_plugin.py +45 -0
- autotrainer/core/event/file_event_plugin.py +91 -0
- autotrainer/core/event/logger_event_plugin.py +43 -0
- autotrainer/core/fixed_array_multiqueue.py +263 -0
- autotrainer/core/fixed_array_queue.py +124 -0
- autotrainer/core/frame_index.py +34 -0
- autotrainer/core/interfaces.py +347 -0
- autotrainer/core/logging.py +714 -0
- autotrainer/core/message/__init__.py +26 -0
- autotrainer/core/message/audio_spectrum_message.py +21 -0
- autotrainer/core/message/measurement_message.py +61 -0
- autotrainer/core/message/message_handler.py +158 -0
- autotrainer/core/message/motor_configuration_message.py +128 -0
- autotrainer/core/message/system_command_kind.py +74 -0
- autotrainer/core/message/system_message_handler.py +122 -0
- autotrainer/core/message/system_status_message.py +112 -0
- autotrainer/core/multiproc.py +50 -0
- autotrainer/core/notification/__init__.py +2 -0
- autotrainer/core/notification/notification_center.py +80 -0
- autotrainer/core/notification/trigger_notification.py +27 -0
- autotrainer/core/observable_object.py +105 -0
- autotrainer/core/perf_monitor.py +77 -0
- autotrainer/core/pose_elements.py +196 -0
- autotrainer/core/project/__init__.py +19 -0
- autotrainer/core/project/project_info.py +486 -0
- autotrainer/core/queue_util.py +60 -0
- autotrainer/core/reach_event.py +38 -0
- autotrainer/core/video_detection.py +326 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from tempfile import NamedTemporaryFile
|
|
10
|
+
from typing import Optional, Dict, Any, List, Type
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from autotrainer.core import Offset3DTuple, get_verbose_logger
|
|
14
|
+
|
|
15
|
+
logger = get_verbose_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_date_format = "%Y%m%d"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_old_format(data: Dict[str, Any]) -> "AnimalSubject":
|
|
22
|
+
kw = {}
|
|
23
|
+
if "name" in data:
|
|
24
|
+
kw['name'] = data["name"]
|
|
25
|
+
animal = AnimalSubject(**kw)
|
|
26
|
+
if "pellet_x" in data and "pellet_y" in data and "pellet_z" in data:
|
|
27
|
+
animal.pellet_x = data["pellet_x"]
|
|
28
|
+
animal.pellet_y = data["pellet_y"]
|
|
29
|
+
animal.pellet_z = data["pellet_z"]
|
|
30
|
+
return animal
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AnimalTraining:
|
|
35
|
+
"""Animal Training configuration"""
|
|
36
|
+
|
|
37
|
+
# NB: protocol == plan ; todo: could/should better be moved to auto-trainer-training repo
|
|
38
|
+
|
|
39
|
+
current_protocol: Optional[str] = None
|
|
40
|
+
protocols: List[Dict[str, Any]] = dataclasses.field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
def get_plan_progress(self, plan_id: str) -> Optional[Dict[str, Any]]:
|
|
43
|
+
# {"plan_id": self.plan_id,
|
|
44
|
+
# "progress_state": self.progress_state,
|
|
45
|
+
# "current_phase_id": None if self.current_phase is None else self.current_phase.phase_id,
|
|
46
|
+
# "progress": progress
|
|
47
|
+
# }
|
|
48
|
+
for prot in self.protocols:
|
|
49
|
+
if prot.get('plan_id') == plan_id:
|
|
50
|
+
return prot
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def set_plan_progress(self, plan_id: str, progress: Dict[str, Any]):
|
|
54
|
+
for idx, prog in enumerate(self.protocols):
|
|
55
|
+
if prog['plan_id'] == plan_id:
|
|
56
|
+
self.protocols[idx] = progress
|
|
57
|
+
return
|
|
58
|
+
self.protocols.append(progress)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class AnimalPelletCounts:
|
|
63
|
+
presented: int = 0 # == successfully sent/delivered to the animal
|
|
64
|
+
reaches: int = 0
|
|
65
|
+
success_reaches: int = 0
|
|
66
|
+
consumed: int = 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class _AnimalSubject:
|
|
71
|
+
"""A subject in an animal experiment."""
|
|
72
|
+
|
|
73
|
+
version: int = 2
|
|
74
|
+
|
|
75
|
+
name: str = ""
|
|
76
|
+
id: str = None # handled in post_init
|
|
77
|
+
|
|
78
|
+
is_pellet_dcs: bool = False
|
|
79
|
+
pellet_x: float = 0
|
|
80
|
+
pellet_y: float = 0
|
|
81
|
+
pellet_z: float = 0
|
|
82
|
+
|
|
83
|
+
training: AnimalTraining = dataclasses.field(default_factory=AnimalTraining)
|
|
84
|
+
|
|
85
|
+
pellet_counts_day_date: datetime.date = dataclasses.field(default_factory=datetime.date.today)
|
|
86
|
+
pellet_counts_day: AnimalPelletCounts = dataclasses.field(default_factory=AnimalPelletCounts)
|
|
87
|
+
pellet_counts_total: AnimalPelletCounts = dataclasses.field(default_factory=AnimalPelletCounts)
|
|
88
|
+
|
|
89
|
+
target_y_limit: Optional[float] = None # in DCS
|
|
90
|
+
|
|
91
|
+
def __post_init__(self):
|
|
92
|
+
if self.id is None:
|
|
93
|
+
self.id = str(uuid.uuid4())
|
|
94
|
+
if not self.name:
|
|
95
|
+
self.name = f"Mouse-{self.id}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class AnimalSubject(_AnimalSubject):
|
|
100
|
+
|
|
101
|
+
def __init__(self, **kwargs):
|
|
102
|
+
kwargs.pop("baseline_magnet_intensity", None)
|
|
103
|
+
super().__init__(**kwargs)
|
|
104
|
+
|
|
105
|
+
def __repr__(self):
|
|
106
|
+
return f"{self.__class__.__name__}(name={self.name!r}, id={self.id!r})"
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_file(cls: Type[Self], file_path: Path) -> Optional[Self]:
|
|
110
|
+
with file_path.open("r") as file:
|
|
111
|
+
try:
|
|
112
|
+
data = json.load(file)
|
|
113
|
+
if not isinstance(data, dict):
|
|
114
|
+
if not data:
|
|
115
|
+
# assuming empty file
|
|
116
|
+
return cls()
|
|
117
|
+
raise ValueError(f"Invalid file format in {file_path} ; not a dict: %s", type(data))
|
|
118
|
+
file_version = data.get("version", None)
|
|
119
|
+
if file_version is None or file_version != AnimalSubject.version:
|
|
120
|
+
logger.notice("Loaded animal version %s vs current Animal version %s",
|
|
121
|
+
file_version, AnimalSubject.version)
|
|
122
|
+
if "id" not in data:
|
|
123
|
+
# old format
|
|
124
|
+
animal = _load_old_format(data)
|
|
125
|
+
else:
|
|
126
|
+
reach = data.pop('reach')
|
|
127
|
+
pellet_dev = reach.pop('pelletDevice', None)
|
|
128
|
+
pellet_dcs = reach.pop('pelletDcs', None)
|
|
129
|
+
if pellet_dcs is None:
|
|
130
|
+
src = pellet_dev
|
|
131
|
+
else:
|
|
132
|
+
src = pellet_dcs
|
|
133
|
+
pellet_x, pellet_y, pellet_z = src['x'], src['y'], src['z']
|
|
134
|
+
training = data.pop('training')
|
|
135
|
+
pellet_counts_day_dct = data.pop("pelletCountsDay", {})
|
|
136
|
+
pellet_counts_total_dct = data.pop("pelletCountsTotal", {})
|
|
137
|
+
count_day_date_str: Optional[str] = data.pop('pelletCountsDayDate', None)
|
|
138
|
+
if count_day_date_str is None:
|
|
139
|
+
pellet_counts_day_date = datetime.date.today()
|
|
140
|
+
else:
|
|
141
|
+
pellet_counts_day_date = datetime.datetime.strptime(count_day_date_str, _date_format).date()
|
|
142
|
+
animal = cls(
|
|
143
|
+
id=data.pop('id'),
|
|
144
|
+
name=data.pop('name'),
|
|
145
|
+
is_pellet_dcs=pellet_dcs is not None,
|
|
146
|
+
target_y_limit=data.pop('targetYLimit', None),
|
|
147
|
+
pellet_x=pellet_x,
|
|
148
|
+
pellet_y=pellet_y,
|
|
149
|
+
pellet_z=pellet_z,
|
|
150
|
+
training=AnimalTraining(
|
|
151
|
+
current_protocol=training.pop('currentProtocol'),
|
|
152
|
+
protocols=training.pop('protocols'),
|
|
153
|
+
),
|
|
154
|
+
pellet_counts_day_date=pellet_counts_day_date,
|
|
155
|
+
pellet_counts_day=AnimalPelletCounts(**pellet_counts_day_dct),
|
|
156
|
+
pellet_counts_total=AnimalPelletCounts(**pellet_counts_total_dct),
|
|
157
|
+
)
|
|
158
|
+
except Exception as err:
|
|
159
|
+
logger.error("Error loading animal subject from %s: %s", file_path, err)
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
logger.debug("loaded animal id=%r name=%r pellet=%s is_dcs=%s current_protocol=%s",
|
|
163
|
+
animal.id, animal.name,
|
|
164
|
+
(animal.pellet_x, animal.pellet_y, animal.pellet_z), animal.is_pellet_dcs,
|
|
165
|
+
animal.training.current_protocol)
|
|
166
|
+
|
|
167
|
+
return animal
|
|
168
|
+
|
|
169
|
+
def to_file(self, file_path: Path):
|
|
170
|
+
reach: Dict[str, Any] = {}
|
|
171
|
+
key = "pelletDcs" if self.is_pellet_dcs else "pelletDevice"
|
|
172
|
+
reach[key] = {
|
|
173
|
+
'x': self.pellet_x,
|
|
174
|
+
'y': self.pellet_y,
|
|
175
|
+
'z': self.pellet_z,
|
|
176
|
+
}
|
|
177
|
+
data = {
|
|
178
|
+
"version": self.version,
|
|
179
|
+
"id": self.id,
|
|
180
|
+
"name": self.name,
|
|
181
|
+
"reach": reach,
|
|
182
|
+
"training": {
|
|
183
|
+
'currentProtocol': self.training.current_protocol,
|
|
184
|
+
'protocols': self.training.protocols,
|
|
185
|
+
},
|
|
186
|
+
"targetYLimit": self.target_y_limit,
|
|
187
|
+
"pelletCountsDayDate": self.pellet_counts_day_date.strftime(_date_format),
|
|
188
|
+
"pelletCountsDay": dataclasses.asdict(self.pellet_counts_day),
|
|
189
|
+
"pelletCountsTotal": dataclasses.asdict(self.pellet_counts_total),
|
|
190
|
+
}
|
|
191
|
+
xyz = Offset3DTuple(self.pellet_x, self.pellet_y, self.pellet_z)
|
|
192
|
+
logger.debug("Saving %s to %s ; xyz=%s", self.name, file_path.as_posix(), xyz.humanize())
|
|
193
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
with NamedTemporaryFile("w", delete=False, dir=file_path.parent) as fh:
|
|
195
|
+
json.dump(data, fh, indent=4)
|
|
196
|
+
os.replace(fh.name, file_path)
|
|
197
|
+
|
|
198
|
+
def check_today_date(self) -> bool:
|
|
199
|
+
"""Return True if changed"""
|
|
200
|
+
today = datetime.date.today()
|
|
201
|
+
prev = self.pellet_counts_day_date
|
|
202
|
+
if prev != today:
|
|
203
|
+
logger.debug("today (%s) != animal prev day date (%s), resetting day counts to 0",
|
|
204
|
+
today, prev)
|
|
205
|
+
self.pellet_counts_day_date = today
|
|
206
|
+
self.pellet_counts_day = AnimalPelletCounts()
|
|
207
|
+
return True
|
|
208
|
+
return False
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CaptureProcessStatus(IntEnum):
|
|
7
|
+
"""Valid VideoCaptureProcess states available through the status Value"""
|
|
8
|
+
|
|
9
|
+
TERMINATED = -2
|
|
10
|
+
"""The capture loop is terminated"""
|
|
11
|
+
|
|
12
|
+
FAILED = -1
|
|
13
|
+
"""Failed to configure or run process"""
|
|
14
|
+
|
|
15
|
+
UNKNOWN = 0
|
|
16
|
+
"""Uninitialized value not yet set by capture process"""
|
|
17
|
+
|
|
18
|
+
INITIALIZED = 1
|
|
19
|
+
"""The process is created, but not started"""
|
|
20
|
+
|
|
21
|
+
RUNNING = 2
|
|
22
|
+
"""The process is running the capture loop"""
|
|
23
|
+
|
|
24
|
+
RECORDING = 3
|
|
25
|
+
"""The process is recording the stream to disk"""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
6
|
+
from autotrainer.core import Offset3DTuple
|
|
7
|
+
|
|
8
|
+
logger = get_verbose_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_3D_CALIB_DIR_NAME = "4mm_6r_8c_4x"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generic_constructor(loader, tag, node):
|
|
15
|
+
cls_name = node.__class__.__name__
|
|
16
|
+
if cls_name == "SequenceNode":
|
|
17
|
+
return loader.construct_sequence(node)
|
|
18
|
+
elif cls_name == "MappingNode":
|
|
19
|
+
return loader.construct_mapping(node)
|
|
20
|
+
else:
|
|
21
|
+
return loader.construct_scalar(node)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GenericSafeLoader(yaml.SafeLoader):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
GenericSafeLoader.add_multi_constructor("", generic_constructor)
|
|
29
|
+
|
|
30
|
+
#
|
|
31
|
+
|
|
32
|
+
class SystemConfigurationLoader(yaml.SafeLoader):
|
|
33
|
+
"""Dedicated yaml loader for SystemConfiguration"""
|
|
34
|
+
safe_load: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SystemConfigurationSafeLoader(SystemConfigurationLoader):
|
|
38
|
+
|
|
39
|
+
safe_load = True
|
|
40
|
+
|
|
41
|
+
def ignore_unknown(self, suffix, node):
|
|
42
|
+
logger.warning("Skipping/ignoring tag/section %s", suffix)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _ignore_unknown(loader, suffix, node):
|
|
47
|
+
return loader.ignore_unknown(suffix, node)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# allow to ignore unknown tags:
|
|
51
|
+
SystemConfigurationSafeLoader.add_multi_constructor('', _ignore_unknown)
|
|
52
|
+
SystemConfigurationSafeLoader.add_multi_constructor('!', _ignore_unknown)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
_offset3dtag = "!Offset3DTuple"
|
|
57
|
+
_shift_xyz_tag = "!ShiftXYZTarget"
|
|
58
|
+
|
|
59
|
+
def construct_offset3d_tuple(loader: yaml.Loader, node: yaml.nodes.MappingNode):
|
|
60
|
+
content = loader.construct_mapping(node)
|
|
61
|
+
return Offset3DTuple(**content)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
SystemConfigurationLoader.add_constructor(_offset3dtag, construct_offset3d_tuple)
|
|
65
|
+
SystemConfigurationLoader.add_constructor(_shift_xyz_tag, construct_offset3d_tuple)
|
|
66
|
+
|
|
67
|
+
#
|
|
68
|
+
|
|
69
|
+
class SystemConfigurationDumper(yaml.SafeDumper):
|
|
70
|
+
"""Dedicated yaml dumper for SystemConfiguration"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def repr_offset3d_tuple(dumper: SystemConfigurationDumper, obj: Offset3DTuple):
|
|
74
|
+
x, y, z = obj
|
|
75
|
+
return dumper.represent_mapping(_offset3dtag, dict(x=x, y=y, z=z))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
SystemConfigurationDumper.add_representer(Offset3DTuple, repr_offset3d_tuple)
|
|
79
|
+
|
|
80
|
+
# above must be before below
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
from .behavior_configuration import BehaviorConfiguration, ShiftXYZTarget
|
|
84
|
+
from .camera_configuration import CameraConfiguration, CameraId
|
|
85
|
+
from .hardware_configuration import HardwareConfiguration
|
|
86
|
+
from .inference_configuration import InferenceConfiguration
|
|
87
|
+
from .persistence_configuration import PersistenceConfiguration
|
|
88
|
+
from .system_configuration import SystemConfiguration
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
import dataclasses
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclasses.dataclass
|
|
6
|
+
class _EmergencyAlarmConfiguration:
|
|
7
|
+
|
|
8
|
+
# 1st possible alarm condition
|
|
9
|
+
use_audio_load_cell_thrash: bool = False
|
|
10
|
+
auto_resume_on_audio_load_cell_thrash_resume: bool = False
|
|
11
|
+
|
|
12
|
+
audio_load_cell_thrash_aggregate_delay: float = 5 # up to how long ago to look at previous results
|
|
13
|
+
#
|
|
14
|
+
# ( ( if count of thrashing triggers greater than this (during last aggregate_delay)
|
|
15
|
+
load_cell_thrash_count: int = 3
|
|
16
|
+
# or percent of time it is ON during aggregate_delay
|
|
17
|
+
load_cell_thrash_percent_on: float = 50
|
|
18
|
+
# )
|
|
19
|
+
# and
|
|
20
|
+
# (
|
|
21
|
+
audio_thrash_count: int = 3 # spectrum thrash count greater than this (during last aggregate_delay)
|
|
22
|
+
audio_thrash_percent_on: float = 50 # or percent of time it is ON during aggregate_delay
|
|
23
|
+
# ) )
|
|
24
|
+
|
|
25
|
+
# 2nd possible alarm condition
|
|
26
|
+
use_presence_missing_after_exit_tunnel: bool = False
|
|
27
|
+
auto_resume_on_presence_seen_after_exit_tunnel: bool = False
|
|
28
|
+
tunnel_to_cage_presence_missing_delay: float = 5
|
|
29
|
+
|
|
30
|
+
# 3rd
|
|
31
|
+
use_external_doors_open: bool = False
|
|
32
|
+
auto_resume_on_external_doors_close: bool = False
|
|
33
|
+
|
|
34
|
+
# 4rd
|
|
35
|
+
use_global_animal_presence: bool = False
|
|
36
|
+
auto_resume_on_global_animal_presence: bool = False
|
|
37
|
+
|
|
38
|
+
# 5th
|
|
39
|
+
use_device_comm_error: bool = True
|
|
40
|
+
auto_resume_on_device_comm_error: bool = True
|
|
41
|
+
|
|
42
|
+
# 6th
|
|
43
|
+
use_system_maintenance: bool = True
|
|
44
|
+
auto_resume_on_system_maintenance: bool = True
|
|
45
|
+
|
|
46
|
+
# 7th
|
|
47
|
+
use_system_fault: bool = True
|
|
48
|
+
auto_resume_on_system_fault: bool = True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclasses.dataclass
|
|
52
|
+
class EmergencyAlarmConfiguration(_EmergencyAlarmConfiguration):
|
|
53
|
+
|
|
54
|
+
def __init__(self, **kwargs): # force kwargs
|
|
55
|
+
super().__init__(**kwargs)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclasses.dataclass
|
|
5
|
+
class _GlobalAnimalPresenceConfig:
|
|
6
|
+
|
|
7
|
+
presence_missing_delay_hours: float = 12
|
|
8
|
+
# if mouse not seen in tunnel AND not seen in cage longer than the delay, then trigger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GlobalAnimalPresenceConfig(_GlobalAnimalPresenceConfig):
|
|
12
|
+
|
|
13
|
+
def __init__(self, *,
|
|
14
|
+
presence_missing_delay: float = None, # noqa
|
|
15
|
+
**kwargs):
|
|
16
|
+
del presence_missing_delay # old param name # todo: remove some when later
|
|
17
|
+
super().__init__(**kwargs)
|