|
@@ -6,6 +6,7 @@ import uuid
|
|
import json
|
|
import json
|
|
import traceback
|
|
import traceback
|
|
from datetime import datetime, timezone, timedelta
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
+from collections import deque
|
|
|
|
|
|
import common.sys_comm as sys_comm
|
|
import common.sys_comm as sys_comm
|
|
from common.sys_comm import (
|
|
from common.sys_comm import (
|
|
@@ -161,6 +162,35 @@ class EventAttr_TargetAbsence(EventAttr_Base):
|
|
self.enter_ts_ = -1
|
|
self.enter_ts_ = -1
|
|
self.absence_time_ = -1
|
|
self.absence_time_ = -1
|
|
|
|
|
|
|
|
+# 事件属性 睡眠监测
|
|
|
|
+class EventAttr_SleepMonitoring(EventAttr_Base):
|
|
|
|
+ motion_stat = ["peaceful", "micro", "active", "leave"] # 空间状态
|
|
|
|
+ # 呼吸率划分:r0:[0-8) r1:[8,12) r2:[12,18) r3:[18,25] r4:25以上
|
|
|
|
+ breathe_stat = ["r0", "r1", "r2", "r3", "r4"] # 呼吸状态:呼吸过慢/异常,深睡,浅睡,REM/清醒,呼吸过快/清醒
|
|
|
|
+ # 深睡(deep),浅睡(light),REM,清醒(awake),离床(leave)
|
|
|
|
+ sleep_stat = [
|
|
|
|
+ ["deep", "deep", "light", "REM", "awake"], # peaceful
|
|
|
|
+ ["deep", "light", "light", "REM", "awake"], # micro
|
|
|
|
+ ["awake", "awake", "awake", "awake", "awake"], # active
|
|
|
|
+ ["leave", "leave", "leave", "leave", "leave"]] # leave
|
|
|
|
+ # 异常:呼吸过慢(slow_breathe),呼吸过快(fast_breathe)
|
|
|
|
+
|
|
|
|
+ def __init__(self, event_type):
|
|
|
|
+ self.start_sleep_ts_: int = -1 # 睡眠开始时间
|
|
|
|
+ self.end_sleep_ts_: int = -1 # 睡眠结束时间
|
|
|
|
+ self.motion_stat_ = None # 当前运动状态
|
|
|
|
+ self.breathe_stat_ = None # 当前呼吸状态
|
|
|
|
+ self.sleep_stat_ = None # 当前睡眠状态
|
|
|
|
+ self.sleep_segments_ = [] # 状态阶段记录 [{"ts":xxx, "stat":xxx}, ...]
|
|
|
|
+ self.last_motion_ = 0.0 # 最新的运动状态
|
|
|
|
+ self.last_breath_ = 0.0 # 最新的呼吸率
|
|
|
|
+ self.last_pts_ = [] # 最后的点目标
|
|
|
|
+ self.last_update_ts_ = -1
|
|
|
|
+ self.miss_target_count_ = 0
|
|
|
|
+
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+
|
|
# 事件属性 清理过期事件
|
|
# 事件属性 清理过期事件
|
|
class EventAttr_CleanExpireEvents(EventAttr_Base):
|
|
class EventAttr_CleanExpireEvents(EventAttr_Base):
|
|
def __init__(self, event_type):
|
|
def __init__(self, event_type):
|
|
@@ -180,6 +210,8 @@ event_attr_map = {
|
|
EventType.BATHROOM_STAY_FREQUENCY.value : EventAttr_BathroomStayFrequency,
|
|
EventType.BATHROOM_STAY_FREQUENCY.value : EventAttr_BathroomStayFrequency,
|
|
EventType.TARGET_ABSENCE.value : EventAttr_TargetAbsence,
|
|
EventType.TARGET_ABSENCE.value : EventAttr_TargetAbsence,
|
|
|
|
|
|
|
|
+ EventType.SLEEP_MONITORING.value : EventAttr_SleepMonitoring,
|
|
|
|
+
|
|
EventType.CLEAN_EXPIRE_EVENTS.value : EventAttr_CleanExpireEvents,
|
|
EventType.CLEAN_EXPIRE_EVENTS.value : EventAttr_CleanExpireEvents,
|
|
}
|
|
}
|
|
|
|
|
|
@@ -202,6 +234,10 @@ class AlarmPlan:
|
|
EventType.NIGHT_TOILETING_FREQUENCY_ABNORMAL.value : lambda plan: AlarmPlan.handle_night_toileting_frequency_abnormal(plan),
|
|
EventType.NIGHT_TOILETING_FREQUENCY_ABNORMAL.value : lambda plan: AlarmPlan.handle_night_toileting_frequency_abnormal(plan),
|
|
EventType.BATHROOM_STAY_FREQUENCY.value : lambda plan: AlarmPlan.handle_bathroom_stay_frequency(plan),
|
|
EventType.BATHROOM_STAY_FREQUENCY.value : lambda plan: AlarmPlan.handle_bathroom_stay_frequency(plan),
|
|
EventType.TARGET_ABSENCE.value : lambda plan: AlarmPlan.handle_target_absence(plan),
|
|
EventType.TARGET_ABSENCE.value : lambda plan: AlarmPlan.handle_target_absence(plan),
|
|
|
|
+
|
|
|
|
+ EventType.SLEEP_MONITORING.value : lambda plan: AlarmPlan.handle_sleep_monitoring(plan),
|
|
|
|
+
|
|
|
|
+ # 平台事件(任务)
|
|
EventType.CLEAN_EXPIRE_EVENTS.value : lambda plan: AlarmPlan.handle_clear_expire_events(plan),
|
|
EventType.CLEAN_EXPIRE_EVENTS.value : lambda plan: AlarmPlan.handle_clear_expire_events(plan),
|
|
}
|
|
}
|
|
|
|
|
|
@@ -232,7 +268,7 @@ class AlarmPlan:
|
|
self.linkage_action_ = linkage_action # 联动动作
|
|
self.linkage_action_ = linkage_action # 联动动作
|
|
self.tenant_id_ = tenant_id # 租户id
|
|
self.tenant_id_ = tenant_id # 租户id
|
|
|
|
|
|
- # 维护状态(根据TimePlanu判断)
|
|
|
|
|
|
+ # 维护状态(根据TimePlan判断)
|
|
self.status_ = 0 # 0未激活,1激活,-1过期
|
|
self.status_ = 0 # 0未激活,1激活,-1过期
|
|
self.status_update_ts_ = -1 # 状态更新时间,初始值为-1
|
|
self.status_update_ts_ = -1 # 状态更新时间,初始值为-1
|
|
|
|
|
|
@@ -833,6 +869,172 @@ class AlarmPlan:
|
|
LOGERR(f"[{frame.filename}:{frame.lineno}] @{frame.name}(), error: {e}")
|
|
LOGERR(f"[{frame.filename}:{frame.lineno}] @{frame.name}(), error: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
+ # 睡眠监测
|
|
|
|
+ def handle_sleep_monitoring(self):
|
|
|
|
+ # ---------------- 参数配置 ----------------
|
|
|
|
+ NO_TARGET_TIMEOUT_S = 3 # 超过3秒无数据认为无目标
|
|
|
|
+ NO_TARGET_MAX_COUNT = 3 # 连续3次无目标后确定离床
|
|
|
|
+ MOTION_SMOOTH_WINDOW = 10 # 平滑窗口大小(取最近10帧计算平均)
|
|
|
|
+ STAY_THRESHOLD_PEACEFUL = 0.05 # 静止阈值
|
|
|
|
+ STAY_THRESHOLD_MICRO = 0.15 # 微动阈值
|
|
|
|
+ LEAVE_BED_TS = 3 # 离床判定时间阈值
|
|
|
|
+
|
|
|
|
+ try:
|
|
|
|
+ dev_id = self.dev_id_
|
|
|
|
+ device:Device = g_Dev.g_dev_mgr.find_dev_map(dev_id)
|
|
|
|
+ if not device:
|
|
|
|
+ return
|
|
|
|
+ if (not self.rect_):
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ rtd_list = device.get_rtd_que_copy()
|
|
|
|
+ if not rtd_list:
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # 初始化状态机
|
|
|
|
+ if not hasattr(self, "sleep_stat_"):
|
|
|
|
+ self.sleep_stat_: str = "leave" # 当前睡眠状态
|
|
|
|
+ self.update_sleep_stat_: str = "leave" # 要更新的睡眠状态
|
|
|
|
+ self.avg_motion_: float = 0.0 # 当前运动幅值
|
|
|
|
+ self.motion_stat_: str = "leave" # 当前运动状态
|
|
|
|
+ self.avg_breath_: float = 0.0 # 当前呼吸率
|
|
|
|
+ self.breathe_stat_: str = "r0" # 当前呼吸状态
|
|
|
|
+
|
|
|
|
+ self.start_sleep_ts_: int = -1 # 睡眠开始时间
|
|
|
|
+ self.end_sleep_ts_: int = -1 # 睡眠结束时间
|
|
|
|
+
|
|
|
|
+ self.motion_window = deque(maxlen=MOTION_SMOOTH_WINDOW) # 近 N 次运动距离均值
|
|
|
|
+ self.sleep_segments_ = [] # 状态阶段记录 [{"ts":xxx, "stat":xxx}, ...]
|
|
|
|
+ self.last_pts_ = [] # 最后的点目标
|
|
|
|
+ self.last_update_ts_ = -1
|
|
|
|
+ self.miss_target_count_ = 0
|
|
|
|
+ self.last_leave_ts = -1 # 上次离床判定时间,离床判定时间超过5秒视为离床事件
|
|
|
|
+
|
|
|
|
+ now_ts = get_utc_time_s()
|
|
|
|
+ rtd_unit = rtd_list[-1]
|
|
|
|
+ ts = rtd_unit["timestamp"]
|
|
|
|
+ target_point = rtd_unit["target_point"]
|
|
|
|
+
|
|
|
|
+ ## 1. 空间状态分析
|
|
|
|
+ if now_ts - ts > NO_TARGET_TIMEOUT_S:
|
|
|
|
+ ## 目标不存在
|
|
|
|
+ # 起夜
|
|
|
|
+ x, y, z, snr = target_point
|
|
|
|
+ if not helper.is_point_in_rect(x, y, self.rect_):
|
|
|
|
+ motion = "leave"
|
|
|
|
+
|
|
|
|
+ # 检测不到体动
|
|
|
|
+ if self.start_sleep_ts_ == -1:
|
|
|
|
+ return
|
|
|
|
+ else:
|
|
|
|
+ motion = "peaceful"
|
|
|
|
+ else:
|
|
|
|
+ ## 目标存在
|
|
|
|
+ x = target_point[0]
|
|
|
|
+ y = target_point[1]
|
|
|
|
+ if not helper.is_point_in_rect(x, y, self.rect_):
|
|
|
|
+ # 不在床上
|
|
|
|
+ motion = "leave"
|
|
|
|
+ else:
|
|
|
|
+ # 在床上
|
|
|
|
+ # 计算体动
|
|
|
|
+ WINDOWS_S = 10 # 滑动事件窗口
|
|
|
|
+ recent_rtds = [r for r in rtd_list if now_ts - r["timestamp"] <= WINDOWS_S]
|
|
|
|
+ if len(recent_rtds) < 5:
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # 取最新点
|
|
|
|
+ x,y,z,snr = recent_rtds[-1]["target_point"]
|
|
|
|
+ # 检查是否在床上
|
|
|
|
+ if not helper.is_point_in_rect(x, y, self.rect_):
|
|
|
|
+ if self.last_leave_ts == -1:
|
|
|
|
+ self.last_leave_ts = now_ts
|
|
|
|
+ elif now_ts - self.last_leave_ts > LEAVE_BED_TS:
|
|
|
|
+ motion = "leave"
|
|
|
|
+ else:
|
|
|
|
+ self.last_leave_ts = -1
|
|
|
|
+ # 计算位移序列
|
|
|
|
+ motions = []
|
|
|
|
+ for i in range(1, len(recent_rtds)):
|
|
|
|
+ x1, y1, z1, _ = recent_rtds[i - 1]["target_point"]
|
|
|
|
+ x2, y2, z2, _ = recent_rtds[i]["target_point"]
|
|
|
|
+ dist = ((x2 - x1)**2 + (y2 - y1)**2 + (z2 - z1)**2)**0.5
|
|
|
|
+ motions.append(dist)
|
|
|
|
+ if motions:
|
|
|
|
+ avg_motion = sum(motions) / len(motions)
|
|
|
|
+ self.motion_window.append(avg_motion)
|
|
|
|
+ motion_smooth = sum(self.motion_window) / len(self.motion_window)
|
|
|
|
+ else:
|
|
|
|
+ motion_smooth = 0
|
|
|
|
+
|
|
|
|
+ # 状态判定
|
|
|
|
+ if motion_smooth < STAY_THRESHOLD_PEACEFUL:
|
|
|
|
+ motion = "peaceful"
|
|
|
|
+ elif motion_smooth < STAY_THRESHOLD_MICRO:
|
|
|
|
+ motion = "mocro"
|
|
|
|
+ else:
|
|
|
|
+ motion = "active"
|
|
|
|
+
|
|
|
|
+ self.motion_stat_ = motion
|
|
|
|
+
|
|
|
|
+ ## 2. 呼吸率分析
|
|
|
|
+ BREATHE_WINDOWS_S = 5 # 呼吸滑动时间窗口
|
|
|
|
+ recent_breaths =[r["breath_rpm"] for r in rtd_list
|
|
|
|
+ if (now_ts - r["timestamp"] <= BREATHE_WINDOWS_S) and
|
|
|
|
+ ("breath_rpm" in r and isinstance(r["breath_rpm"], (int, float)))]
|
|
|
|
+
|
|
|
|
+ if not recent_breaths:
|
|
|
|
+ breathe_stat = "r0" # 无数据时视为异常/无呼吸
|
|
|
|
+ avg_breath = 0.0
|
|
|
|
+ else:
|
|
|
|
+ avg_breath = sum(recent_breaths) / len(recent_breaths)
|
|
|
|
+
|
|
|
|
+ if avg_breath < 8:
|
|
|
|
+ breathe_stat = "r0"
|
|
|
|
+ elif avg_breath < 12:
|
|
|
|
+ breathe_stat = "r1"
|
|
|
|
+ elif avg_breath < 18:
|
|
|
|
+ breathe_stat = "r1"
|
|
|
|
+ else:
|
|
|
|
+ breathe_stat = "r1"
|
|
|
|
+
|
|
|
|
+ self.avg_breath_ = avg_breath
|
|
|
|
+ self.breathe_stat_ = breathe_stat
|
|
|
|
+
|
|
|
|
+ ## 3. 睡眠状态分析
|
|
|
|
+ try:
|
|
|
|
+ i_motion = EventAttr_SleepMonitoring.motion_stat.index(self.motion_stat_)
|
|
|
|
+ i_breath = EventAttr_SleepMonitoring.breathe_stat.index(breathe_stat)
|
|
|
|
+ self.update_sleep_stat_ = EventAttr_SleepMonitoring.sleep_stat[i_motion][i_breath]
|
|
|
|
+ except ValueError:
|
|
|
|
+ LOGERR(f"infalid i_montion or i_breath")
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ # 3.1 更新睡眠报告
|
|
|
|
+ sleep_node = {
|
|
|
|
+ "ts": now_ts,
|
|
|
|
+ "sleep_stat": self.update_sleep_stat_
|
|
|
|
+ }
|
|
|
|
+ self.sleep_segments_.append(sleep_node)
|
|
|
|
+
|
|
|
|
+ self.sleep_stat_ = self.update_sleep_stat_
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ # 1. 分析空间状态
|
|
|
|
+ # 2. 分析呼吸率
|
|
|
|
+ # 3. 分析睡眠状态
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ except json.JSONDecodeError as e:
|
|
|
|
+ tb_info = traceback.extract_tb(e.__traceback__)
|
|
|
|
+ for frame in tb_info:
|
|
|
|
+ LOGERR(f"[{frame.filename}:{frame.lineno}] @{frame.name}(), error:{e}, {e.doc}")
|
|
|
|
+ except Exception as e:
|
|
|
|
+ tb_info = traceback.extract_tb(e.__traceback__)
|
|
|
|
+ for frame in tb_info:
|
|
|
|
+ LOGERR(f"[{frame.filename}:{frame.lineno}] @{frame.name}(), error: {e}")
|
|
|
|
+
|
|
|
|
+
|
|
# 清理过期事件
|
|
# 清理过期事件
|
|
def handle_clear_expire_events(self):
|
|
def handle_clear_expire_events(self):
|
|
try:
|
|
try:
|