diff --git a/iti/applications/models/__init__.py b/iti/applications/models/__init__.py index 24e941d..338153a 100644 --- a/iti/applications/models/__init__.py +++ b/iti/applications/models/__init__.py @@ -13,4 +13,7 @@ from .sys.sys_file import SysFile, SysFileSchema, SysFileDirectory, SysFileDirec from .iot.iot_workshop import IotWorkshop, IotWorkshopSchema from .iot.iot_device import IotDevice, IotDeviceSchema from .iot.iot_endpoint import IotEndpoint, IotEndpointSchema -from .iot.iot_node import IotNode, IotNodeSchema \ No newline at end of file +from .iot.iot_node import IotNode, IotNodeSchema +from .iot.iot_alert_rule import IotAlertRule, IotAlertRuleSchema +from .iot.iot_alert_log import IotAlertLog, IotAlertLogSchema +from .iot.iot_alert_push import IotAlertPush, IotAlertPushSchema \ No newline at end of file diff --git a/iti/applications/models/iot/iot_alert_log.py b/iti/applications/models/iot/iot_alert_log.py new file mode 100644 index 0000000..b167f75 --- /dev/null +++ b/iti/applications/models/iot/iot_alert_log.py @@ -0,0 +1,40 @@ +from iti.applications.extensions import db +from iti.applications.common.crud import TimeModelMixin +from iti.applications.common.utils import BaseSchema +from apiflask.fields import String, Integer, DateTime, Nested + + +class IotAlertLog(db.Model, TimeModelMixin): + """ + 告警日志表 + """ + __tablename__ = "iot_alert_log" + id = db.Column( + db.Integer, + primary_key=True, + autoincrement=True, + comment="标识", + ) + alert_tag = db.Column(db.String(255), nullable=False, comment="告警标签") + alert_target_name = db.Column(db.String(255), nullable=False, comment="告警对象名称") + alert_content = db.Column(db.String(2048), nullable=False, comment="告警文本") + alert_level = db.Column(db.Integer, nullable=False, comment="告警级别 0-预警,1-一般,2-紧急,3-严重") + status = db.Column(db.Integer, nullable=False, default=1, comment="状态 1-告警中,0-已恢复") + trigger_count = db.Column(db.Integer, nullable=False, default=0, comment="触发次数") + +class IotAlertLogSchema(BaseSchema): + """ + 告警日志表响应结构 + """ + class Meta: + name = "IotAlertLog" + + id = Integer() + alert_tag = String() + alert_target_name = String() + alert_content = String() + alert_level = Integer() + status = Integer() + trigger_count = Integer() + created_at = DateTime(format="%Y-%m-%d %H:%M:%S") + updated_at = DateTime(format="%Y-%m-%d %H:%M:%S") diff --git a/iti/applications/models/iot/iot_alert_push.py b/iti/applications/models/iot/iot_alert_push.py new file mode 100644 index 0000000..fbac2ee --- /dev/null +++ b/iti/applications/models/iot/iot_alert_push.py @@ -0,0 +1,31 @@ +from iti.applications.extensions import db +from iti.applications.common.crud import TimeModelMixin +from iti.applications.common.utils import BaseSchema +from apiflask.fields import String, Integer, DateTime, Nested + +class IotAlertPush(TimeModelMixin, db.Model): + """ + 告警推送表 + """ + __tablename__ = "iot_alert_push" + id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment="标识") + target_name = db.Column(db.String(255), nullable=False, comment="接收对象名称") + push_url = db.Column(db.String(2048), nullable=False, comment="告警推送URL") + alert_level = db.Column(db.String(255), nullable=False, comment="告警等级") + status = db.Column(db.Integer, nullable=False, default=1, comment="状态 1-启用,0-禁用") + + +class IotAlertPushSchema(BaseSchema): + """ + 告警推送表响应结构 + """ + class Meta: + name = "IotAlertPush" + + id = Integer() + target_name = String() + push_url = String() + alert_level = String() + status = Integer() + created_at = DateTime(format="%Y-%m-%d %H:%M:%S") + updated_at = DateTime(format="%Y-%m-%d %H:%M:%S") \ No newline at end of file diff --git a/iti/applications/models/iot/iot_alert_rule.py b/iti/applications/models/iot/iot_alert_rule.py new file mode 100644 index 0000000..f029979 --- /dev/null +++ b/iti/applications/models/iot/iot_alert_rule.py @@ -0,0 +1,42 @@ +from iti.applications.extensions import db +from iti.applications.common.crud import TimeModelMixin +from iti.applications.common.utils import BaseSchema +from apiflask.fields import String, Integer, DateTime, Nested + + +class IotAlertRule(db.Model, TimeModelMixin): + """ + 告警规则表 + """ + __tablename__ = "iot_alert_rule" + id = db.Column( + db.Integer, + primary_key=True, + autoincrement=True, + comment="标识", + ) + rule_name = db.Column(db.String(255), nullable=False, comment="告警规则名称") + node_id = db.Column(db.Integer, nullable=False, comment="采集节点ID") + trigger_count = db.Column(db.Integer, nullable=False, comment="阈值触发次数,超过次数后告警") + alert_rule = db.Column(db.String(255), nullable=False, comment="告警触发表达式") + alert_text = db.Column(db.String(2048), nullable=False, comment="告警文本") + alert_level = db.Column(db.Integer, nullable=False, comment="告警级别 0-预警,1-一般,2-紧急,3-严重") + status = db.Column(db.Integer, nullable=False, default=1, comment="状态 1-启用,0-禁用") + +class IotAlertRuleSchema(BaseSchema): + """ + 告警规则表响应结构 + """ + class Meta: + name = "IotAlertRule" + + id = Integer() + rule_name = String() + node_id = Integer() + trigger_count = Integer() + alert_rule = String() + alert_text = String() + alert_level = Integer() + status = Integer() + created_at = DateTime(format="%Y-%m-%d %H:%M:%S") + updated_at = DateTime(format="%Y-%m-%d %H:%M:%S") diff --git a/iti/applications/models/iot/iot_endpoint.py b/iti/applications/models/iot/iot_endpoint.py index 1bf253a..51cdc9b 100644 --- a/iti/applications/models/iot/iot_endpoint.py +++ b/iti/applications/models/iot/iot_endpoint.py @@ -16,8 +16,6 @@ class IotEndpoint(db.Model, TimeModelMixin): autoincrement=True, comment="标识", ) - workshop_id = db.Column(db.Integer, nullable=False, default=0, unique=True, comment="车间ID") - device_id = db.Column(db.Integer, nullable=False, default=0, unique=True, comment="设备ID") endpoint_name = db.Column(db.String(255), nullable=False, unique=True, comment="采集端名称") endpoint_number = db.Column(db.String(20), nullable=False, comment="采集端编号") description = db.Column(db.Text, nullable=True, comment="采集端描述") @@ -27,15 +25,6 @@ class IotEndpoint(db.Model, TimeModelMixin): specification_model = db.Column(db.String(255), nullable=False, comment="规格型号") is_online = db.Column(db.Integer, nullable=False, default=0, comment="在线状态 0:离线 1:在线") status = db.Column(db.Integer, nullable=False, default=0, comment="状态 0:停用 1:运行中 2:维修中") - #关系 - workshop = db.relationship( - "IotWorkshop", - primaryjoin="foreign(IotEndpoint.workshop_id) == IotWorkshop.id", - ) - device = db.relationship( - "IotDevice", - primaryjoin="foreign(IotEndpoint.device_id) == IotDevice.id", - ) class IotEndpointSchema(BaseSchema): @@ -46,8 +35,6 @@ class IotEndpointSchema(BaseSchema): name = "IotEndpoint" id = Integer() - workshop_id = Integer() - device_id = Integer() endpoint_name = String() endpoint_number = String() description = String() @@ -59,9 +46,6 @@ class IotEndpointSchema(BaseSchema): status = Integer() created_at = DateTime(format="%Y-%m-%d %H:%M:%S") updated_at = DateTime(format="%Y-%m-%d %H:%M:%S") - #关系 - workshop = Nested("IotWorkshopSimpleSchema") - device = Nested("IotDeviceSimpleSchema") class IotEndpointSimpleSchema(BaseSchema): diff --git a/iti/applications/routes/iot/__init__.py b/iti/applications/routes/iot/__init__.py index 18215ef..58bde71 100644 --- a/iti/applications/routes/iot/__init__.py +++ b/iti/applications/routes/iot/__init__.py @@ -3,6 +3,9 @@ from .workshop_ctl import bp as workshop_bp from .device_ctl import bp as device_bp from .endpoint_ctl import bp as endpoint_bp from .node_ctl import bp as node_bp +from .alert_rule_ctl import bp as alert_rule_bp +from .alert_log_ctl import bp as alert_log_bp +from .alert_push_ctl import bp as alert_push_bp iot_bp = APIBlueprint("iot", __name__, url_prefix="/iot") @@ -12,5 +15,8 @@ def register_iot_bp(app): iot_bp.register_blueprint(device_bp) iot_bp.register_blueprint(endpoint_bp) iot_bp.register_blueprint(node_bp) + iot_bp.register_blueprint(alert_rule_bp) + iot_bp.register_blueprint(alert_log_bp) + iot_bp.register_blueprint(alert_push_bp) app.register_blueprint(iot_bp) diff --git a/iti/applications/routes/iot/alert_log_ctl.py b/iti/applications/routes/iot/alert_log_ctl.py new file mode 100644 index 0000000..612556b --- /dev/null +++ b/iti/applications/routes/iot/alert_log_ctl.py @@ -0,0 +1,96 @@ +from apiflask import APIBlueprint +from iti.applications.service.iot.alert import add_endpoint_alert_log +from iti.applications.extensions import db +from iti.applications.common.utils import success, page_schema, page +from iti.applications.models import ( + IotAlertLog, + IotAlertLogSchema, +) +from .schemas.alert_log import ( + AlertLogQuery, + AlertLogAddRequest, + AlertLogUpdateRequest, +) +from iti.applications.common import ModelFilter +from iti.applications.common.exceptions.biz_exp import BizException +from flask_jwt_extended import jwt_required +from sqlalchemy import select, delete, exists +from sqlalchemy.sql.functions import func +from sqlalchemy.orm import noload +from iti.applications.common import permission + +bp = APIBlueprint("iot_alert_log", __name__, url_prefix="/alertLog", tag="告警中心") + +@bp.get("/list") +@jwt_required() +@bp.doc(security="JWT") +@permission("iot:alertLog:list") +@bp.input(AlertLogQuery.Schema(partial=True), location="query") +@bp.output(IotAlertLogSchema(many=True)) +def list_alert_log(query_data: AlertLogQuery): + """ + 获取告警日志列表 + """ + + return success(get_list_or_page(query_data)) + +@bp.get("/page") +@jwt_required() +@bp.doc(security="JWT") +@permission("iot:alertLog:list") +@bp.input(AlertLogQuery.Schema(partial=True), location="query") +@bp.output(page_schema(IotAlertLogSchema)) +def page_alert_log(query_data: AlertLogQuery): + """ + 获取告警日志分页列表 + """ + + return success(get_list_or_page(query_data)) + + +@bp.post("/add/") +# @jwt_required() +# @bp.doc(security="JWT") +# @permission("iot:alertLog:add") +def add_alert_log(endpoint_id: int): + """ + 添加采集端网络不可达告警日志 + """ + + result = add_endpoint_alert_log(endpoint_id) + if len(result) > 0: + raise BizException(result) + + return success() + +@bp.post("/recover/") +@jwt_required() +@bp.doc(security="JWT") +@permission("iot:alertLog:update") +def recover_alert_log(id: int): + """ + 恢复告警日志 + """ + + alert_log = db.session.scalar(select(IotAlertLog).filter_by(id=id)) + if not alert_log: + raise BizException("告警日志不存在") + + alert_log.trigger_count =0 + alert_log.status = 0 + db.session.commit() + return success() + +def get_list_or_page(query_data: AlertLogQuery): + """ + 获取告警日志列表 + """ + query = select(IotAlertLog).order_by(IotAlertLog.created_at.desc()) + if query_data.alert_tag is not None: + query = query.filter(IotAlertLog.alert_tag == query_data.alert_tag) + if query_data.status is not None: + query = query.filter(IotAlertLog.status == query_data.status) + if query_data.page and query_data.size: + return db.paginate(query, page=query_data.page, per_page=query_data.size) + else: + return db.session.scalars(query).all() \ No newline at end of file diff --git a/iti/applications/routes/iot/alert_push_ctl.py b/iti/applications/routes/iot/alert_push_ctl.py new file mode 100644 index 0000000..e0a82e0 --- /dev/null +++ b/iti/applications/routes/iot/alert_push_ctl.py @@ -0,0 +1,83 @@ +from apiflask import APIBlueprint +from iti.applications.extensions import db, sys_log +from iti.applications.common.utils import success, page_schema, page +from iti.applications.models import ( + IotAlertPush, + IotAlertPushSchema, +) +from .schemas.alert_push import ( + AlertPushQuery, + AlertPushAddRequest, + AlertPushUpdateRequest, +) +from iti.applications.common import ModelFilter +from iti.applications.common.exceptions.biz_exp import BizException +from flask_jwt_extended import jwt_required +from sqlalchemy import select, delete, exists +from sqlalchemy.sql.functions import func +from sqlalchemy.orm import noload +from iti.applications.common import permission + +bp = APIBlueprint("iot_alert_push", __name__, url_prefix="/alertPush", tag="消息通知") + +@bp.get("/list") +@jwt_required() +@bp.doc(security="JWT") +@permission("iot:alertPush:list") +@bp.input(AlertPushQuery.Schema(partial=True), location="query") +@bp.output(IotAlertPushSchema(many=True)) +def list_alert_push(query_data: AlertPushQuery): + """ + 获取消息通知列表 + """ + + return success(get_list_or_page(query_data)) + +@bp.get("/page") +@jwt_required() +@bp.doc(security="JWT") +@permission("iot:alertPush:list") +@bp.input(AlertPushQuery.Schema(partial=True), location="query") +@bp.output(page_schema(IotAlertPushSchema)) +def page_alert_push(query_data: AlertPushQuery): + """ + 获取消息通知分页列表 + """ + + return success(get_list_or_page(query_data)) + + +@bp.post("/add") +@jwt_required() +@bp.doc(security="JWT") +@permission("iot:alertPush:add") +@bp.input(AlertPushAddRequest,location="json") +def add_alert_push(json_data: dict): + """ + 添加消息通知 + """ + + alert_push = IotAlertPush(**json_data) + alert_push.status = 0 + db.session.add(alert_push) + db.session.commit() + return success() + + +def get_list_or_page(query_data: AlertPushQuery): + """ + 获取消息通知列表 + """ + + query = select(IotAlertPush).order_by(IotAlertPush.created_at.desc()) + if query_data.keyword: + kw = ModelFilter.escape_like(query_data.keyword) + query = query.filter( + IotAlertPush.target_name.like(f"%{kw}%") + ) + if query_data.status is not None: + query = query.filter(IotAlertPush.status == query_data.status) + if query_data.page and query_data.size: + return db.paginate(query, page=query_data.page, per_page=query_data.size) + else: + return db.session.scalars(query).all() diff --git a/iti/applications/routes/iot/alert_rule_ctl.py b/iti/applications/routes/iot/alert_rule_ctl.py new file mode 100644 index 0000000..4ddb1ab --- /dev/null +++ b/iti/applications/routes/iot/alert_rule_ctl.py @@ -0,0 +1,21 @@ +from apiflask import APIBlueprint +from iti.applications.extensions import db, sys_log +from iti.applications.common.utils import success, page_schema, page +from iti.applications.models import ( + IotAlertRule, + IotAlertRuleSchema, +) +from .schemas.alert_rule import ( + AlertRuleQuery, + AlertRuleAddRequest, + AlertRuleUpdateRequest, +) +from iti.applications.common import ModelFilter +from iti.applications.common.exceptions.biz_exp import BizException +from flask_jwt_extended import jwt_required +from sqlalchemy import select, delete, exists +from sqlalchemy.sql.functions import func +from sqlalchemy.orm import noload +from iti.applications.common import permission + +bp = APIBlueprint("iot_alert_rule", __name__, url_prefix="/alertRule", tag="告警规则") diff --git a/iti/applications/routes/iot/endpoint_ctl.py b/iti/applications/routes/iot/endpoint_ctl.py index 2300954..2fc148f 100644 --- a/iti/applications/routes/iot/endpoint_ctl.py +++ b/iti/applications/routes/iot/endpoint_ctl.py @@ -33,7 +33,7 @@ def list_endpoint(query_data: EndpointQuery): 获取采集端列表 """ - return success(get_list(query_data)) + return success(get_list_or_page(query_data)) @bp.get("/page") @@ -47,7 +47,7 @@ def page_endpoint(query_data: EndpointQuery): 分页获取采集端列表 """ - return page(get_page(query_data)) + return page(get_list_or_page(query_data)) @bp.post("/add") @@ -72,10 +72,6 @@ def add_endpoint(json_data: dict): raise BizException("同编号采集端已存在") endpoint = IotEndpoint(**json_data) - device = db.session.scalar(select(IotDevice).options(noload(IotDevice.workshop)).filter_by(id = endpoint.device_id)) - if not device: - raise BizException("设备信息不存在") - endpoint.workshop_id = device.workshop_id endpoint.status = 0 db.session.add(endpoint) db.session.commit() @@ -104,9 +100,7 @@ def update_endpoint(id: int, json_data: dict): raise BizException("同编号采集端已存在") endpoint = db.session.scalar( - select(IotEndpoint) - .options(noload(IotEndpoint.workshop), noload(IotEndpoint.device)) - .filter_by(id=id)) + select(IotEndpoint).filter_by(id=id)) if not endpoint: raise BizException("采集端信息不存在") for key, value in json_data.items(): @@ -128,7 +122,6 @@ def delete_endpoint(id: int): endpoint = db.session.scalar( select(IotEndpoint) - .options(noload(IotEndpoint.workshop), noload(IotEndpoint.device)) .filter_by(id=id)) if not endpoint: raise BizException("采集端不存在") @@ -158,43 +151,21 @@ def count_endpoint(): return success(countData) -def get_page(query_data: EndpointQuery): - """ - 获取采集端信息分页 - """ - query = select(IotEndpoint).order_by(IotEndpoint.created_at.desc()) - if query_data.keyword: - kw = ModelFilter.escape_like(query_data.keyword) - query = query.filter( - IotEndpoint.endpoint_name.like(f"%{kw}%") - | IotEndpoint.endpoint_number.like(f"%{kw}%") - ) - if query_data.device_id: - query = query.filter(IotEndpoint.device_id == query_data.device_id) - elif query_data.workshop_id: - query = query.filter(IotEndpoint.workshop_id == query_data.workshop_id) - if query_data.status is not None: - query = query.filter(IotEndpoint.status == query_data.status) - - return db.paginate(query, page=query_data.page, per_page=query_data.size) - -def get_list(query_data: EndpointQuery): +def get_list_or_page(query_data: EndpointQuery): """ 获取采集端信息列表 """ - query = select(IotEndpoint).options(noload(IotEndpoint.device), noload(IotEndpoint.workshop)).order_by(IotEndpoint.created_at.desc()) + query = select(IotEndpoint).order_by(IotEndpoint.created_at.desc()) if query_data.keyword: kw = ModelFilter.escape_like(query_data.keyword) query = query.filter( IotEndpoint.endpoint_name.like(f"%{kw}%") | IotEndpoint.endpoint_number.like(f"%{kw}%") ) - if query_data.device_id: - query = query.filter(IotEndpoint.device_id == query_data.device_id) - elif query_data.workshop_id: - query = query.filter(IotEndpoint.workshop_id == query_data.workshop_id) if query_data.status is not None: query = query.filter(IotEndpoint.status == query_data.status) - - return db.session.scalars(query).all() \ No newline at end of file + if query_data.page and query_data.size: + return db.paginate(query, page=query_data.page, per_page=query_data.size) + else: + return db.session.scalars(query).all() \ No newline at end of file diff --git a/iti/applications/routes/iot/node_ctl.py b/iti/applications/routes/iot/node_ctl.py index ff76372..37be1ed 100644 --- a/iti/applications/routes/iot/node_ctl.py +++ b/iti/applications/routes/iot/node_ctl.py @@ -18,6 +18,7 @@ from iti.applications.common import ModelFilter from iti.applications.common.exceptions.biz_exp import BizException from flask_jwt_extended import jwt_required from sqlalchemy import select, delete, exists +from sqlalchemy.sql.functions import func from sqlalchemy.orm import noload from iti.applications.common import permission @@ -51,6 +52,23 @@ def page_node(query_data: NodeQuery): return page(get_page(query_data)) +@bp.get("/count") +@jwt_required() +@bp.doc(security="JWT") +@permission("iot:node:list") +def count_node(): + """ + 统计采集节点数量 + """ + + countData = {} + nodeReady = db.session.query(func.count(IotNode.id).label('number')).filter_by(status=1).first().number + nodeUnready = db.session.query(func.count(IotNode.id).label('number')).filter_by(status=0).first().number + countData["ready"] = nodeReady + countData["unReady"] = nodeUnready + countData["total"] = nodeReady + nodeUnready + + return success(countData) @bp.post("/add") @jwt_required() @@ -158,7 +176,7 @@ def get_page(query_data: NodeQuery): query = select(IotNode).order_by(IotNode.created_at.desc()) if query_data.endpoint_id: query = query.filter(IotNode.endpoint_id == query_data.endpoint_id) - elif query_data.device_id: + if query_data.device_id: query = query.filter(IotNode.device_id == query_data.device_id) elif query_data.workshop_id: query = query.filter(IotNode.workshop_id == query_data.workshop_id) @@ -175,7 +193,7 @@ def get_list(query_data: NodeQuery): query = select(IotNode).options(noload(IotNode.workshop), noload(IotNode.device), noload(IotNode.endpoint)).order_by(IotNode.created_at.desc()) if query_data.endpoint_id: query = query.filter(IotNode.endpoint_id == query_data.endpoint_id) - elif query_data.device_id: + if query_data.device_id: query = query.filter(IotNode.device_id == query_data.device_id) elif query_data.workshop_id: query = query.filter(IotNode.workshop_id == query_data.workshop_id) diff --git a/iti/applications/routes/iot/schemas/alert_log.py b/iti/applications/routes/iot/schemas/alert_log.py new file mode 100644 index 0000000..6802738 --- /dev/null +++ b/iti/applications/routes/iot/schemas/alert_log.py @@ -0,0 +1,122 @@ +from dataclasses import field +from marshmallow_dataclass import dataclass +from marshmallow import validates_schema, ValidationError +from iti.applications.common.utils.schema import BaseSchema, Pagination +from typing import ClassVar, Optional +from apiflask import fields + +@dataclass(base_schema=BaseSchema) +class AlertLogQuery(Pagination): + """ + 告警日志信息查询请求 + """ + alert_tag: str = field( + default=None, + metadata={ + "required": False, + "metadata": {"example": "ep1-nd0", "description": "告警标签"}, + }, + ) + status: int = field( + default=None, + metadata={ + "required": False, + "metadata": {"example": 0, "description": "状态"}, + }, + ) + Schema: ClassVar[BaseSchema] = BaseSchema + +class AlertLogAddRequest(BaseSchema): + """ + 告警日志信息添加请求 + """ + + alert_tag = fields.String( + required=True, + metadata={ + "example": "ep1-nd0", + "description": "告警标签", + }, + ) + alert_target_name = fields.String( + required=True, + metadata={ + "example": "ep1-nd0", + "description": "告警对象名称", + }, + ) + trigger_count = fields.Integer( + required=True, + metadata={ + "example": 1, + "description": "触发次数", + }, + ) + status = fields.Integer( + required=True, + metadata={ + "example": 0, + "description": "状态", + }, + ) + alert_content = fields.String( + required=True, + metadata={ + "example": "ep1-nd0", + "description": "告警内容", + }, + ) + alert_level = fields.Integer( + required=True, + metadata={ + "example": 0, + "description": "告警级别", + }, + ) + +class AlertLogUpdateRequest(BaseSchema): + """ + 告警日志信息更新请求 + """ + alert_tag = fields.String( + required=True, + metadata={ + "example": "ep1-nd0", + "description": "告警标签", + }, + ) + alert_target_name = fields.String( + required=True, + metadata={ + "example": "ep1-nd0", + "description": "告警对象名称", + }, + ) + trigger_count = fields.Integer( + required=True, + metadata={ + "example": 1, + "description": "触发次数", + }, + ) + status = fields.Integer( + required=True, + metadata={ + "example": 0, + "description": "状态", + }, + ) + alert_content = fields.String( + required=True, + metadata={ + "example": "ep1-nd0", + "description": "告警内容", + }, + ) + alert_level = fields.Integer( + required=True, + metadata={ + "example": 0, + "description": "告警级别", + }, + ) diff --git a/iti/applications/routes/iot/schemas/alert_push.py b/iti/applications/routes/iot/schemas/alert_push.py new file mode 100644 index 0000000..2537b4d --- /dev/null +++ b/iti/applications/routes/iot/schemas/alert_push.py @@ -0,0 +1,80 @@ +from dataclasses import field +from marshmallow_dataclass import dataclass +from marshmallow import validates_schema, ValidationError +from iti.applications.common.utils.schema import BaseSchema, Pagination +from typing import ClassVar, Optional +from apiflask import fields + +@dataclass(base_schema=BaseSchema) +class AlertPushQuery(Pagination): + """ + 告警推送信息查询请求 + """ + keyword: Optional[str] = field( + default=None, + metadata={ + "required": False, + "metadata": { + "description": "关键字 [消息对象名称] 模糊查询" + }, + }, + ) + status: int = field( + default=None, + metadata={ + "required": False, + "metadata": {"example": 0, "description": "状态"}, + }, + ) + Schema: ClassVar[BaseSchema] = BaseSchema + +class AlertPushAddRequest(BaseSchema): + """ + 告警推送信息添加请求 + """ + target_name = fields.String( + required=True, + metadata={ + "example": "ERP", + "description": "告警对象名称", + }, + ) + push_url = fields.String( + required=True, + metadata={ + "example": "https://www.baidu.com", + "description": "告警推送URL", + }, + ) + status = fields.Integer( + required=False, + metadata={ + "example": 1, + "description": "状态 0-禁用,1-启用", + }, + load_default=1, + ) + +class AlertPushUpdateRequest(BaseSchema): + """ + 告警推送信息更新请求 + """ + target_name = fields.String( + required=True, + metadata={ + "example": "ERP", + "description": "告警对象名称", + }, + ) + push_url = fields.String( + required=True, + metadata={ + "example": "https://www.baidu.com", + "description": "告警推送URL", + }, + ) + status = fields.Integer( + required=False, + metadata={"example": 1, "description": "状态 0-禁用,1-启用"}, + load_default=1, + ) \ No newline at end of file diff --git a/iti/applications/routes/iot/schemas/alert_rule.py b/iti/applications/routes/iot/schemas/alert_rule.py new file mode 100644 index 0000000..14d56ef --- /dev/null +++ b/iti/applications/routes/iot/schemas/alert_rule.py @@ -0,0 +1,100 @@ +from dataclasses import field +from marshmallow_dataclass import dataclass +from marshmallow import validates_schema, ValidationError +from iti.applications.common.utils.schema import BaseSchema, Pagination +from typing import ClassVar, Optional +from apiflask import fields + + +@dataclass(base_schema=BaseSchema) +class AlertRuleQuery(Pagination): + """ + 告警规则信息查询请求 + """ + node_id: int = field( + default=None, + metadata={ + "required": False, + "metadata": {"example": 1, "description": "节点ID"}, + }, + ) + status: int = field( + default=None, + metadata={ + "required": False, + "metadata": {"example": 0, "description": "状态"}, + }, + ) + Schema: ClassVar[BaseSchema] = BaseSchema + +class AlertRuleAddRequest(BaseSchema): + """ + 告警规则信息添加请求 + """ + node_id = fields.Integer( + required=True, + metadata={"example": 1, "description": "节点ID"}, + ) + rule_name = fields.String( + required=True, + metadata={"example": "告警规则1", "description": "告警规则名称"}, + ) + alert_rule = fields.String( + required=True, + metadata={"example": "x<4", "description": "告警规则表达式"}, + ) + alert_text = fields.String( + required=False, + metadata={"example": "节点{{node.title}}告警内容", "description": "告警内容文本模板"}, + ) + alert_level = fields.Integer( + required=False, + metadata={"example": 0, "description": "告警级别 0-预警,1-一般,2-紧急,3-严重"}, + load_default=0, + ) + trigger_count = fields.Integer( + required=False, + metadata={"example": 1, "description": "阈值触发次数,超过次数后告警"}, + load_default=1, + ) + status = fields.Integer( + required=False, + metadata={"example": 1, "description": "状态 1-启用,0-禁用"}, + load_default=1, + ) + +class AlertRuleUpdateRequest(BaseSchema): + """ + 告警规则信息更新请求 + """ + + node_id = fields.Integer( + required=True, + metadata={"example": 1, "description": "节点ID"}, + ) + rule_name = fields.String( + required=False, + metadata={"example": "告警规则1", "description": "告警规则名称"}, + ) + alert_rule = fields.String( + required=False, + metadata={"example": "x<4", "description": "告警规则表达式"}, + ) + alert_text = fields.String( + required=False, + metadata={"example": "节点{{node.title}}告警内容", "description": "告警内容文本模板"}, + ) + alert_level = fields.Integer( + required=False, + metadata={"example": 0, "description": "告警级别 0-预警,1-一般,2-紧急,3-严重"}, + ) + trigger_count = fields.Integer( + required=False, + metadata={"example": 1, "description": "阈值触发次数,超过次数后告警"}, + load_default=1, + ) + status = fields.Integer( + required=False, + metadata={"example": 1, "description": "状态 1-启用,0-禁用"}, + load_default=1, + ) \ No newline at end of file diff --git a/iti/applications/routes/iot/schemas/endpoint.py b/iti/applications/routes/iot/schemas/endpoint.py index c36b66e..a679c1c 100644 --- a/iti/applications/routes/iot/schemas/endpoint.py +++ b/iti/applications/routes/iot/schemas/endpoint.py @@ -21,20 +21,6 @@ class EndpointQuery(Pagination): }, }, ) - workshop_id: int = field( - default=None, - metadata={ - "required": False, - "metadata": {"example": 1, "description": "车间ID"}, - }, - ) - device_id: int = field( - default=None, - metadata={ - "required": False, - "metadata": {"example": 1, "description": "设备ID"}, - }, - ) status: int = field( default=None, metadata={ @@ -50,10 +36,6 @@ class EndpointAddRequest(BaseSchema): 采集端新增信息 """ - device_id = fields.Integer( - required=True, - metadata={"example": 1, "description": "设备ID"}, - ) endpoint_name = fields.String( required=True, metadata={"example": "采集端名称", "descriptrion": "采集端名称"}, @@ -92,10 +74,6 @@ class EndpointUpdateRequest(BaseSchema): 更新采集端信息 """ - device_id = fields.Integer( - required=True, - metadata={"example": 1, "description": "设备ID"}, - ) endpoint_name = fields.String( required=True, metadata={"example": "采集端名称", "descriptrion": "采集端名称"}, diff --git a/iti/applications/service/iot/alert.py b/iti/applications/service/iot/alert.py new file mode 100644 index 0000000..5177787 --- /dev/null +++ b/iti/applications/service/iot/alert.py @@ -0,0 +1,58 @@ +from sqlalchemy.orm import noload +from sqlalchemy.sql._typing import ColumnExpressionArgument +from typing import List, Dict, Any, Optional, Set +from iti.applications.extensions import db +from flask import current_app +from iti.applications.models import ( + IotAlertLog, + IotAlertRule, + IotAlertPush, + IotEndpoint, +) +from sqlalchemy import select, distinct + +def add_endpoint_alert_log(endpoint_id: int): + """ + 添加采集端告警日志 + Args: + endpoint_id: 采集端ID + """ + endpoint = db.session.scalar(select(IotEndpoint).filter_by(id=endpoint_id)) + if not endpoint: + return "采集端不存在" + alert_tag = f"ep{endpoint.id}_nd0" + alert_log = db.session.scalar(select(IotAlertLog).filter_by(alert_tag=alert_tag)) + if alert_log: + alert_log.trigger_count += 1 + if alert_log.trigger_count >= 3: + alert_log.status = 1 + push_alert(alert_log) + else: + dict_data = dict( + alert_tag=alert_tag, + alert_target_name=f"{endpoint.endpoint_name}({endpoint.endpoint_number})", + alert_level=1, + status=0, + trigger_count=1, + alert_content=f"采集端 {endpoint.endpoint_name}({endpoint.endpoint_number}) 网络不可达", + ) + alert_log = IotAlertLog(**dict_data) + + db.session.add(alert_log) + db.session.commit() + return "" + + +def push_alert(alert_log: IotAlertLog): + """ + 推送告警消息 + Args: + alert_log: 告警日志 + """ + + level = f"{alert_log.alert_level}" + alert_push_list = db.session.scalars(select(IotAlertPush).filter(IotAlertPush.alert_level.contains(level))).all() + if alert_push_list: + for alert_push in alert_push_list: + # TODO 调用地址发送告警消息 + current_app.logger.info(f"推送告警消息到 {alert_push.push_url},内容:{alert_log.alert_content}") \ No newline at end of file diff --git a/iti/applications/service/iot/influxdb_mgr.py b/iti/applications/service/iot/influxdb_mgr.py index 6923cf3..2b6d677 100644 --- a/iti/applications/service/iot/influxdb_mgr.py +++ b/iti/applications/service/iot/influxdb_mgr.py @@ -102,7 +102,7 @@ class InfluxDBMgr: if not hasattr(self, '_query_api') or not self._query_api: self._query_api = self.client.query_api() - test_query = f'from(bucket: "{self.bucket}") |> range(start: -1m) |> limit(n: 1)' + test_query = f'from(bucket: "{self.bucket}") |> range(start: -5s) |> limit(n: 1)' self._query_api.query(test_query) return True except Exception as e: @@ -154,7 +154,7 @@ class InfluxDBMgr: flux_query = f''' from(bucket: "{self.bucket}") - |> range(start: -15m) + |> range(start: -5m) |> filter(fn: (r) => r._measurement == "{query_measurement}") |> filter(fn: (r) => r._field == "{query_field}") |> yield(name: "mean")