├── .gitignore ├── LICENSE ├── README.md ├── deploy ├── README.md ├── nginx │ ├── nginx.conf │ ├── virtual_host.conf │ └── www │ │ └── .gitignore └── rule_engine │ └── .gitignore ├── docs ├── cn │ ├── README.md │ ├── device_data │ │ ├── charts.md │ │ └── reports.md │ ├── devices │ │ ├── emq_select.md │ │ ├── end_devices.md │ │ ├── gateway.md │ │ ├── groups.md │ │ └── security.md │ ├── products │ │ ├── codec.md │ │ └── emq_select.md │ └── publish │ │ ├── lwm2m.md │ │ └── mqtt.md └── en │ └── README.md ├── rule-engine ├── .gitignore ├── README.md ├── actorcloud-db-sink │ ├── README.md │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── io │ │ │ └── emqx │ │ │ └── pulsar │ │ │ └── io │ │ │ ├── ActorcloudPayload.java │ │ │ ├── ActorcloudSink.java │ │ │ ├── BaseData.java │ │ │ ├── DataModel.java │ │ │ ├── MapJsonDeserializer.java │ │ │ ├── Result.java │ │ │ ├── SubDevice.java │ │ │ └── jdbc │ │ │ ├── JdbcAbstractSink.java │ │ │ ├── JdbcSinkConfig.java │ │ │ └── JdbcUtils.java │ │ └── resources │ │ └── META-INF │ │ └── services │ │ └── pulsar-io.yaml ├── agent │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── io │ │ │ │ └── emqx │ │ │ │ └── stream │ │ │ │ └── agent │ │ │ │ ├── AgentApplication.java │ │ │ │ ├── Rule.java │ │ │ │ ├── RuleController.java │ │ │ │ └── analyzer │ │ │ │ └── RuleFeature.java │ │ └── resources │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── io │ │ └── emqx │ │ └── stream │ │ └── agent │ │ └── RuleControllerTests.java ├── client │ ├── pom.xml │ ├── readme.md │ ├── src │ │ └── main │ │ │ └── java │ │ │ ├── io │ │ │ └── emqx │ │ │ │ └── stream │ │ │ │ └── client │ │ │ │ ├── actorCloud │ │ │ │ └── MqttPublish.java │ │ │ │ ├── demo │ │ │ │ ├── RuleCommon.java │ │ │ │ └── RuleWindow.java │ │ │ │ ├── mqtt │ │ │ │ ├── MqttPublishSample.java │ │ │ │ └── Sample.java │ │ │ │ ├── presto │ │ │ │ └── Sample.java │ │ │ │ └── pulsar │ │ │ │ ├── ConsumerSample.java │ │ │ │ ├── ProducerSample.java │ │ │ │ ├── Sample.java │ │ │ │ └── SchemaProducer.java │ │ │ └── scripts │ │ │ ├── actorcloud-db-sink-config.yml │ │ │ ├── agent.service │ │ │ ├── db-sink-config.yml │ │ │ ├── emqx-config.yml │ │ │ ├── mail-sink-config.yml │ │ │ ├── publish-sink-config.yml │ │ │ └── stream-admin │ └── upload.cmd ├── common │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── emqx │ │ ├── pulsar │ │ └── functions │ │ │ └── windowing │ │ │ └── SessionConfig.java │ │ └── stream │ │ └── common │ │ ├── Constants.java │ │ ├── JsonParser.java │ │ └── internal │ │ ├── MockContext.java │ │ ├── MockGenericRecord.java │ │ └── MockLogger.java ├── db-sink │ ├── README.md │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── io │ │ │ └── emqx │ │ │ └── pulsar │ │ │ └── io │ │ │ ├── DatabaseActionConfig.java │ │ │ ├── DatabaseSink.java │ │ │ └── jdbc │ │ │ ├── JdbcAbstractSink.java │ │ │ ├── JdbcSinkConfig.java │ │ │ └── JdbcUtils.java │ │ └── resources │ │ └── META-INF │ │ └── services │ │ └── pulsar-io.yaml ├── emqx-source │ ├── README.md │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── io │ │ │ │ └── emqx │ │ │ │ └── pulsar │ │ │ │ └── io │ │ │ │ ├── AbstractEMQXSource.java │ │ │ │ ├── EMQXConfig.java │ │ │ │ ├── EMQXSink.java │ │ │ │ ├── EMQXSource.java │ │ │ │ └── SinkConfig.java │ │ └── resources │ │ │ └── META-INF │ │ │ └── services │ │ │ └── pulsar-io.yaml │ │ └── test │ │ └── java │ │ └── io │ │ └── emqx │ │ └── pulsar │ │ └── io │ │ └── tests │ │ ├── EMQXConfigTest.java │ │ └── EMQXTest.java ├── mail-sink │ ├── README.md │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── io │ │ │ └── emqx │ │ │ └── pulsar │ │ │ └── io │ │ │ ├── MailActionConfig.java │ │ │ ├── MailSink.java │ │ │ ├── MailSinkConfig.java │ │ │ └── SendMail.java │ │ └── resources │ │ └── META-INF │ │ └── services │ │ └── pulsar-io.yaml ├── pom.xml ├── publish-sink │ ├── README.md │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── io │ │ │ └── emqx │ │ │ └── pulsar │ │ │ └── io │ │ │ ├── PublishSink.java │ │ │ └── PublishSinkConfig.java │ │ └── resources │ │ └── META-INF │ │ └── services │ │ └── pulsar-io.yaml ├── rule-function │ ├── pom.xml │ └── src │ │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── emqx │ │ │ └── pulsar │ │ │ └── functions │ │ │ ├── AbstractRuleFunction.java │ │ │ ├── ContextWindowFunctionExecutor.java │ │ │ ├── DummyWindowFunction.java │ │ │ ├── RuleFunction.java │ │ │ ├── StringRuleFunction.java │ │ │ ├── StringWindowRuleFunction.java │ │ │ ├── WindowRuleFunction.java │ │ │ └── windowing │ │ │ ├── SessionTimeTriggerPolicy.java │ │ │ └── WindowAllowEmptyManager.java │ │ └── test │ │ └── java │ │ └── io │ │ └── emqx │ │ └── pulsar │ │ └── functions │ │ ├── TestContextWindowFunctionExecutor.java │ │ └── TestRuleFunction.java ├── sql │ ├── StreamAnalytic.md │ ├── pom.xml │ └── src │ │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── emqx │ │ │ └── stream │ │ │ └── common │ │ │ ├── GeoUtils.java │ │ │ ├── IRule.java │ │ │ ├── Rule.java │ │ │ └── sql │ │ │ ├── ISqlEngine.java │ │ │ ├── SqlEngine.java │ │ │ ├── SqlExecutionContext.java │ │ │ ├── SqlFunction.java │ │ │ ├── SqlFunctionProvider.java │ │ │ ├── Util.java │ │ │ ├── analyzer │ │ │ ├── FromItemAnalyzer.java │ │ │ ├── GroupExpressionAnalyzer.java │ │ │ ├── JoinConditionAnalyzer.java │ │ │ ├── RuleTablesNameFinder.java │ │ │ ├── SelectExpressionAnalyzer.java │ │ │ └── SqlAnalyzer.java │ │ │ ├── plan │ │ │ ├── AggregatePlan.java │ │ │ ├── FilterPlan.java │ │ │ ├── IPlan.java │ │ │ ├── JoinPlan.java │ │ │ ├── OutputPlan.java │ │ │ ├── PlanAdaptor.java │ │ │ ├── ProjectPlan.java │ │ │ ├── SqlPlan.java │ │ │ └── SyncPlan.java │ │ │ ├── pojo │ │ │ ├── JoinCondition.java │ │ │ ├── Selection.java │ │ │ ├── Tuple.java │ │ │ └── Window.java │ │ │ ├── validator │ │ │ └── StreamSqlValidator.java │ │ │ └── visitor │ │ │ ├── Aggregator.java │ │ │ ├── ExpressionEvaluator.java │ │ │ ├── ExpressionFilter.java │ │ │ └── FromVisitor.java │ │ └── test │ │ └── java │ │ └── io │ │ └── emqx │ │ └── stream │ │ └── common │ │ ├── TestSqlProcessor.java │ │ └── TestSqlValidator.java ├── topic-distribute-function │ ├── pom.xml │ └── src │ │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── emqx │ │ │ └── pulsar │ │ │ └── functions │ │ │ └── TopicDistributeFunction.java │ │ └── test │ │ └── java │ │ └── io │ │ └── emqx │ │ └── pulsar │ │ └── functions │ │ └── testDistributeFunction.java └── webhook-sink │ ├── README.md │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── io │ │ └── emqx │ │ └── pulsar │ │ └── io │ │ ├── WebhookActionConfig.java │ │ └── WebhookSink.java │ └── resources │ └── META-INF │ └── services │ └── pulsar-io.yaml ├── server ├── .env ├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md ├── actor_libs │ ├── __init__.py │ ├── auth │ │ ├── __init__.py │ │ ├── base.py │ │ └── resources.py │ ├── base_test.py │ ├── cache │ │ ├── __init__.py │ │ ├── _dict_code.py │ │ └── base.py │ ├── database │ │ ├── __init__.py │ │ ├── async_db │ │ │ ├── __init__.py │ │ │ └── base.py │ │ ├── orm │ │ │ ├── __init__.py │ │ │ ├── _model.py │ │ │ ├── _query.py │ │ │ └── utils.py │ │ └── sql │ │ │ ├── __init__.py │ │ │ └── base.py │ ├── decorators.py │ ├── emqx │ │ └── publish │ │ │ ├── protocol │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ └── lwm2m.py │ │ │ └── schemas.py │ ├── errors.py │ ├── http_tools │ │ ├── __init__.py │ │ ├── async_http.py │ │ ├── responses │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── emqx.py │ │ │ └── task_scheduler.py │ │ └── sync_http.py │ ├── logs.py │ ├── manage │ │ ├── __init__.py │ │ ├── data_init │ │ │ ├── __init__.py │ │ │ ├── db_base.py │ │ │ ├── default_roles.py │ │ │ └── table_init.py │ │ └── supervisord │ │ │ ├── __init__.py │ │ │ ├── _gunicorn.py │ │ │ ├── _templates │ │ │ ├── __init__.py │ │ │ ├── gunicorn.jinja │ │ │ └── supervisor.jinja │ │ │ └── base.py │ ├── schemas │ │ ├── __init__.py │ │ ├── devices.py │ │ └── fields.py │ ├── send_mails.py │ ├── tasks │ │ ├── __init__.py │ │ ├── _sql_statement.py │ │ ├── backend.py │ │ ├── exceptions.py │ │ ├── timer.py │ │ └── utils.py │ ├── types │ │ ├── __init__.py │ │ ├── base.py │ │ ├── exceptions.py │ │ ├── orm.py │ │ └── task.py │ └── utils.py ├── app │ ├── __init__.py │ ├── base.py │ ├── models.py │ ├── schemas.py │ └── services │ │ ├── __init__.py │ │ ├── alerts │ │ ├── __init__.py │ │ ├── models.py │ │ ├── resources.yml │ │ ├── schemas.py │ │ └── views │ │ │ ├── __init__.py │ │ │ └── alerts.py │ │ ├── applications │ │ ├── __init__.py │ │ ├── models.py │ │ ├── resources.yml │ │ ├── schemas.py │ │ └── views │ │ │ ├── __init__.py │ │ │ └── applications.py │ │ ├── base │ │ ├── __init__.py │ │ ├── models.py │ │ ├── resources.yml │ │ ├── schemas.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── file_system.py │ │ │ ├── logs.py │ │ │ ├── messages.py │ │ │ ├── overview.py │ │ │ ├── roles.py │ │ │ ├── select_options.py │ │ │ ├── system.py │ │ │ ├── tasks.py │ │ │ ├── tenants.py │ │ │ └── users.py │ │ ├── device_data │ │ ├── README.md │ │ ├── __init__.py │ │ ├── models.py │ │ ├── resources.yml │ │ ├── schemas.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── _reports_type │ │ │ ├── __init__.py │ │ │ └── aggr_events.py │ │ │ ├── _utils.py │ │ │ ├── capability_data.py │ │ │ ├── charts.py │ │ │ ├── connect_logs.py │ │ │ ├── events.py │ │ │ └── reports.py │ │ ├── devices │ │ ├── __init__.py │ │ ├── models.py │ │ ├── resources.yml │ │ ├── schemas.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── devices.py │ │ │ ├── emqx.py │ │ │ ├── groups.py │ │ │ ├── security.py │ │ │ └── select_options.py │ │ ├── products │ │ ├── __init__.py │ │ ├── models.py │ │ ├── resources.yml │ │ ├── schemas.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── codec.py │ │ │ ├── data_points.py │ │ │ ├── data_streams.py │ │ │ ├── products.py │ │ │ ├── select_options.py │ │ │ └── stream_points.py │ │ ├── publish │ │ ├── README.md │ │ ├── __init__.py │ │ ├── models.py │ │ ├── resources.yml │ │ ├── schemas.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── devices.py │ │ │ └── timers.py │ │ ├── rules │ │ ├── __init__.py │ │ ├── models.py │ │ ├── resources.yml │ │ ├── schemas.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── actions.py │ │ │ ├── rules.py │ │ │ └── select_options.py │ │ └── tasks_scheduler │ │ ├── README.md │ │ ├── __init__.py │ │ ├── async_tasks │ │ ├── README.md │ │ ├── __init__.py │ │ └── app │ │ │ ├── __init__.py │ │ │ ├── config.py │ │ │ ├── emqx │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── callback.py │ │ │ ├── publish.py │ │ │ └── sql_statements.py │ │ │ ├── excels │ │ │ ├── __init__.py │ │ │ ├── _utils.py │ │ │ ├── devices_export.py │ │ │ ├── devices_import.py │ │ │ ├── multi_language.py │ │ │ ├── sql_statements.py │ │ │ └── validate.py │ │ │ ├── extra.py │ │ │ └── views.py │ │ └── timer_tasks │ │ ├── README.md │ │ ├── __init__.py │ │ ├── app │ │ ├── __init__.py │ │ ├── api_count │ │ │ ├── __init__.py │ │ │ ├── count_task.py │ │ │ └── sql_statements.py │ │ ├── base.py │ │ ├── config.py │ │ ├── device_count │ │ │ ├── __init__.py │ │ │ ├── count_task.py │ │ │ └── sql_statements.py │ │ ├── device_events │ │ │ ├── __init__.py │ │ │ ├── aggr_task.py │ │ │ └── sql_statement.py │ │ ├── emqx_bills │ │ │ ├── __init__.py │ │ │ ├── aggr_task.py │ │ │ └── sql_statements.py │ │ └── timer_publish │ │ │ ├── __init__.py │ │ │ ├── _device.py │ │ │ ├── _parse.py │ │ │ ├── _utills.py │ │ │ └── publish_task.py │ │ └── test │ │ ├── __init__.py │ │ └── timer_publish.py ├── config │ ├── .gitignore │ ├── __init__.py │ ├── base │ │ ├── default_roles.yml │ │ ├── dict_code.yml │ │ ├── lwm2m_obj │ │ │ ├── 10241.xml │ │ │ ├── 10242.xml │ │ │ ├── 10243.xml │ │ │ ├── 10244.xml │ │ │ ├── 10245.xml │ │ │ ├── 10246.xml │ │ │ ├── 10247.xml │ │ │ ├── 10248.xml │ │ │ ├── 10249.xml │ │ │ ├── 10250.xml │ │ │ ├── 10251.xml │ │ │ ├── 10252.xml │ │ │ ├── 10253.xml │ │ │ ├── 10254.xml │ │ │ ├── 10255.xml │ │ │ ├── 10256.xml │ │ │ ├── 10257.xml │ │ │ ├── 10258.xml │ │ │ ├── 10259.xml │ │ │ ├── 10260.xml │ │ │ ├── 10262.xml │ │ │ ├── 10263.xml │ │ │ ├── 10264.xml │ │ │ ├── 10265.xml │ │ │ ├── 10266.xml │ │ │ ├── 10267.xml │ │ │ ├── 10268.xml │ │ │ ├── 10269.xml │ │ │ ├── 10270.xml │ │ │ ├── 10271.xml │ │ │ ├── 10272.xml │ │ │ ├── 10273.xml │ │ │ ├── 10274.xml │ │ │ ├── 10275.xml │ │ │ ├── 10276.xml │ │ │ ├── 10277.xml │ │ │ ├── 10278.xml │ │ │ ├── 10279.xml │ │ │ ├── 10280.xml │ │ │ ├── 10281.xml │ │ │ ├── 10282.xml │ │ │ ├── 10283.xml │ │ │ ├── 10284.xml │ │ │ ├── 10286.xml │ │ │ ├── 10299.xml │ │ │ ├── 2048.xml │ │ │ ├── 2049.xml │ │ │ ├── 2050.xml │ │ │ ├── 2051.xml │ │ │ ├── 2052.xml │ │ │ ├── 2053.xml │ │ │ ├── 2054.xml │ │ │ ├── 2055.xml │ │ │ ├── 2056.xml │ │ │ ├── 2057.xml │ │ │ ├── 3200.xml │ │ │ ├── 3201.xml │ │ │ ├── 3202.xml │ │ │ ├── 3203.xml │ │ │ ├── 3300.xml │ │ │ ├── 3301.xml │ │ │ ├── 3302.xml │ │ │ ├── 3303.xml │ │ │ ├── 3304.xml │ │ │ ├── 3305.xml │ │ │ ├── 3306.xml │ │ │ ├── 3308.xml │ │ │ ├── 3310.xml │ │ │ ├── 3311.xml │ │ │ ├── 3312.xml │ │ │ ├── 3313.xml │ │ │ ├── 3314.xml │ │ │ ├── 3315.xml │ │ │ ├── 3316.xml │ │ │ ├── 3317.xml │ │ │ ├── 3318.xml │ │ │ ├── 3319.xml │ │ │ ├── 3320.xml │ │ │ ├── 3321.xml │ │ │ ├── 3322.xml │ │ │ ├── 3323.xml │ │ │ ├── 3324.xml │ │ │ ├── 3325.xml │ │ │ ├── 3326.xml │ │ │ ├── 3327.xml │ │ │ ├── 3328.xml │ │ │ ├── 3329.xml │ │ │ ├── 3330.xml │ │ │ ├── 3331.xml │ │ │ ├── 3332.xml │ │ │ ├── 3333.xml │ │ │ ├── 3334.xml │ │ │ ├── 3335.xml │ │ │ ├── 3336.xml │ │ │ ├── 3337.xml │ │ │ ├── 3338.xml │ │ │ ├── 3339.xml │ │ │ ├── 3340.xml │ │ │ ├── 3341.xml │ │ │ ├── 3342.xml │ │ │ ├── 3343.xml │ │ │ ├── 3344.xml │ │ │ ├── 3345.xml │ │ │ ├── 3346.xml │ │ │ ├── 3347.xml │ │ │ ├── 3348.xml │ │ │ ├── 3349.xml │ │ │ ├── 3350.xml │ │ │ ├── 3351.xml │ │ │ ├── 3352.xml │ │ │ ├── 3353.xml │ │ │ ├── 3354.xml │ │ │ ├── 3355.xml │ │ │ ├── 3356.xml │ │ │ ├── 3357.xml │ │ │ ├── 3358.xml │ │ │ ├── 3359.xml │ │ │ ├── 3360.xml │ │ │ ├── 3361.xml │ │ │ ├── 3362.xml │ │ │ ├── 3363.xml │ │ │ ├── 3364.xml │ │ │ ├── 3365.xml │ │ │ ├── 3366.xml │ │ │ ├── 3367.xml │ │ │ ├── 3368.xml │ │ │ ├── 3369.xml │ │ │ ├── 3370.xml │ │ │ ├── 3371.xml │ │ │ ├── 3372.xml │ │ │ ├── 3373.xml │ │ │ ├── 3374.xml │ │ │ ├── 3375.xml │ │ │ ├── 3376.xml │ │ │ ├── 3377.xml │ │ │ ├── 3378.xml │ │ │ ├── 3379.xml │ │ │ ├── 3380.xml │ │ │ ├── 3381.xml │ │ │ ├── 3382.xml │ │ │ ├── 3383.xml │ │ │ ├── 3384.xml │ │ │ ├── 3385.xml │ │ │ ├── 3386.xml │ │ │ ├── LWM2M_APN_connection_profile-v1_0_obj_11.xml │ │ │ ├── LWM2M_Access_Control-v1_0_1_obj_2.xml │ │ │ ├── LWM2M_Bearer_selection-v1_0_obj_13.xml │ │ │ ├── LWM2M_Cellular_connectivity-v1_0_obj_10.xml │ │ │ ├── LWM2M_Connectivity_Monitoring-v1_0_1_obj_4.xml │ │ │ ├── LWM2M_Connectivity_Statistics-v1_0_1_obj_7.xml │ │ │ ├── LWM2M_DevCapMgmt-v1_0_obj_15.xml │ │ │ ├── LWM2M_Device-v1_0_1_obj_3.xml │ │ │ ├── LWM2M_Firmware_Update-v1_0_1_obj_5.xml │ │ │ ├── LWM2M_LOCKWIPE-v1_0_obj_8.xml │ │ │ ├── LWM2M_Location-v1_0_obj_6.xml │ │ │ ├── LWM2M_Portfolio-v1_0_obj_16.xml │ │ │ ├── LWM2M_Security-v1_0_obj_0.xml │ │ │ ├── LWM2M_Server-v1_0_obj_1.xml │ │ │ ├── LWM2M_Software_Component-v1_0_obj_14.xml │ │ │ ├── LWM2M_Software_Management-v1_0_obj_7.xml │ │ │ ├── LWM2M_WLAN_connectivity4-v1_0_obj_12.xml │ │ │ ├── OMA-SUP-LWM2M_BinaryAppDataContainer-V1_0-20171205-C.xml │ │ │ └── OMA-SUP-XML_LWM2M_EventLog-V1_0-20180228-C.xml │ │ └── services.yml │ ├── certs │ │ └── actorcloud │ │ │ ├── my_ca.crt │ │ │ ├── my_ca.key │ │ │ └── root_ca.crt │ └── config.yml ├── instance │ └── certs │ │ └── actorcloud │ │ └── .gitignore ├── manage.py ├── migrations │ └── .gitignore ├── requirements.txt ├── run.py └── static │ ├── download │ ├── export_excels │ │ └── .gitignore │ └── templates │ │ ├── .gitignore │ │ ├── devices_template_en.xlsx │ │ └── devices_template_zh.xlsx │ ├── images │ ├── favicon.ico │ ├── logo-dark.png │ ├── logo.png │ └── sign.png │ └── upload │ ├── excels │ └── .gitignore │ ├── images │ └── .gitignore │ └── packages │ └── .gitignore └── ui ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── apps.config.js ├── babel.config.js ├── commitlint.config.js ├── package.json ├── postcss.config.js ├── public ├── index.html └── static │ ├── .gitkeep │ └── css │ ├── fullLoading.css │ ├── iconfont.css │ ├── iconfont.eot │ ├── iconfont.js │ ├── iconfont.svg │ ├── iconfont.ttf │ ├── iconfont.woff │ └── iconfont.woff2 ├── src ├── App.vue ├── apps │ ├── accounts │ │ ├── assets │ │ │ └── images │ │ │ │ ├── bg-circle.png │ │ │ │ ├── bg-large.png │ │ │ │ ├── bg-small.png │ │ │ │ └── logo-actor.png │ │ ├── components │ │ │ ├── MessageRightbar.vue │ │ │ ├── RoleCardList.vue │ │ │ ├── RoleDetails.vue │ │ │ ├── RoleSelect.vue │ │ │ └── Roles.vue │ │ ├── lang │ │ │ ├── en.js │ │ │ └── zh_CN.js │ │ ├── routes.js │ │ ├── static │ │ │ └── bg-animation.json │ │ ├── store.js │ │ └── views │ │ │ ├── AppRoleDetails.vue │ │ │ ├── AppRoles.vue │ │ │ ├── ApplicationDetails.vue │ │ │ ├── Applications.vue │ │ │ ├── Dashboard.vue │ │ │ ├── Invitations.vue │ │ │ ├── Login.vue │ │ │ ├── LoginLogs.vue │ │ │ ├── MessageCenterDetails.vue │ │ │ ├── MessagesCenter.vue │ │ │ ├── Operation.vue │ │ │ ├── OperationDetails.vue │ │ │ ├── Overview.vue │ │ │ ├── Signup.vue │ │ │ ├── TenantDetails.vue │ │ │ ├── Tenants.vue │ │ │ ├── UserDetails.vue │ │ │ ├── UserRoleDetails.vue │ │ │ ├── UserRoles.vue │ │ │ └── Users.vue │ ├── alerts │ │ ├── assets │ │ │ └── tag.scss │ │ ├── lang │ │ │ ├── en.js │ │ │ └── zh_CN.js │ │ ├── routes.js │ │ ├── store.js │ │ └── views │ │ │ ├── CurrentAlertDetails.vue │ │ │ ├── CurrentAlerts.vue │ │ │ ├── HistoryAlertDetails.vue │ │ │ └── HistoryAlerts.vue │ ├── devices │ │ ├── assets │ │ │ └── images │ │ │ │ ├── alert-dark.png │ │ │ │ ├── alert.png │ │ │ │ ├── log-dark.png │ │ │ │ ├── log.png │ │ │ │ ├── noData.png │ │ │ │ └── noTrack.png │ │ ├── components │ │ │ ├── AddDevice.vue │ │ │ ├── ClientCapabilityData.vue │ │ │ ├── ClientDetails.vue │ │ │ ├── ClientEvent.vue │ │ │ ├── ClientTable.vue │ │ │ ├── ClientsCharts.vue │ │ │ ├── ConnectLogs.vue │ │ │ ├── CreateCert.vue │ │ │ ├── CreateProduct.vue │ │ │ ├── DeviceDataTable.vue │ │ │ ├── DeviceDetailTabs.vue │ │ │ ├── GatewayDetailTabs.vue │ │ │ ├── GroupDetailTabs.vue │ │ │ ├── InstructionCustom.vue │ │ │ ├── InstructionDialog.vue │ │ │ ├── InstructionLwM2M.vue │ │ │ ├── InstructionPlatform.vue │ │ │ ├── LocationSelectDialog.vue │ │ │ └── TimerPublishForm.vue │ │ ├── lang │ │ │ ├── en.js │ │ │ └── zh_CN.js │ │ ├── routes.js │ │ ├── store.js │ │ └── views │ │ │ ├── CertDetails.vue │ │ │ ├── Certs.vue │ │ │ ├── DeviceCapabilityData.vue │ │ │ ├── DeviceChildren.vue │ │ │ ├── DeviceCreate.vue │ │ │ ├── DeviceDetails.vue │ │ │ ├── DeviceDetailsCharts.vue │ │ │ ├── DeviceDetailsConnect.vue │ │ │ ├── DeviceDetailsControl.vue │ │ │ ├── DeviceDetailsEvents.vue │ │ │ ├── Devices.vue │ │ │ ├── GatewayCreate.vue │ │ │ ├── GatewayDetails.vue │ │ │ ├── GatewayDetailsConnect.vue │ │ │ ├── GatewayDetailsControl.vue │ │ │ ├── GatewayDetailsDevicesData.vue │ │ │ ├── GatewayDetailsEvents.vue │ │ │ ├── GatewayDevices.vue │ │ │ ├── Gateways.vue │ │ │ ├── GroupDetails.vue │ │ │ ├── GroupDetailsDeviceData.vue │ │ │ └── Groups.vue │ ├── lang.js │ ├── products │ │ ├── assets │ │ │ └── images │ │ │ │ └── noData.png │ │ ├── components │ │ │ ├── DataPointDialog.vue │ │ │ ├── DataPointForm.vue │ │ │ ├── DataPointTable.vue │ │ │ ├── ProductBreadcrumb.vue │ │ │ └── ProductDetailTabs.vue │ │ ├── lang │ │ │ ├── en.js │ │ │ └── zh_CN.js │ │ ├── routes.js │ │ ├── store.js │ │ └── views │ │ │ ├── DataStreamDetails.vue │ │ │ ├── ProductCodec.vue │ │ │ ├── ProductDefinition.vue │ │ │ ├── ProductDetails.vue │ │ │ ├── ProductDevices.vue │ │ │ ├── Products.vue │ │ │ └── ProfileReviews.vue │ ├── routes.js │ ├── rules │ │ ├── components │ │ │ ├── LocationScopeEditDialog.vue │ │ │ └── TopicForm.vue │ │ ├── lang │ │ │ ├── en.js │ │ │ └── zh_CN.js │ │ ├── routes.js │ │ ├── store.js │ │ └── views │ │ │ ├── ActionDetails.vue │ │ │ ├── Actions.vue │ │ │ ├── BusinessRuleDetails.vue │ │ │ ├── BusinessRules.vue │ │ │ ├── ScopeRuleDetails.vue │ │ │ ├── ScopeRules.vue │ │ │ ├── TimerPublish.vue │ │ │ └── TimerPublishDetails.vue │ ├── store.js │ ├── system │ │ ├── lang │ │ │ ├── en.js │ │ │ └── zh_CN.js │ │ ├── routes.js │ │ ├── store.js │ │ └── views │ │ │ ├── Logo.vue │ │ │ └── SystemInfo.vue │ └── test-center │ │ ├── lang │ │ ├── en.js │ │ └── zh_CN.js │ │ ├── routes.js │ │ ├── store.js │ │ └── views │ │ ├── CoAP.vue │ │ └── MQTTClient.vue ├── assets │ ├── images │ │ ├── 403.png │ │ ├── 404.png │ │ ├── created.png │ │ ├── empty-page.png │ │ ├── logo-darkTheme.png │ │ └── select-file.png │ ├── scss │ │ ├── base.scss │ │ ├── detailsPage.scss │ │ ├── element-variables.scss │ │ ├── emqCardList.scss │ │ ├── emqCrud.scss │ │ ├── reset.scss │ │ ├── themes │ │ │ ├── dark.scss │ │ │ └── light.scss │ │ └── variable.scss │ └── svg │ │ └── loading.svg ├── bus.js ├── components │ ├── 403.vue │ ├── 404.vue │ ├── CodeEditor.vue │ ├── EditToggleButton.vue │ ├── EmptyPage.vue │ ├── EmqButton.vue │ ├── EmqCrud.vue │ ├── EmqDetailsPageHead.vue │ ├── EmqDialog.vue │ ├── EmqExportExcel.vue │ ├── EmqImportExcel.vue │ ├── EmqSearchForm.vue │ ├── EmqSearchSelect.vue │ ├── EmqSelect.vue │ ├── EmqTag.vue │ ├── EmqUpload.vue │ ├── RightbarPop.vue │ ├── TabsCardHead.vue │ ├── TempPage.vue │ └── charts │ │ ├── Bar.vue │ │ ├── Line.vue │ │ ├── OnOffStatus.vue │ │ ├── PatternStatus.vue │ │ ├── Pie.vue │ │ ├── PieDoughnut.vue │ │ ├── SingleLine.vue │ │ └── mixins │ │ └── resize.js ├── filters.js ├── lang │ ├── en.js │ ├── index.js │ └── zh_CN.js ├── layout │ ├── components │ │ ├── Leftbar.vue │ │ └── Topbar.vue │ ├── lang │ │ ├── en.js │ │ ├── index.js │ │ └── zh_CN.js │ └── views │ │ └── Home.vue ├── main.js ├── mixins │ ├── common.js │ ├── currentDevices.js │ ├── currentProducts.js │ ├── detailsPage.js │ ├── loadDictCode.js │ └── select.js ├── router.js ├── store.js └── utils │ ├── MQTTConnect.js │ ├── api.js │ ├── chartUtils.js │ ├── element.js │ ├── installer.js │ ├── time.js │ └── variable.js ├── tests └── unit │ ├── .eslintrc.js │ └── components │ └── TabsCardHead.spec.js ├── vue.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | # Deploy Guide 2 | 3 | * nginx: nginx proxy 4 | * rule_engine: pulsar stream 5 | -------------------------------------------------------------------------------- /deploy/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | error_log /var/log/nginx/error.log info; 2 | 3 | events { 4 | use epoll; 5 | worker_connections 2048; 6 | } 7 | 8 | http { 9 | include /etc/nginx/mime.types; 10 | default_type application/octet-stream; 11 | charset utf-8; 12 | server_names_hash_bucket_size 64; 13 | server_tokens off; 14 | access_log off; 15 | sendfile on; 16 | client_max_body_size 128m; 17 | keepalive_timeout 120; 18 | gzip on; 19 | gzip_min_length 1k; 20 | gzip_comp_level 6; 21 | gzip_types text/plain application/x-javascript application/javascript text/javascript text/css application/xml; 22 | include virtual_host.conf; 23 | } -------------------------------------------------------------------------------- /deploy/nginx/virtual_host.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | listen 80; 4 | server_name host_ip; 5 | root /etc/nginx/www; 6 | index index.html; 7 | location / { 8 | try_files $uri $uri/ /index.html; 9 | } 10 | location /api/v1/ { 11 | proxy_pass http://127.0.0.1:7000/api/v1/; # ActorCloud backend node 12 | proxy_set_header X-Real-IP $remote_addr; 13 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 14 | proxy_set_header X-Forwarded-Proto $scheme; 15 | proxy_read_timeout 900; 16 | } 17 | location ~* ^.+.(jpg|jpeg|gif|css|png|js|ico|html|map)$ { 18 | access_log off; 19 | expires 12h; 20 | } 21 | } -------------------------------------------------------------------------------- /deploy/nginx/www/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /deploy/rule_engine/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /docs/cn/README.md: -------------------------------------------------------------------------------- 1 | # actorcloud 中文文档 2 | * 版本: v3.0 -------------------------------------------------------------------------------- /docs/cn/device_data/charts.md: -------------------------------------------------------------------------------- 1 | # 设备图表 2 | 3 | ### 设备历史图表数据 4 | > GET /devices/<:id>/charts 5 | ##### Request args 6 | * timeUnit(string): 5m, 1h, 6h, 1d, 1w 7 | 8 | ##### Example response 9 | ```json 10 | [ 11 | { 12 | "chartData": { 13 | "time": [ 14 | "2019-05-07 14:04:30" 15 | ], 16 | "value": [ 17 | 80 18 | ] 19 | }, 20 | "chartName": "温湿度数据流/湿度", 21 | "dataPointID": "hum", 22 | "streamID": "hum_temp" 23 | }, 24 | { 25 | "chartData": { 26 | "time": [ 27 | "2019-05-07 14:04:30" 28 | ], 29 | "value": [ 30 | 20 31 | ] 32 | }, 33 | "chartName": "温湿度数据流/温度", 34 | "dataPointID": "temp", 35 | "streamID": "hum_temp" 36 | } 37 | ] 38 | ``` 39 | 40 | ### 设备最新数据 41 | > GET /last_data_charts 42 | 43 | ##### Example response 44 | ```json 45 | [ 46 | { 47 | "chartData": { 48 | "time": "2019-05-07 14:04:30", 49 | "value": 80 50 | }, 51 | "chartName": "温湿度数据流/湿度", 52 | "dataPointID": "hum", 53 | "streamID": "hum_temp" 54 | }, 55 | { 56 | "chartData": { 57 | "time": "2019-05-07 14:04:30", 58 | "value": 20 59 | }, 60 | "chartName": "温湿度数据流/温度", 61 | "dataPointID": "temp", 62 | "streamID": "hum_temp" 63 | } 64 | ] 65 | ``` -------------------------------------------------------------------------------- /docs/cn/device_data/reports.md: -------------------------------------------------------------------------------- 1 | # 报表数据 2 | 3 | 4 | ### 设备聚合数据报表 5 | > GET /api/v1/reports 6 | -------------------------------------------------------------------------------- /docs/cn/devices/groups.md: -------------------------------------------------------------------------------- 1 | # 分组 2 | 3 | ### 获取分组列表 4 | > GET /groups 5 | ##### Example response 6 | ```json 7 | { 8 | "items": [ 9 | { 10 | "createAt": null, 11 | "description": null, 12 | "endDeviceCount": 2, 13 | "gatewayCount": 1, 14 | "groupID": "123456", 15 | "groupName": "分组", 16 | "id": 1, 17 | "updateAt": null 18 | } 19 | ], 20 | "meta": { 21 | "count": 1, 22 | "limit": 10, 23 | "page": 1 24 | } 25 | } 26 | ``` -------------------------------------------------------------------------------- /docs/cn/publish/lwm2m.md: -------------------------------------------------------------------------------- 1 | # LWM2M 协议设备下发 2 | 3 | 4 | ### 自定义指令下发 5 | ###### 请求示例 6 | > POST /device_publish 7 | ```json 8 | { 9 | "deviceID": "738692655169712", 10 | "topic": "/19/1/0", 11 | "payload": "{\"led\":true}" 12 | } 13 | ``` -------------------------------------------------------------------------------- /docs/cn/publish/mqtt.md: -------------------------------------------------------------------------------- 1 | # MQTT 协议设备下发 2 | 3 | 4 | ### 自定义指令下发 5 | ###### 请求示例 6 | > POST /device_publish 7 | ```json 8 | { 9 | "deviceID": "19689d1049961ce6a1d8416d1e08c2b1", 10 | "topic": "test", 11 | "payload": "{\"led\":true}" 12 | } 13 | ``` 14 | 15 | ##### 响应示例 16 | ```json 17 | { 18 | "message": "Client publish success", 19 | "progress": 100, 20 | "result": { 21 | "deviceID": "19689d1049961ce6a1d8416d1e08c2b1", 22 | "taskID": "9ceed57f8ae97d5e98ca9dada915ad2d" 23 | }, 24 | "status": 3 25 | } 26 | ``` 27 | 28 | ##### 客户端接收示例 29 | ```json 30 | {"data_type":"request","task_id":"9ceed57f8ae97d5e98ca9dada915ad2d","data":{"led":true}} 31 | ``` -------------------------------------------------------------------------------- /docs/en/README.md: -------------------------------------------------------------------------------- 1 | # actorcloud english documentation 2 | * version: v3.0 -------------------------------------------------------------------------------- /rule-engine/.gitignore: -------------------------------------------------------------------------------- 1 | target -------------------------------------------------------------------------------- /rule-engine/actorcloud-db-sink/README.md: -------------------------------------------------------------------------------- 1 | # actorcloud-db-sink 2 | 3 | 4 | ```bash 5 | bin/pulsar-admin sink create \ 6 | --className io.emqx.pulsar.io.ActorcloudSink \ 7 | --archive /opt/stream/actorcloud-db-sink-x.y.z.nar \ 8 | --tenant public \ 9 | --namespace default \ 10 | --name __sink_ac_db \ 11 | --inputs __emqx_all \ 12 | --sink-config-file /opt/stream/actorcloud-db-sink-config.yml 13 | ``` -------------------------------------------------------------------------------- /rule-engine/actorcloud-db-sink/src/main/java/io/emqx/pulsar/io/BaseData.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | import java.util.Map; 4 | 5 | abstract class BaseData { 6 | 7 | abstract boolean isInvalid(); 8 | 9 | 10 | boolean isDataMapInvalid(Map map) { 11 | if (map == null) { 12 | return true; 13 | } else { 14 | return map.values().stream().anyMatch(BaseData::isInvalid); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rule-engine/actorcloud-db-sink/src/main/java/io/emqx/pulsar/io/DataModel.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | 4 | import lombok.Data; 5 | 6 | @Data 7 | public class DataModel extends BaseData { 8 | /** 9 | * time : 12314141 10 | * value : 82 11 | */ 12 | 13 | private long time = -1; 14 | private Object value; 15 | 16 | public boolean isInvalid() { 17 | return time == -1 || value == null; 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /rule-engine/actorcloud-db-sink/src/main/java/io/emqx/pulsar/io/Result.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Data; 5 | 6 | 7 | @Data 8 | public class Result extends BaseData { 9 | 10 | /** 11 | * {"task_id":"177a4f99809b5639a425172797bc68ff",code:200} 12 | */ 13 | 14 | @SerializedName("task_id") 15 | private String taskID; 16 | 17 | private int code = -1; 18 | 19 | @Override 20 | boolean isInvalid() { 21 | return taskID == null || code == -1; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /rule-engine/actorcloud-db-sink/src/main/java/io/emqx/pulsar/io/SubDevice.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Data; 5 | 6 | import java.util.HashMap; 7 | 8 | @Data 9 | public class SubDevice extends BaseData { 10 | 11 | /** 12 | * { 13 | * "device_id": "device_id_1", 14 | * "data": { 15 | * "status": { 16 | * "time": 1547661822, 17 | * "value": true 18 | * }, 19 | * "mode": { 20 | * "time": 1547661822, 21 | * "value": "cold" 22 | * } } 23 | * } 24 | */ 25 | 26 | @SerializedName("device_id") 27 | private String deviceID; 28 | 29 | private HashMap data; 30 | 31 | @Override 32 | public boolean isInvalid() { 33 | return deviceID == null || isDataMapInvalid(data); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /rule-engine/actorcloud-db-sink/src/main/java/io/emqx/pulsar/io/jdbc/JdbcSinkConfig.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io.jdbc; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 5 | import lombok.*; 6 | import lombok.experimental.Accessors; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.Serializable; 11 | import java.util.Map; 12 | 13 | 14 | @Data 15 | @Setter 16 | @Getter 17 | @EqualsAndHashCode 18 | @ToString 19 | @Accessors(chain = true) 20 | public class JdbcSinkConfig implements Serializable { 21 | 22 | private static final long serialVersionUID = 1L; 23 | 24 | 25 | private String userName; 26 | 27 | private String password; 28 | 29 | private String jdbcUrl; 30 | 31 | private String tableName; 32 | 33 | private String[] columns; 34 | 35 | private int timeoutMs = 500; 36 | 37 | private int batchSize = 10; 38 | 39 | @SuppressWarnings("unused") 40 | public static JdbcSinkConfig load(String yamlFile) throws IOException { 41 | ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); 42 | return mapper.readValue(new File(yamlFile), JdbcSinkConfig.class); 43 | } 44 | 45 | public static JdbcSinkConfig load(Map map) throws IOException { 46 | ObjectMapper mapper = new ObjectMapper(); 47 | return mapper.readValue(new ObjectMapper().writeValueAsString(map), JdbcSinkConfig.class); 48 | } 49 | } -------------------------------------------------------------------------------- /rule-engine/actorcloud-db-sink/src/main/resources/META-INF/services/pulsar-io.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: actorcloud-db-sink 21 | description: Sink to save data to actorcloud's database 22 | sinkClass: io.emqx.pulsar.io.ActorcloudSink -------------------------------------------------------------------------------- /rule-engine/agent/src/main/java/io/emqx/stream/agent/AgentApplication.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.agent; 2 | 3 | import org.apache.pulsar.client.admin.PulsarAdmin; 4 | import org.apache.pulsar.client.api.PulsarClientException; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.Bean; 8 | 9 | import static org.springframework.boot.SpringApplication.run; 10 | 11 | @SuppressWarnings("unused") 12 | @SpringBootApplication 13 | public class AgentApplication { 14 | public static void main(String[] args) { 15 | run(AgentApplication.class, args); 16 | } 17 | 18 | 19 | @Value("${pulsar.adminUrl}") 20 | private String pulsarUrl; 21 | 22 | 23 | @Bean 24 | public PulsarAdmin pulsarAdmin() throws PulsarClientException { 25 | return PulsarAdmin.builder().serviceHttpUrl(pulsarUrl).build(); 26 | } 27 | } -------------------------------------------------------------------------------- /rule-engine/agent/src/main/java/io/emqx/stream/agent/Rule.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.agent; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NonNull; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | public class Rule { 13 | @NonNull 14 | private String id; 15 | @NonNull 16 | private String sql; 17 | @NonNull 18 | private List> actions; 19 | private boolean enabled; 20 | } -------------------------------------------------------------------------------- /rule-engine/agent/src/main/java/io/emqx/stream/agent/analyzer/RuleFeature.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.agent.analyzer; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import lombok.experimental.Accessors; 6 | 7 | import java.util.List; 8 | 9 | @NoArgsConstructor 10 | @Accessors(fluent = true) @Data 11 | public class RuleFeature { 12 | private List topics; 13 | private int windowType; //0 none, 1 duration 2 count 3 session 14 | private long windowLength; 15 | private long windowInterval; 16 | } 17 | -------------------------------------------------------------------------------- /rule-engine/agent/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | pulsar.broker=pulsar://127.0.0.1:6650 2 | server.port=8888 3 | pulsar.tenant=public 4 | pulsar.namespace=default 5 | pulsar.ruleClass=io.emqx.pulsar.functions.StringRuleFunction 6 | pulsar.windowRuleClass=io.emqx.pulsar.functions.ContextWindowFunctionExecutor 7 | pulsar.ruleJar=file:/opt/stream/0.5.3/rule-function-jar-with-dependencies.jar 8 | #below are deprecated 9 | pulsar.adminTopic=__acruleadmin 10 | pulsar.adminUrl=http://127.0.0.1:8080 11 | pulsar.respTopic=__acruleresp 12 | pulsar.ruleTopic=__acschema 13 | pulsar.sourceTopic=__actorcloud -------------------------------------------------------------------------------- /rule-engine/client/readme.md: -------------------------------------------------------------------------------- 1 | # Client samples for emqx and pulsar 2 | 3 | Just samples, for dev only -------------------------------------------------------------------------------- /rule-engine/client/src/main/java/io/emqx/stream/client/pulsar/Sample.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.client.pulsar; 2 | 3 | import org.apache.pulsar.client.api.Consumer; 4 | import org.apache.pulsar.client.api.Message; 5 | import org.apache.pulsar.client.api.PulsarClient; 6 | import org.apache.pulsar.client.api.PulsarClientException; 7 | 8 | public class Sample { 9 | 10 | 11 | 12 | @SuppressWarnings("InfiniteLoopStatement") 13 | public static void main(String[] args) throws PulsarClientException { 14 | String namespace = "sample/standalone/ns1"; // This namespace is created automatically 15 | String topic = String.format("persistent://%s/my-topic", namespace); 16 | PulsarClient client = PulsarClient.builder() 17 | .serviceUrl("pulsar://127.0.0.1:6650") 18 | .build(); 19 | Consumer consumer = client.newConsumer() 20 | .topic(topic) 21 | .subscriptionName("my-subscription") 22 | .subscribe(); 23 | do { 24 | // Wait for a message 25 | Message msg = consumer.receive(); 26 | 27 | System.out.printf("Message received: %s", new String(msg.getData())); 28 | 29 | // Acknowledge the message so that it can be deleted by the message broker 30 | consumer.acknowledge(msg); 31 | } while (true); 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /rule-engine/client/src/main/java/scripts/actorcloud-db-sink-config.yml: -------------------------------------------------------------------------------- 1 | configs: 2 | userName: root 3 | password: public 4 | jdbcUrl: jdbc:postgresql://192.168.6.15:5432/actorcloud 5 | tableName: device_events 6 | columns: 7 | - topic 8 | - msgTime 9 | - tenantID 10 | - deviceID 11 | - data 12 | - dataType 13 | - streamID 14 | - responseResult 15 | -------------------------------------------------------------------------------- /rule-engine/client/src/main/java/scripts/agent.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=GOAT stream rule engine agent 3 | After=syslog.target 4 | 5 | [Service] 6 | EnvironmentFile=/etc/systemd/system/agent.conf 7 | User=root 8 | ExecStart=/usr/bin/java -jar ${RULE_ENGINE_PATH}/agent.jar --pulsar.ruleJar=file:${RULE_ENGINE_PATH}/rule-function-jar-with-dependencies.jar 9 | SuccessExitStatus=143 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /rule-engine/client/src/main/java/scripts/db-sink-config.yml: -------------------------------------------------------------------------------- 1 | configs: 2 | userName: actorcloud 3 | password: public 4 | jdbcUrl: jdbc:postgresql://192.168.0.3:5432/actorcloud 5 | tableName: device_events 6 | columns: 7 | - topic 8 | - msgTime 9 | - tenantID 10 | - deviceID 11 | - productID 12 | - payload_string 13 | -------------------------------------------------------------------------------- /rule-engine/client/src/main/java/scripts/emqx-config.yml: -------------------------------------------------------------------------------- 1 | configs: 2 | brokerUrl: tcp://127.0.0.1:11883 3 | inputTopics: $share/group1/# 4 | ruleId: __emqx_all -------------------------------------------------------------------------------- /rule-engine/client/src/main/java/scripts/mail-sink-config.yml: -------------------------------------------------------------------------------- 1 | configs: 2 | host: smtp.xxx.com 3 | port: 25 4 | user: xxx@xxx.com 5 | password: xxxxxx 6 | from: xxx@xxx.com 7 | encryptionType: STARTTLS 8 | debug: false 9 | 10 | -------------------------------------------------------------------------------- /rule-engine/client/src/main/java/scripts/publish-sink-config.yml: -------------------------------------------------------------------------------- 1 | configs: 2 | url: http://127.0.0.1:8081/api/v3/mqtt/publish 3 | username: actorcloud 4 | password: Mjg3MjcxMjk4ODkzNjA3NzMzMzc3OTY0MTk0NTI2NjU4NTG -------------------------------------------------------------------------------- /rule-engine/client/upload.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Script for windows to upload the output jars to remote 3 | set SSHRSA=C:\Users\Administrator\.ssh\id_rsa.ppk 4 | set PROJECT_PATH=D:\Workspace\incubator 5 | set VERSION=0.5.3 6 | set TARGET=root@139.198.189.110:/opt/stream 7 | 8 | echo Start uploading 9 | pscp -i %SSHRSA% -P 2222 -r %PROJECT_PATH%\target\%VERSION% %TARGET% -------------------------------------------------------------------------------- /rule-engine/common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | stream 7 | com.emqx.stream 8 | 0.5.3 9 | 10 | 4.0.0 11 | 12 | common 13 | 14 | 15 | 16 | com.google.code.gson 17 | gson 18 | 2.8.2 19 | 20 | 21 | org.apache.pulsar 22 | pulsar-functions-api 23 | ${pulsar.version} 24 | provided 25 | 26 | 27 | -------------------------------------------------------------------------------- /rule-engine/common/src/main/java/io/emqx/pulsar/functions/windowing/SessionConfig.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.functions.windowing; 2 | 3 | import lombok.Data; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | import lombok.experimental.Accessors; 8 | 9 | @Data 10 | @Setter 11 | @Getter 12 | @Accessors(chain = true) 13 | @ToString 14 | public class SessionConfig { 15 | private Long windowLengthDurationMs; 16 | private Long timeoutlDurationMs; 17 | } 18 | -------------------------------------------------------------------------------- /rule-engine/common/src/main/java/io/emqx/stream/common/JsonParser.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common; 2 | 3 | import java.lang.reflect.Type; 4 | import java.util.Map; 5 | 6 | import com.google.gson.Gson; 7 | import com.google.gson.JsonSyntaxException; 8 | import com.google.gson.reflect.TypeToken; 9 | 10 | public class JsonParser { 11 | 12 | public static Map parseMqttMessage(String input) { 13 | Type type = new TypeToken>() {}.getType(); 14 | try { 15 | return new Gson().fromJson(input, type); 16 | } catch(JsonSyntaxException exp) { 17 | return null; 18 | } 19 | } 20 | 21 | public static Map parseRule(String input) { 22 | Type type = new TypeToken>() {}.getType(); 23 | try { 24 | return new Gson().fromJson(input, type); 25 | } catch(JsonSyntaxException exp) { 26 | return null; 27 | } 28 | } 29 | 30 | public static String toJson(Object resultMap) { 31 | return new Gson().toJson(resultMap); 32 | } 33 | 34 | @SuppressWarnings("unused") 35 | public static T fromJson(String json, Class classOfT) { 36 | return new Gson().fromJson(json, classOfT); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rule-engine/common/src/main/java/io/emqx/stream/common/internal/MockGenericRecord.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.internal; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import io.emqx.stream.common.JsonParser; 8 | import org.apache.pulsar.client.api.schema.Field; 9 | import org.apache.pulsar.client.api.schema.GenericRecord; 10 | 11 | public class MockGenericRecord implements GenericRecord { 12 | private final Map map; 13 | 14 | public MockGenericRecord(String json) { 15 | map = JsonParser.parseMqttMessage(json); 16 | } 17 | 18 | public MockGenericRecord(Map map) { 19 | this.map = map; 20 | } 21 | 22 | @Override 23 | public Object getField(String fieldName) { 24 | return map.get(fieldName); 25 | } 26 | 27 | @Override 28 | public List getFields() { 29 | List results = new LinkedList<>(); 30 | int i = 0; 31 | for(String key: map.keySet()) { 32 | results.add(new Field(key, i)); 33 | i++; 34 | } 35 | return results; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /rule-engine/db-sink/README.md: -------------------------------------------------------------------------------- 1 | # db-sink 2 | 3 | Run 4 | ```bash 5 | bin/pulsar-admin sink create \ 6 | --className io.emqx.pulsar.io.DatabaseSink \ 7 | --archive /opt/stream/db-sink-x.y.z.nar \ 8 | --tenant public \ 9 | --namespace default \ 10 | --name __sink_db \ 11 | --inputs __acaction_db \ 12 | --sink-config-file /opt/stream/db-sink-config.yml 13 | ``` -------------------------------------------------------------------------------- /rule-engine/db-sink/src/main/java/io/emqx/pulsar/io/DatabaseActionConfig.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.Data; 5 | import org.apache.pulsar.shade.com.fasterxml.jackson.annotation.JsonIgnoreProperties; 6 | 7 | import java.io.IOException; 8 | import java.util.Map; 9 | 10 | @Data 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | public class DatabaseActionConfig { 13 | 14 | private Map columns; 15 | 16 | public static DatabaseActionConfig load(Map map) throws IOException { 17 | ObjectMapper mapper = new ObjectMapper(); 18 | return mapper.readValue(new ObjectMapper().writeValueAsString(map), DatabaseActionConfig.class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rule-engine/db-sink/src/main/java/io/emqx/pulsar/io/jdbc/JdbcSinkConfig.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io.jdbc; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 5 | import lombok.*; 6 | import lombok.experimental.Accessors; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.Serializable; 11 | import java.util.Map; 12 | 13 | 14 | @Data 15 | @Setter 16 | @Getter 17 | @EqualsAndHashCode 18 | @ToString 19 | @Accessors(chain = true) 20 | public class JdbcSinkConfig implements Serializable { 21 | 22 | private static final long serialVersionUID = 1L; 23 | 24 | 25 | private String userName; 26 | 27 | private String password; 28 | 29 | private String jdbcUrl; 30 | 31 | private String tableName; 32 | 33 | private String[] columns; 34 | 35 | private int timeoutMs = 500; 36 | 37 | private int batchSize = 10; 38 | 39 | @SuppressWarnings("unused") 40 | public static JdbcSinkConfig load(String yamlFile) throws IOException { 41 | ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); 42 | return mapper.readValue(new File(yamlFile), JdbcSinkConfig.class); 43 | } 44 | 45 | public static JdbcSinkConfig load(Map map) throws IOException { 46 | ObjectMapper mapper = new ObjectMapper(); 47 | return mapper.readValue(new ObjectMapper().writeValueAsString(map), JdbcSinkConfig.class); 48 | } 49 | } -------------------------------------------------------------------------------- /rule-engine/db-sink/src/main/resources/META-INF/services/pulsar-io.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: db-sink 21 | description: Sink to save data to database 22 | sinkClass: io.emqx.pulsar.io.DatabaseSink -------------------------------------------------------------------------------- /rule-engine/emqx-source/README.md: -------------------------------------------------------------------------------- 1 | # Pulsar Source Connector for EMQX 2 | 3 | This is the source connector to subscribe to emqx and then feed into pulsar. 4 | 5 | ## How to Run 6 | 7 | Assume emqtt and pulsar are run at localhost. 8 | 0. Change the config for emqtt to not listen on localhost:8080, because 8080 is also used by pulsar 9 | 1. Run `mvn install -DskipTests`, make sure emqx-x.x.x-xxxxxx.nar is produced at /target 10 | 2. Open pulsar, run pulsar admin to add new source connector. Example: 11 | `sudo ./pulsar-admin source localrun --className EMQXSource -a ~/emqx-0.0.1-SNAPSHOT.nar --tenant public --namespace default --name emqx-source --destinationTopicName tttoxic` 12 | 3. Start a pulsar client to consume topic tttoxic or any topic specified at step 2 13 | 4. Open emqtt, try to publish message to topic /sample. Make sure pulsar client receives the topic. 14 | 15 | # Pulsar Sink Connector for EMQX 16 | 17 | ## How to run 18 | 19 | ```bash 20 | bin/pulsar-admin sink create \ 21 | --className io.emqx.pulsar.io.EMQXSink \ 22 | --archive /opt/stream/emqx-source-x.y.z.nar \ 23 | --tenant public \ 24 | --namespace default \ 25 | --name __sink_mqtt \ 26 | --inputs __acaction_mqtt \ 27 | --sink-config '{"brokerUrl":"tcp://127.0.0.1:11883"}' 28 | ``` -------------------------------------------------------------------------------- /rule-engine/emqx-source/src/main/java/io/emqx/pulsar/io/EMQXConfig.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | import java.io.Serializable; 4 | import java.util.Map; 5 | 6 | import com.google.gson.Gson; 7 | 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.Getter; 11 | import lombok.Setter; 12 | import lombok.ToString; 13 | import lombok.experimental.Accessors; 14 | 15 | @Data 16 | @Setter 17 | @Getter 18 | @EqualsAndHashCode 19 | @ToString 20 | @Accessors(chain = true) 21 | public class EMQXConfig implements Serializable { 22 | private static final long serialVersionUID = 1L; 23 | 24 | private String brokerUrl = "tcp://localhost:1883"; 25 | private String inputTopics; 26 | private String ruleId; 27 | private String userName; 28 | private String password; 29 | private String clientId; 30 | 31 | public static EMQXConfig load(Map map) { 32 | return new Gson().fromJson(new Gson().toJson(map), EMQXConfig.class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rule-engine/emqx-source/src/main/java/io/emqx/pulsar/io/EMQXSource.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | import io.emqx.stream.common.Constants; 4 | import lombok.Data; 5 | import org.apache.pulsar.functions.api.Record; 6 | 7 | import java.util.Optional; 8 | 9 | public class EMQXSource extends AbstractEMQXSource { 10 | @Override 11 | protected void doConsume(String topic, String message, long time) { 12 | consume(new StringRecord(Optional.of(randomString()), String.format("%s%s%s%s%d", topic, 13 | Constants.MESSAGE_SEPERATOR, message, Constants.MESSAGE_SEPERATOR, time)) ); 14 | } 15 | 16 | @Data 17 | static private class StringRecord implements Record { 18 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 19 | private final Optional key; 20 | private final String value; 21 | } 22 | } -------------------------------------------------------------------------------- /rule-engine/emqx-source/src/main/java/io/emqx/pulsar/io/SinkConfig.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | import java.io.Serializable; 4 | import java.util.Map; 5 | 6 | import com.google.gson.Gson; 7 | 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.Getter; 11 | import lombok.Setter; 12 | import lombok.ToString; 13 | import lombok.experimental.Accessors; 14 | 15 | @Data 16 | @Setter 17 | @Getter 18 | @EqualsAndHashCode 19 | @ToString 20 | @Accessors(chain = true) 21 | public class SinkConfig implements Serializable { 22 | private static final long serialVersionUID = 1L; 23 | 24 | private String brokerUrl = "tcp://localhost:1883"; 25 | private String outputTopic; 26 | private String ruleId; 27 | 28 | public static SinkConfig load(Map map) { 29 | return new Gson().fromJson(new Gson().toJson(map), SinkConfig.class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /rule-engine/emqx-source/src/main/resources/META-INF/services/pulsar-io.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: emqx-source 21 | description: EMQX source connector 22 | sourceClass: io.emqx.pulsar.io.EMQXSource 23 | sinkClass: io.emqx.pulsar.io.EMQXSink 24 | -------------------------------------------------------------------------------- /rule-engine/emqx-source/src/test/java/io/emqx/pulsar/io/tests/EMQXConfigTest.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io.tests; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import io.emqx.pulsar.io.EMQXConfig; 7 | import org.testng.Assert; 8 | import org.testng.annotations.DataProvider; 9 | import org.testng.annotations.Test; 10 | 11 | public class EMQXConfigTest { 12 | 13 | @DataProvider(name = "configs") 14 | public Object[][] createData() { 15 | Map correctConf = new HashMap<>(); 16 | correctConf.put("inputTopics", "sample/abc"); 17 | correctConf.put("ruleId", "rule1"); 18 | EMQXConfig emqxConf = new EMQXConfig(); 19 | emqxConf.setBrokerUrl("tcp://localhost:1883").setInputTopics("sample/abc").setRuleId("rule1"); 20 | 21 | Map redundantConf = new HashMap<>(); 22 | redundantConf.put("inputTopics", "sample/abc;sample/dce"); 23 | redundantConf.put("unrelated", "nothing"); 24 | redundantConf.put("ruleId", "rule2"); 25 | EMQXConfig emqxConf2 = new EMQXConfig(); 26 | emqxConf2.setBrokerUrl("tcp://localhost:1883").setInputTopics("sample/abc;sample/dce").setRuleId("rule2"); 27 | 28 | return new Object[][] { 29 | { correctConf, emqxConf }, 30 | { redundantConf, emqxConf2 } 31 | }; 32 | } 33 | 34 | @Test(dataProvider = "configs") 35 | public void testLoad(Map conf, EMQXConfig emqxConf) { 36 | Assert.assertEquals(EMQXConfig.load(conf), emqxConf); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rule-engine/emqx-source/src/test/java/io/emqx/pulsar/io/tests/EMQXTest.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io.tests; 2 | 3 | import io.emqx.pulsar.io.EMQXSource; 4 | import org.apache.pulsar.functions.api.Record; 5 | import org.testng.Assert; 6 | 7 | public class EMQXTest { 8 | 9 | @SuppressWarnings("unused") 10 | public void testOpen() throws Exception { 11 | EMQXSource source = new EMQXSource(); 12 | source.open(null, null); 13 | //TODO automatic this 14 | // Run emq client to publish message 15 | Record message = source.read(); 16 | while(message == null) { 17 | Thread.sleep(5000); 18 | message = source.read(); 19 | } 20 | Assert.assertEquals("new", message.getValue()); 21 | // Disconnect the client from the server 22 | source.close(); 23 | log("Disconnected"); 24 | } 25 | 26 | @SuppressWarnings("SameParameterValue") 27 | private void log(String message) { 28 | System.out.println(message); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rule-engine/mail-sink/README.md: -------------------------------------------------------------------------------- 1 | # mail-sink 2 | 3 | 4 | ```bash 5 | bin/pulsar-admin sink create \ 6 | --className io.emqx.pulsar.io.MailSink \ 7 | --archive /opt/stream/mail-sink-x.y.z.nar \ 8 | --tenant public \ 9 | --namespace default \ 10 | --name __sink_mail \ 11 | --inputs __acaction_mail \ 12 | --sink-config-file /opt/stream/mail-sink-config.yml 13 | ``` -------------------------------------------------------------------------------- /rule-engine/mail-sink/src/main/java/io/emqx/pulsar/io/MailActionConfig.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.apache.pulsar.shade.com.fasterxml.jackson.annotation.JsonIgnoreProperties; 7 | import org.apache.pulsar.shade.com.fasterxml.jackson.databind.ObjectMapper; 8 | 9 | import java.io.IOException; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | @Getter 14 | @Setter 15 | @JsonIgnoreProperties(ignoreUnknown = true) 16 | class MailActionConfig { 17 | 18 | private String title; 19 | private String content; 20 | private List emails; 21 | 22 | static MailActionConfig load(Map map) throws IOException { 23 | ObjectMapper mapper = new ObjectMapper(); 24 | return mapper.readValue(new ObjectMapper().writeValueAsString(map), MailActionConfig.class); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /rule-engine/mail-sink/src/main/java/io/emqx/pulsar/io/MailSinkConfig.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | import lombok.Getter; 4 | import org.apache.pulsar.shade.com.fasterxml.jackson.annotation.JsonIgnoreProperties; 5 | import org.apache.pulsar.shade.com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | import java.io.IOException; 8 | import java.util.Map; 9 | 10 | @Getter 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | class MailSinkConfig { 13 | 14 | private String host; 15 | private int port = 25; 16 | private String user; 17 | private String password; 18 | private String from; 19 | // SSL or STARTTLS 20 | private String encryptionType; 21 | private boolean debug; 22 | 23 | 24 | static MailSinkConfig load(Map map) throws IOException { 25 | ObjectMapper mapper = new ObjectMapper(); 26 | return mapper.readValue(new ObjectMapper().writeValueAsString(map), MailSinkConfig.class); 27 | } 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /rule-engine/mail-sink/src/main/resources/META-INF/services/pulsar-io.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: mail-sink 21 | description: Sink to send mail 22 | sinkClass: io.emqx.pulsar.io.MailSink -------------------------------------------------------------------------------- /rule-engine/publish-sink/README.md: -------------------------------------------------------------------------------- 1 | # publish-sink 2 | 3 | 4 | ```bash 5 | bin/pulsar-admin sink create \ 6 | --className io.emqx.pulsar.io.PublishSink \ 7 | --archive /opt/stream/publish-sink-x.y.z.nar \ 8 | --tenant public \ 9 | --namespace default \ 10 | --name __sink_publish \ 11 | --inputs __acaction_publish \ 12 | --sink-config-file /opt/stream/publish-sink-config.yml 13 | ``` -------------------------------------------------------------------------------- /rule-engine/publish-sink/src/main/java/io/emqx/pulsar/io/PublishSinkConfig.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | import lombok.Getter; 4 | import org.apache.pulsar.shade.com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | import java.io.IOException; 7 | import java.util.Map; 8 | 9 | 10 | @Getter 11 | public class PublishSinkConfig { 12 | 13 | private String url; 14 | private String username; 15 | private String password; 16 | 17 | public static PublishSinkConfig load(Map map) throws IOException { 18 | ObjectMapper mapper = new ObjectMapper(); 19 | return mapper.readValue(new ObjectMapper().writeValueAsString(map), PublishSinkConfig.class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rule-engine/publish-sink/src/main/resources/META-INF/services/pulsar-io.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: publish-sink 21 | description: Sink to publish message to EMQX 22 | sinkClass: io.emqx.pulsar.io.PublishSink -------------------------------------------------------------------------------- /rule-engine/rule-function/src/main/java/io/emqx/pulsar/functions/DummyWindowFunction.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.functions; 2 | 3 | import java.util.Collection; 4 | 5 | public class DummyWindowFunction implements java.util.function.Function, Void> { 6 | @Override 7 | public Void apply(Collection topicMessages) { 8 | return null; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /rule-engine/rule-function/src/main/java/io/emqx/pulsar/functions/RuleFunction.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.functions; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import org.apache.pulsar.client.api.schema.GenericRecord; 8 | import org.apache.pulsar.functions.api.Context; 9 | import org.apache.pulsar.functions.api.Function; 10 | 11 | public class RuleFunction extends AbstractRuleFunction implements Function { 12 | 13 | @Override 14 | public Void process(GenericRecord input, Context context) throws Exception { 15 | List> inputs = new ArrayList<>(); 16 | inputs.add(genericRecordToMap(input)); 17 | return super.process(inputs, context); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rule-engine/rule-function/src/main/java/io/emqx/pulsar/functions/StringRuleFunction.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.functions; 2 | 3 | import org.apache.pulsar.functions.api.Context; 4 | import org.apache.pulsar.functions.api.Function; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * Run SQL engine against the message in String format. 12 | * The message will contain both meta data and real payload 13 | * Example: {"tenantId": "mytenant", "productId": 123, "payload":"{\"temp\":23.4}"} 14 | */ 15 | public class StringRuleFunction extends AbstractRuleFunction implements Function { 16 | 17 | @Override 18 | public Void process(String topicMessage, Context context) throws Exception { 19 | List> inputs = new ArrayList<>(); 20 | inputs.add(topicMessageToMap(topicMessage, context.getLogger())); 21 | return super.process(inputs, context); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rule-engine/rule-function/src/main/java/io/emqx/pulsar/functions/StringWindowRuleFunction.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.functions; 2 | 3 | import org.apache.pulsar.functions.api.Context; 4 | import org.apache.pulsar.functions.api.Function; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class StringWindowRuleFunction extends AbstractRuleFunction implements Function, Void> { 12 | 13 | @Override 14 | public Void process(Collection topicMessages, Context context) throws Exception { 15 | List> inputs = new ArrayList<>(); 16 | for(String topicMessage : topicMessages) { 17 | inputs.add(topicMessageToMap(topicMessage, context.getLogger())); 18 | } 19 | return super.process(inputs, context); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rule-engine/rule-function/src/main/java/io/emqx/pulsar/functions/WindowRuleFunction.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.functions; 2 | 3 | import java.util.Collection; 4 | import java.util.stream.Collectors; 5 | 6 | import org.apache.pulsar.client.api.schema.GenericRecord; 7 | import org.apache.pulsar.functions.api.Context; 8 | import org.apache.pulsar.functions.api.Function; 9 | 10 | public class WindowRuleFunction extends AbstractRuleFunction implements Function, Void> { 11 | 12 | @Override 13 | public Void process(Collection inputs, Context context) throws Exception { 14 | return super.process(inputs.parallelStream().map(this::genericRecordToMap).collect(Collectors.toList()), context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rule-engine/rule-function/src/main/java/io/emqx/pulsar/functions/windowing/WindowAllowEmptyManager.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.functions.windowing; 2 | 3 | import org.apache.pulsar.functions.windowing.TriggerPolicy; 4 | import org.apache.pulsar.functions.windowing.WindowLifecycleListener; 5 | import org.apache.pulsar.functions.windowing.WindowManager; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | 10 | @SuppressWarnings("unchecked") 11 | public class WindowAllowEmptyManager extends WindowManager { 12 | public WindowAllowEmptyManager(WindowLifecycleListener lifecycleListener, Collection queue) { 13 | super(lifecycleListener, queue); 14 | } 15 | 16 | @Override 17 | public boolean onTrigger() { 18 | boolean nonEmptyEvents = super.onTrigger(); 19 | if(!nonEmptyEvents){ 20 | windowLifecycleListener.onActivation(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), 21 | evictionPolicy.getContext().getReferenceTime()); 22 | } 23 | return true; 24 | } 25 | 26 | public TriggerPolicy getTriggerPolicy(){ 27 | return triggerPolicy; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /rule-engine/sql/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.emqx.stream 6 | stream 7 | 0.5.3 8 | 9 | sql 10 | 11 | 12 | 13 | com.emqx.stream 14 | common 15 | ${project.parent.version} 16 | 17 | 18 | com.github.jsqlparser 19 | jsqlparser 20 | 0.9 21 | 22 | 23 | org.slf4j 24 | slf4j-api 25 | 1.7.25 26 | 27 | 28 | org.apache.pulsar 29 | pulsar-client-schema 30 | ${pulsar.version} 31 | provided 32 | 33 | 34 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/IRule.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | public interface IRule { 7 | /** 8 | * Run the rule against the new message. The input 9 | * is the current message.The state(window) needs to 10 | * be stored by the rule processor. 11 | */ 12 | List> apply(List> inputs) throws Exception; 13 | List> getActions(); 14 | } -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/Rule.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | 7 | import org.slf4j.Logger; 8 | 9 | import io.emqx.stream.common.sql.ISqlEngine; 10 | import io.emqx.stream.common.sql.SqlEngine; 11 | 12 | import lombok.Getter; 13 | import net.sf.jsqlparser.JSQLParserException; 14 | 15 | public class Rule implements IRule { 16 | private ISqlEngine engine; 17 | @Getter 18 | private List> actions; 19 | // --Commented out by Inspection (2019/4/10 0010 上午 10:30):private CompletableFuture processFuture; 20 | 21 | public Rule(String rawStr, Logger logger) throws JSQLParserException { 22 | this(Objects.requireNonNull(JsonParser.parseRule(rawStr)), logger); 23 | } 24 | 25 | @SuppressWarnings({"unchecked", "WeakerAccess"}) 26 | public Rule(Map payload, Logger logger) throws JSQLParserException { 27 | String sql = (String) payload.get("sql"); 28 | engine = new SqlEngine(sql, logger); 29 | actions = (List>) payload.get("actions"); 30 | } 31 | 32 | @Override 33 | public List> apply(List> messages) throws Exception { 34 | return engine.process(messages); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/ISqlEngine.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | public interface ISqlEngine { 7 | List> process(Map message) throws Exception; 8 | List> process(List> message) throws Exception; 9 | } -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/SqlFunction.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql; 2 | 3 | import io.emqx.stream.common.Constants; 4 | import lombok.Data; 5 | import lombok.experimental.Accessors; 6 | 7 | import java.util.List; 8 | 9 | @Accessors(fluent = true) @Data 10 | public class SqlFunction { 11 | private String name; 12 | private List stages; 13 | private List arguments; 14 | private Class returnType; 15 | private String description; 16 | } 17 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/Util.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql; 2 | 3 | import net.sf.jsqlparser.schema.Table; 4 | import net.sf.jsqlparser.statement.select.FromItem; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class Util { 12 | public static String getFromItemName(FromItem fromItem){ 13 | if(fromItem.getAlias() != null){ 14 | return fromItem.getAlias().getName(); 15 | }else if(fromItem instanceof Table){ 16 | return ((Table) fromItem).getName(); 17 | }else{ 18 | return fromItem.toString(); 19 | } 20 | } 21 | 22 | public static List> deepCopyMapList(List> source){ 23 | List> dest = new ArrayList<>(); 24 | source.forEach(ele -> { 25 | Map t = new HashMap<>(ele); 26 | dest.add(t); 27 | }); 28 | return dest; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/analyzer/FromItemAnalyzer.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.analyzer; 2 | 3 | import lombok.NonNull; 4 | import lombok.RequiredArgsConstructor; 5 | import net.sf.jsqlparser.schema.Table; 6 | import net.sf.jsqlparser.statement.select.*; 7 | 8 | import java.util.List; 9 | 10 | @RequiredArgsConstructor 11 | public class FromItemAnalyzer implements FromItemVisitor { 12 | @NonNull 13 | private final List fromItems; 14 | 15 | @Override 16 | public void visit(Table table) { 17 | fromItems.add(table); 18 | } 19 | 20 | @Override 21 | public void visit(SubSelect subSelect) { 22 | fromItems.add(subSelect); 23 | } 24 | 25 | @Override 26 | public void visit(SubJoin subJoin) { 27 | 28 | } 29 | 30 | @Override 31 | public void visit(LateralSubSelect lateralSubSelect) { 32 | //Only deal in join 33 | } 34 | 35 | @Override 36 | public void visit(ValuesList valuesList) { 37 | fromItems.add(valuesList); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/analyzer/RuleTablesNameFinder.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.analyzer; 2 | 3 | import net.sf.jsqlparser.statement.select.Join; 4 | import net.sf.jsqlparser.statement.select.PlainSelect; 5 | import net.sf.jsqlparser.util.TablesNamesFinder; 6 | 7 | public class RuleTablesNameFinder extends TablesNamesFinder { 8 | @Override 9 | public void visit(PlainSelect plainSelect) { 10 | if(plainSelect.getFromItem()!= null){ 11 | plainSelect.getFromItem().accept(this); 12 | } 13 | 14 | if (plainSelect.getJoins() != null) { 15 | for (Join join : plainSelect.getJoins()) { 16 | join.getRightItem().accept(this); 17 | } 18 | } 19 | if (plainSelect.getWhere() != null) { 20 | plainSelect.getWhere().accept(this); 21 | } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/plan/AggregatePlan.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.plan; 2 | 3 | import io.emqx.stream.common.sql.SqlExecutionContext; 4 | import io.emqx.stream.common.sql.pojo.Window; 5 | import io.emqx.stream.common.sql.visitor.Aggregator; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.experimental.Accessors; 9 | import net.sf.jsqlparser.expression.Expression; 10 | 11 | import java.util.List; 12 | 13 | @EqualsAndHashCode(callSuper = true) 14 | @Accessors(fluent = true) @Data 15 | public class AggregatePlan extends PlanAdaptor { 16 | private List expressions; 17 | private Window window; 18 | 19 | @Override 20 | public SqlExecutionContext execute(SqlExecutionContext context) throws Exception { 21 | context = super.execute(context); 22 | if(!expressions.isEmpty()){ 23 | Aggregator aggregator = new Aggregator(); 24 | context.groups(aggregator.grouping(expressions, context)); 25 | } 26 | return context; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/plan/FilterPlan.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.plan; 2 | 3 | import io.emqx.stream.common.sql.SqlExecutionContext; 4 | import io.emqx.stream.common.sql.visitor.ExpressionFilter; 5 | import lombok.Data; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.experimental.Accessors; 8 | import net.sf.jsqlparser.expression.Expression; 9 | import net.sf.jsqlparser.expression.operators.conditional.AndExpression; 10 | 11 | @EqualsAndHashCode(callSuper = true) 12 | @Accessors(fluent = true) @Data 13 | public class FilterPlan extends PlanAdaptor{ 14 | private Expression expression; 15 | private boolean isHaving; 16 | 17 | @Override 18 | public SqlExecutionContext execute(SqlExecutionContext context) throws Exception { 19 | context = super.execute(context); 20 | ExpressionFilter expFilter = new ExpressionFilter(context.logger()); 21 | //filter having 22 | if(isHaving){ 23 | if(context.groups() != null){ 24 | context.groups(expFilter.filterHaving(expression, context)); 25 | }else{ 26 | context.records(expFilter.filterHavingNoGroup(expression, context)); 27 | } 28 | }else{ 29 | if(context.joins() != null){ //filter multiple from 30 | context.joins(expFilter.filterWhere(expression, context)); 31 | }else{ //filter common where 32 | context.records(expFilter.filterWhere(expression, context)); 33 | } 34 | } 35 | return context; 36 | } 37 | 38 | public void addCondition(Expression expr){ 39 | if(expr != null){ 40 | if(expression == null){ 41 | expression = expr; 42 | }else{ 43 | expression = new AndExpression(expression, expr); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/plan/IPlan.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.plan; 2 | 3 | import io.emqx.stream.common.sql.SqlExecutionContext; 4 | 5 | public interface IPlan{ 6 | @SuppressWarnings("UnusedReturnValue") 7 | IPlan dependant(IPlan dependant); 8 | //Return a list of hashMap or a hashMap of list of hashMap 9 | SqlExecutionContext execute(SqlExecutionContext context) throws Exception; 10 | } -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/plan/PlanAdaptor.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.plan; 2 | 3 | import io.emqx.stream.common.sql.SqlExecutionContext; 4 | import lombok.Data; 5 | import lombok.experimental.Accessors; 6 | 7 | @Accessors(fluent = true) @Data 8 | public abstract class PlanAdaptor implements IPlan { 9 | protected IPlan dependant; 10 | @Override 11 | public SqlExecutionContext execute(SqlExecutionContext context) throws Exception { 12 | if(dependant != null){ 13 | context = dependant.execute(context); 14 | } 15 | return context; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/plan/SqlPlan.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.plan; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.experimental.Accessors; 6 | 7 | @EqualsAndHashCode(callSuper = true) 8 | @Accessors(fluent = true) 9 | @Data 10 | public class SqlPlan extends PlanAdaptor{ 11 | } -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/pojo/JoinCondition.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.pojo; 2 | 3 | import io.emqx.stream.common.Constants; 4 | import lombok.Data; 5 | import lombok.experimental.Accessors; 6 | import net.sf.jsqlparser.statement.select.SubSelect; 7 | 8 | import java.util.List; 9 | 10 | @Accessors(fluent = true) @Data 11 | public class JoinCondition { 12 | private Constants.JoinType type; 13 | private List> equivalents; 14 | private SubSelect lateral; 15 | } 16 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/pojo/Selection.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.pojo; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import lombok.experimental.Accessors; 6 | import net.sf.jsqlparser.expression.Expression; 7 | import net.sf.jsqlparser.schema.Table; 8 | 9 | @NoArgsConstructor 10 | @Accessors(fluent = true) @Data 11 | public class Selection { 12 | private boolean all; 13 | private int unnest; //0 is not, 1 is cross, 2 is outer 14 | private Table table; 15 | private Expression expression; 16 | private String outputName; 17 | } 18 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/pojo/Tuple.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.pojo; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.Accessors; 6 | 7 | @RequiredArgsConstructor 8 | @Accessors(fluent = true) @Data 9 | public class Tuple { 10 | private final X left; 11 | private final Y right; 12 | } 13 | -------------------------------------------------------------------------------- /rule-engine/sql/src/main/java/io/emqx/stream/common/sql/pojo/Window.java: -------------------------------------------------------------------------------- 1 | package io.emqx.stream.common.sql.pojo; 2 | 3 | import io.emqx.stream.common.Constants.WindowType; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | @NoArgsConstructor 9 | @Accessors(fluent = true) @Data 10 | public class Window { 11 | private WindowType type; 12 | private boolean isDuration; 13 | private long size; 14 | //Set to 0 if no slide window 15 | private long hopSize; 16 | } 17 | -------------------------------------------------------------------------------- /rule-engine/topic-distribute-function/src/main/java/io/emqx/pulsar/functions/TopicDistributeFunction.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.functions; 2 | 3 | import io.emqx.stream.common.Constants; 4 | import org.apache.pulsar.functions.api.Context; 5 | import org.apache.pulsar.functions.api.Function; 6 | 7 | /** 8 | * Predefined function for redistributing EMQX messages into multiple pulsar topics 9 | * The input topic must be the output topic of the AC EMQX source 10 | * 11 | * This function just redistribute the emqx topic as a pulsar topic. 12 | */ 13 | public class TopicDistributeFunction implements Function { 14 | @Override 15 | public Void process(String message, Context context) { 16 | String[] messages = message.split(Constants.MESSAGE_SEPERATOR); 17 | if(messages.length == 3){ 18 | String topic = messages[0].replaceAll("/", "%%"); 19 | context.getLogger().info("Publish to topic {}", topic); 20 | context.publish(topic, message); 21 | }else{ 22 | context.getLogger().error("Invalid input message {}", message); 23 | } 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rule-engine/topic-distribute-function/src/test/java/io/emqx/pulsar/functions/testDistributeFunction.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.functions; 2 | 3 | import io.emqx.stream.common.internal.MockContext; 4 | import org.testng.Assert; 5 | import org.testng.annotations.Test; 6 | 7 | public class testDistributeFunction { 8 | @Test 9 | public void testDistribute(){ 10 | MockContext context = new MockContext(); 11 | TopicDistributeFunction function = new TopicDistributeFunction(); 12 | function.process("up/tenant1/product1/device2/topic1%;{\"temperature\":27,\"ts\":1541152486032}", context); 13 | Assert.assertEquals(context.getResultTopic(), "up%%tenant1%%product1%%device2%%topic1"); 14 | Assert.assertEquals(context.getResult(), "{\"temperature\":27,\"ts\":1541152486032}"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rule-engine/webhook-sink/README.md: -------------------------------------------------------------------------------- 1 | # webhook-sink 2 | 3 | 4 | ```bash 5 | bin/pulsar-admin sink create \ 6 | --className io.emqx.pulsar.io.WebhookSink \ 7 | --archive /opt/stream/webhook-sink-x.y.z.nar \ 8 | --tenant public \ 9 | --namespace default \ 10 | --name __sink_webhook \ 11 | --inputs __acaction_webhook \ 12 | ``` -------------------------------------------------------------------------------- /rule-engine/webhook-sink/src/main/java/io/emqx/pulsar/io/WebhookActionConfig.java: -------------------------------------------------------------------------------- 1 | package io.emqx.pulsar.io; 2 | 3 | import lombok.Data; 4 | import org.apache.pulsar.shade.com.fasterxml.jackson.annotation.JsonIgnoreProperties; 5 | import org.apache.pulsar.shade.com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | import java.io.IOException; 8 | import java.util.Map; 9 | 10 | @Data 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | public class WebhookActionConfig { 13 | 14 | private String url; 15 | 16 | private String token; 17 | 18 | public static WebhookActionConfig load(Map map) throws IOException { 19 | ObjectMapper mapper = new ObjectMapper(); 20 | return mapper.readValue(new ObjectMapper().writeValueAsString(map), WebhookActionConfig.class); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /rule-engine/webhook-sink/src/main/resources/META-INF/services/pulsar-io.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: webhook-sink 21 | description: Sink to send message to webhook 22 | sinkClass: io.emqx.pulsar.io.WebhookSink -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=. 2 | FLASK_SKIP_DOTENV=1 3 | FLASK_APP=manage 4 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | .pytest_cache 4 | .dockerignore 5 | actor_data 6 | private_services 7 | deploy/production/codec/_temp/ 8 | instance/* 9 | !instance/certs/ 10 | __pycache__/ 11 | -------------------------------------------------------------------------------- /server/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | aiohttp = "==3.5.4" 10 | arrow = "==0.14.1" 11 | asyncpg = "==0.18.3" 12 | bcrypt = "==3.1.6" 13 | croniter = "==0.3.30" 14 | flask = "==1.0.3" 15 | werkzeug = "==0.16.1" 16 | flask-cors = "==3.0.7" 17 | flask-httpauth = "==3.3.0" 18 | flask-mail = "==0.9.1" 19 | flask-migrate = "==2.5.2" 20 | flask-sqlalchemy = "==2.4.0" 21 | flask-uploads = "==0.2.1" 22 | gunicorn = "==19.9.0" 23 | immutables = "==0.9" 24 | itsdangerous = "==1.1.0" 25 | lxml = "==4.3.3" 26 | marshmallow = "==2.19.2" 27 | mode = "==4.0.0" 28 | numpy = "==1.16.4" 29 | pandas = "==0.24.2" 30 | paramiko = "==2.4.2" 31 | psycopg2-binary = "==2.8.2" 32 | pyopenssl = "==19.0.0" 33 | pytest = "==4.6.2" 34 | python-multipart = "==0.0.5" 35 | pytz = "==2019.1" 36 | pyyaml = "==5.1" 37 | requests = "==2.22.0" 38 | sqlalchemy = "==1.3.4" 39 | starlette = "==0.12.0" 40 | uvicorn = "==0.7.1" 41 | uvloop = "==0.12.2" 42 | xlrd = "==1.2.0" 43 | xlsxwriter = "==1.1.8" 44 | sqlparse = "==0.3.0" 45 | 46 | [requires] 47 | python_version = "3.6" 48 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # ActorCloud Server 2 | 3 | ### RUN 4 | * backend: python run.py backend 5 | * async-tasks: python run.py async-tasks 6 | * timer-tasks: python run.py timer-tasks -------------------------------------------------------------------------------- /server/actor_libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/actor_libs/__init__.py -------------------------------------------------------------------------------- /server/actor_libs/cache/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Cache 2 | 3 | 4 | __all__ = ['Cache'] 5 | -------------------------------------------------------------------------------- /server/actor_libs/cache/_dict_code.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from actor_libs.types import DictCodeCache 4 | 5 | 6 | __all__ = ['cache_dict_code'] 7 | 8 | 9 | def cache_dict_code() -> DictCodeCache: 10 | from app.models import DictCode 11 | 12 | record = defaultdict(dict) 13 | dict_code_values = DictCode.query.all() 14 | 15 | for dict_code in dict_code_values: 16 | int_value = dict_code.codeValue 17 | code_value = int_value if int_value is not None else dict_code.codeStringValue 18 | record[dict_code.code][code_value] = { 19 | 'enLabel': dict_code.enLabel, 20 | 'zhLabel': dict_code.zhLabel 21 | } 22 | return record 23 | -------------------------------------------------------------------------------- /server/actor_libs/cache/base.py: -------------------------------------------------------------------------------- 1 | from actor_libs.types import DictCodeCache 2 | from ._dict_code import cache_dict_code 3 | 4 | 5 | class Cache: 6 | _instance = None 7 | _dict_code_cache: DictCodeCache = {} 8 | models_schema_cache = {} 9 | 10 | def __new__(cls, *args, **kwargs): 11 | if not cls._instance: 12 | cls._instance = super(Cache, cls).__new__(cls, *args, **kwargs) 13 | return cls._instance 14 | 15 | @property 16 | def dict_code(self): 17 | if not self._dict_code_cache: 18 | self._dict_code_cache = cache_dict_code() 19 | return self._dict_code_cache 20 | -------------------------------------------------------------------------------- /server/actor_libs/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/actor_libs/database/__init__.py -------------------------------------------------------------------------------- /server/actor_libs/database/async_db/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import AsyncPostgres 2 | 3 | 4 | __all__ = ['db'] 5 | 6 | db = AsyncPostgres() 7 | -------------------------------------------------------------------------------- /server/actor_libs/database/orm/__init__.py: -------------------------------------------------------------------------------- 1 | from ._model import db, BaseModel, ModelMixin 2 | 3 | 4 | __all__ = ['db', 'BaseModel', 'ModelMixin'] 5 | -------------------------------------------------------------------------------- /server/actor_libs/database/orm/_model.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from flask_sqlalchemy import SQLAlchemy 4 | 5 | from ._query import ExtendQuery 6 | from .utils import dumps_query_result, get_model_schema 7 | 8 | 9 | db = SQLAlchemy(query_class=ExtendQuery) 10 | 11 | 12 | class ModelMixin: 13 | 14 | def to_dict(self, **kwargs): 15 | result = self 16 | return dumps_query_result(result, **kwargs) 17 | 18 | def create(self, request_dict=None, commit=True): 19 | if request_dict: 20 | for key, value in request_dict.items(): 21 | if hasattr(self, key): 22 | setattr(self, key, value) 23 | db.session.add(self) 24 | if commit: 25 | db.session.commit() 26 | else: 27 | db.session.flush() 28 | return self 29 | 30 | def update(self, request_dict=None, commit=True): 31 | if request_dict: 32 | for key, value in request_dict.items(): 33 | if hasattr(self, key): 34 | setattr(self, key, value) 35 | if commit: 36 | db.session.commit() 37 | return self 38 | 39 | def delete(self, commit=False): 40 | db.session.delete(self) 41 | if commit: 42 | db.session.commit() 43 | 44 | 45 | class BaseModel(ModelMixin, db.Model): 46 | __abstract__ = True 47 | id = db.Column(db.Integer, primary_key=True) 48 | createAt = db.Column(db.DateTime, nullable=True, default=datetime.now) 49 | updateAt = db.Column(db.DateTime, nullable=True, onupdate=datetime.now) 50 | -------------------------------------------------------------------------------- /server/actor_libs/database/sql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/actor_libs/database/sql/__init__.py -------------------------------------------------------------------------------- /server/actor_libs/emqx/publish/protocol/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import base_publish_json 2 | from .lwm2m import lwm2m_publish_json 3 | 4 | 5 | PROTOCOL_PUBLISH_JSON_FUNC = { 6 | 'mqtt': base_publish_json, 7 | 'coap': base_publish_json, 8 | 'lwm2m': lwm2m_publish_json, 9 | 'websocket': base_publish_json 10 | } 11 | -------------------------------------------------------------------------------- /server/actor_libs/emqx/publish/protocol/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | __all__ = ['base_publish_json'] 4 | 5 | 6 | def base_publish_json(request_dict): 7 | """ 8 | Building client publish json of base protocol 9 | base protocol: MQTT(1), CoAP(2), WebSocket(6) 10 | """ 11 | 12 | # build publish payload 13 | publish_payload = { 14 | 'data_type': 'request', 15 | 'task_id': request_dict['taskID'], 16 | 'data': request_dict['payload'] 17 | } 18 | if request_dict.get('streamID'): 19 | publish_payload['stream_id'] = request_dict['streamID'] 20 | publish_json = { 21 | 'qos': 1, 22 | 'topic': request_dict['prefixTopic'] + request_dict['topic'], 23 | 'payload': json.dumps(publish_payload) 24 | } 25 | return publish_json 26 | -------------------------------------------------------------------------------- /server/actor_libs/emqx/publish/protocol/lwm2m.py: -------------------------------------------------------------------------------- 1 | import json 2 | from base64 import b64encode 3 | 4 | 5 | __all__ = ['lwm2m_publish_json'] 6 | 7 | 8 | def lwm2m_publish_json(request_dict): 9 | """ Building device publish json of lwm2m protocol """ 10 | 11 | payload = request_dict['payload'] 12 | msg_type = payload.pop('msgType') 13 | if payload['path'] == '/19/1/0': 14 | payload['value'] = json.dumps(payload['value']) 15 | if msg_type == 'write': 16 | # lwm2m write msg type require base64 encode 17 | payload['value'] = b64encode(payload['value'].encode()).decode() 18 | publish_payload = { 19 | 'reqID': request_dict['taskID'], 20 | 'msgType': msg_type, 21 | 'data': payload 22 | } 23 | if request_dict.get('streamID'): 24 | publish_payload['stream_id'] = request_dict['streamID'] 25 | publish_json = { 26 | 'qos': 1, 27 | 'topic': request_dict['prefixTopic'] + 'dn', 28 | 'payload': json.dumps(publish_payload) 29 | } 30 | return publish_json 31 | -------------------------------------------------------------------------------- /server/actor_libs/http_tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .async_http import AsyncHttp 2 | from .sync_http import SyncHttp 3 | 4 | 5 | __all__ = ['AsyncHttp', 'SyncHttp'] 6 | -------------------------------------------------------------------------------- /server/actor_libs/http_tools/responses/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import ActorResponse 2 | from .emqx import handle_emqx_publish_response, handle_emqx_rule_response 3 | from .task_scheduler import handle_task_scheduler_response 4 | 5 | __all__ = [ 6 | 'ActorResponse', 'handle_emqx_publish_response', 7 | 'handle_emqx_rule_response', 'handle_task_scheduler_response' 8 | ] 9 | -------------------------------------------------------------------------------- /server/actor_libs/http_tools/responses/base.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from typing import Dict 3 | 4 | 5 | __all__ = ['ActorResponse', 'handle_base_response'] 6 | 7 | ActorResponse = namedtuple( 8 | 'ActorResponse', 9 | [ 10 | 'taskID', 11 | 'taskStatus', 12 | 'responseCode', 13 | 'responseContent' 14 | ] 15 | ) 16 | 17 | 18 | def handle_base_response(response) -> Dict: 19 | """ 20 | Handle base response 21 | status: 1:waiting, 2:pending, 3:success, 4:fail 22 | """ 23 | 24 | handled_response = {'status': 2} 25 | if response.responseCode == 200: 26 | handled_response['status'] = 3 27 | elif response.responseCode == 422: 28 | handled_response['status'] = 4 29 | handled_response['error'] = 'FORM_INVALID' 30 | elif response.responseCode == 401: 31 | # Auth failed 32 | handled_response['status'] = 4 33 | handled_response['error'] = 'AUTH_FAILED' 34 | elif response.responseCode == 500: 35 | # Timeout or emqx internal error 36 | handled_response['status'] = 4 37 | handled_response['error'] = response.responseContent 38 | else: 39 | # Other unknown error 40 | handled_response['status'] = 4 41 | handled_response['error'] = 'UNKNOWN_ERROR' 42 | return handled_response 43 | -------------------------------------------------------------------------------- /server/actor_libs/http_tools/responses/emqx.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import json 4 | from .base import handle_base_response, ActorResponse 5 | 6 | 7 | __all__ = ['handle_emqx_publish_response', 'handle_emqx_rule_response'] 8 | 9 | 10 | def handle_emqx_publish_response(response: ActorResponse) -> Dict: 11 | """ 12 | Handle emqx publish response 13 | """ 14 | 15 | handled_response = handle_base_response(response) 16 | if handled_response.get('status') == 4: 17 | return handled_response 18 | 19 | try: 20 | response_dict = json.loads(response.responseContent) 21 | except Exception: 22 | response_dict = {'message': response.responseContent} 23 | 24 | handled_response['taskID'] = response.taskID 25 | code = response_dict.get('code') 26 | if code != 0 and response_dict.get('message'): 27 | # Error from emqx 28 | handled_response['status'] = 4 29 | handled_response['error'] = response_dict['message'] 30 | elif code != 0 and not response_dict.get('message'): 31 | # Other unknown situation 32 | handled_response['status'] = 4 33 | handled_response['error'] = response.responseContent 34 | return handled_response 35 | 36 | 37 | def handle_emqx_rule_response(response: ActorResponse) -> Dict: 38 | """ Handle emqx message rule response """ 39 | 40 | handled_response = handle_base_response(response) 41 | if handled_response.get('status') == 4: 42 | return handled_response 43 | handled_response['message'] = response.responseContent 44 | return handled_response 45 | -------------------------------------------------------------------------------- /server/actor_libs/http_tools/responses/task_scheduler.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import json 4 | from .base import handle_base_response 5 | 6 | 7 | def handle_task_scheduler_response(response) -> Dict: 8 | """ 9 | Handle task scheduler response,add taskID to response 10 | """ 11 | 12 | handled_response = handle_base_response(response) 13 | try: 14 | response_dict = json.loads(response.responseContent) 15 | except Exception: 16 | response_dict = {} 17 | 18 | if handled_response.get('status') == 3 and response_dict.get('taskID'): 19 | handled_response['status'] = 3 20 | handled_response['taskID'] = response_dict['taskID'] 21 | else: 22 | handled_response['status'] = 4 23 | return handled_response 24 | -------------------------------------------------------------------------------- /server/actor_libs/logs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def create_logger(name, log_level='ERROR'): 5 | log_level = log_level.upper() 6 | logger = logging.getLogger(name) 7 | handler = logging.StreamHandler() 8 | formatter = logging.Formatter( 9 | '[%(asctime)s -- %(name)s -- %(levelname)s] %(message)s', 10 | datefmt='%Y-%m-%d %H:%M:%S' 11 | ) 12 | handler.setFormatter(formatter) 13 | logger.addHandler(handler) 14 | logger.setLevel(getattr(logging, log_level)) 15 | return logger 16 | 17 | -------------------------------------------------------------------------------- /server/actor_libs/manage/__init__.py: -------------------------------------------------------------------------------- 1 | from .data_init import ( 2 | db_operate, convert_timescaledb, init_services, init_resources, 3 | init_default_roles, update_default_roles, init_admin_account, 4 | init_dict_code, init_system_info, init_lwm2m_info, create_triggers 5 | ) 6 | from .supervisord import supervisord_config 7 | 8 | 9 | __all__ = ['ProjectManage'] 10 | 11 | 12 | class ProjectManage: 13 | 14 | @staticmethod 15 | def project_deploy(): 16 | db_operate(execute_type='deploy') 17 | convert_timescaledb() 18 | create_triggers() 19 | init_services() 20 | init_resources() 21 | init_default_roles() 22 | init_admin_account() 23 | init_dict_code() 24 | init_system_info() 25 | init_lwm2m_info() 26 | supervisord_config() 27 | 28 | @staticmethod 29 | def project_upgrade(): 30 | db_operate(execute_type='upgrade') 31 | init_services() 32 | init_resources() 33 | update_default_roles() 34 | init_dict_code() 35 | init_system_info() 36 | init_lwm2m_info() 37 | supervisord_config() 38 | -------------------------------------------------------------------------------- /server/actor_libs/manage/data_init/__init__.py: -------------------------------------------------------------------------------- 1 | from .db_base import db_operate 2 | from .default_roles import init_default_roles, update_default_roles 3 | from .table_init import ( 4 | convert_timescaledb, create_triggers, init_services, init_resources, 5 | init_admin_account, init_dict_code, init_system_info, init_lwm2m_info 6 | ) 7 | 8 | 9 | __all__ = [ 10 | 'db_operate', 'convert_timescaledb', 'create_triggers', 11 | 'init_resources', 'init_services', 'init_default_roles', 12 | 'update_default_roles', 'init_admin_account', 13 | 'init_dict_code', 'init_system_info', 'init_lwm2m_info' 14 | ] 15 | -------------------------------------------------------------------------------- /server/actor_libs/manage/supervisord/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import supervisord_config 2 | 3 | __all__ = ['supervisord_config'] 4 | -------------------------------------------------------------------------------- /server/actor_libs/manage/supervisord/_gunicorn.py: -------------------------------------------------------------------------------- 1 | import os 2 | from multiprocessing import cpu_count 3 | from typing import AnyStr 4 | 5 | from config import BaseConfig 6 | from ._templates import insert_jinja_template 7 | 8 | 9 | __all__ = ['gunicorn_config'] 10 | 11 | 12 | def gunicorn_config() -> AnyStr: 13 | """ Generate configuration information of gunicorn """ 14 | 15 | project_config = BaseConfig().config 16 | project_path = project_config['PROJECT_PATH'] 17 | gunicorn_config_path = os.path.join( 18 | project_path, 'config/gunicorn.py' 19 | ) 20 | get_cpu_count = cpu_count() 21 | if get_cpu_count > 8: 22 | worker_cpu = 9 23 | elif get_cpu_count <= 2: 24 | worker_cpu = 3 25 | else: 26 | worker_cpu = get_cpu_count * 2 + 1 27 | 28 | backend_node = project_config['BACKEND_NODE'] 29 | port = backend_node.split(':')[-1] 30 | jinja_config = { 31 | 'host': f"0.0.0.0:{port}", 32 | 'worker': worker_cpu, 33 | 'loglevel': project_config['LOG_LEVEL'] 34 | } 35 | insert_jinja_template( 36 | project_path=project_path, 37 | out_path=gunicorn_config_path, 38 | template_name='gunicorn.jinja', 39 | jinja_config=jinja_config 40 | ) 41 | return gunicorn_config_path 42 | -------------------------------------------------------------------------------- /server/actor_libs/manage/supervisord/_templates/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import AnyStr, Dict 3 | 4 | import jinja2 5 | 6 | 7 | def insert_jinja_template( 8 | project_path: AnyStr, out_path: AnyStr, 9 | template_name: AnyStr, jinja_config: Dict) -> None: 10 | """ Based on the jinja template generate configuration """ 11 | 12 | templates_path = os.path.join( 13 | project_path, 'actor_libs/manage/supervisord/_templates' 14 | ) 15 | env = jinja2.Environment( 16 | 17 | loader=jinja2.FileSystemLoader(searchpath=templates_path, encoding='utf-8'), 18 | lstrip_blocks=True) 19 | template = env.get_template(template_name) 20 | result = template.render(jinja_config) 21 | with open(out_path, 'w+') as f: 22 | f.write(result) 23 | -------------------------------------------------------------------------------- /server/actor_libs/manage/supervisord/_templates/gunicorn.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | All the settings are mentioned in the 3 | settings(http://docs.gunicorn.org/en/latest/settings.html#settings) list 4 | """ 5 | 6 | bind = '{{ host }}' 7 | workers = {{ worker }} 8 | loglevel = '{{ loglevel }}' 9 | timeout = 300 10 | backlog = 1024 11 | max_requests = 30000 12 | max_requests_jitter = 2 13 | limit_request_line = 1024 14 | limit_request_fields = 256 15 | limit_request_field_size = 1024 16 | -------------------------------------------------------------------------------- /server/actor_libs/manage/supervisord/_templates/supervisor.jinja: -------------------------------------------------------------------------------- 1 | [group:actorcloud] 2 | programs={{ group_programs }} 3 | priority=999 4 | {% for server in services %} 5 | 6 | [program:{{ server.name }}] 7 | directory={{ server.directory }} 8 | command={{ server.command }} 9 | {%- if server.environment %} 10 | environment=PYTHONPATH={{ server.environment }} 11 | {%- endif %} 12 | user={{ server.user }} 13 | numprocs=1 14 | autostart=true 15 | autorestart=true 16 | stopasgroup=true 17 | killasgroup=true 18 | startsecs=20 19 | redirect_stderr=true 20 | stdout_logfile={{ server.log }} 21 | stdout_logfile_maxbytes=50MB 22 | stdout_logfile_backups=5 23 | stopwaitsecs = 30 24 | {% endfor %} 25 | -------------------------------------------------------------------------------- /server/actor_libs/send_mails.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | 3 | from flask_mail import Message 4 | 5 | from app import app, mail 6 | 7 | 8 | def send_async_email(msg): 9 | with app.app_context(): 10 | mail.send(msg) 11 | 12 | 13 | def send_text(email, title, content): 14 | msg = Message(title, recipients=[email], charset='utf-8') 15 | msg.body = content 16 | thr = Thread(target=send_async_email, args=[msg]) 17 | thr.start() 18 | 19 | 20 | def send_html(email, title, content): 21 | msg = Message(title, recipients=[email], charset='utf-8') 22 | msg.html = content 23 | thr = Thread(target=send_async_email, args=[msg]) 24 | thr.start() 25 | -------------------------------------------------------------------------------- /server/actor_libs/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/actor_libs/tasks/__init__.py -------------------------------------------------------------------------------- /server/actor_libs/tasks/_sql_statement.py: -------------------------------------------------------------------------------- 1 | insert_task_sql = """ 2 | INSERT INTO actor_tasks 3 | ("createAt", "taskID", "taskStatus", "taskName", "taskInfo") 4 | VALUES('{createAt}', '{taskID}', {taskStatus}, '{taskName}', '{taskInfo}') 5 | """ 6 | 7 | update_task_sql = """ 8 | UPDATE "actor_tasks" 9 | SET "updateAt"='{updateAt}', 10 | "taskStatus"={taskStatus}, 11 | "taskProgress"={taskProgress}, 12 | "taskResult"='{taskResult}' 13 | WHERE "taskID"='{taskID}' 14 | """ 15 | -------------------------------------------------------------------------------- /server/actor_libs/tasks/exceptions.py: -------------------------------------------------------------------------------- 1 | from actor_libs.errors import ExceptionT 2 | 3 | 4 | class TaskException(ExceptionT): 5 | 6 | def __init__(self, code, *, error_code=None, message=None): 7 | self.code = code 8 | self.error_code = error_code if error_code else 'TASK_ERROR' 9 | self.message = message if message else 'task error' 10 | 11 | def __call__(self, *args, **kwargs): 12 | return f"{self.error_code}: {self.message}" 13 | -------------------------------------------------------------------------------- /server/actor_libs/tasks/utils.py: -------------------------------------------------------------------------------- 1 | """Crontab Utilities.""" 2 | from datetime import datetime, tzinfo 3 | import time 4 | 5 | from croniter.croniter import croniter 6 | 7 | 8 | def secs_for_next(cron_format: str, tz: tzinfo = None) -> float: 9 | """Return seconds until next execution given crontab style format.""" 10 | now_ts = time.time() 11 | # If we have a tz object we'll make now timezone aware, and 12 | # if not will set now to be the current timestamp (tz 13 | # unaware) 14 | # If we have tz, now will be a datetime, if not an integer 15 | now = datetime.now(tz) if tz else now_ts 16 | cron_it = croniter(cron_format, start_time=now) 17 | return cron_it.get_next(float) - now_ts 18 | -------------------------------------------------------------------------------- /server/actor_libs/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import EventLoop, JSONDecoder, JSONEncoder, StrOrURL, DictCodeCache 2 | from .task import TaskRegistry, TaskResult 3 | 4 | 5 | __all__ = [ 6 | # types.base 7 | 'EventLoop', 8 | 'JSONEncoder', 9 | 'JSONDecoder', 10 | 'StrOrURL', 11 | 'DictCodeCache', 12 | 13 | # types.task 14 | 'TaskResult', 15 | 'TaskRegistry' 16 | ] 17 | -------------------------------------------------------------------------------- /server/actor_libs/types/base.py: -------------------------------------------------------------------------------- 1 | from asyncio import AbstractEventLoop 2 | from typing import Any, Callable, Union, Dict 3 | 4 | from yarl import URL 5 | 6 | 7 | __all__ = ['EventLoop', 'JSONDecoder', 'JSONEncoder', 'StrOrURL', 'DictCodeCache'] 8 | 9 | EventLoop = AbstractEventLoop 10 | JSONEncoder = Callable[[Any], str] 11 | JSONDecoder = Callable[[str], Any] 12 | StrOrURL = Union[str, URL] 13 | DictCodeCache = Dict 14 | -------------------------------------------------------------------------------- /server/actor_libs/types/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class ExceptionT(Exception): 3 | code: int 4 | error_code: str 5 | message: str 6 | -------------------------------------------------------------------------------- /server/actor_libs/types/orm.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import BaseQuery 2 | from flask_sqlalchemy.model import DefaultMeta 3 | 4 | __all__ = ['BaseQueryT', 'BaseModelT'] 5 | 6 | 7 | class BaseQueryT(BaseQuery): 8 | ... 9 | 10 | 11 | class BaseModelT(DefaultMeta): 12 | ... 13 | 14 | 15 | -------------------------------------------------------------------------------- /server/actor_libs/types/task.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | MutableMapping, Callable, Union, Awaitable, Dict, Any 3 | ) 4 | 5 | 6 | __all__ = ['FaustApp', 'TaskRegistry', 'TaskResult'] 7 | 8 | TaskRegistry = MutableMapping[str, Callable[..., Awaitable]] 9 | TaskResult = Union[Dict[str, Union[Union[int, str, dict], Any]], Dict[str, Union[dict, Any]]] 10 | -------------------------------------------------------------------------------- /server/app/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from importlib import import_module 4 | 5 | from actor_libs.utils import get_services_path 6 | 7 | 8 | def import_models(): 9 | active_services = get_services_path() 10 | for key, value in active_services.items(): 11 | schemas_path = os.path.join(value, 'models.py') 12 | if not os.path.exists(schemas_path): 13 | continue 14 | service_path = '.'.join(value.partition('app')[-1].split('/')) 15 | service_models_path = 'app{0}.models'.format(service_path) 16 | models_module = import_module(service_models_path) 17 | service_models = models_module.__all__ if hasattr(models_module, '__all__') else [] 18 | for name, attr in models_module.__dict__.items(): 19 | if name in service_models: 20 | setattr(sys.modules[__name__], name, attr) 21 | 22 | 23 | import_models() 24 | -------------------------------------------------------------------------------- /server/app/schemas.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from importlib import import_module 4 | from actor_libs.utils import get_services_path 5 | 6 | 7 | def import_schemas(): 8 | active_services = get_services_path() 9 | for key, value in active_services.items(): 10 | schemas_path = os.path.join(value, 'schemas.py') 11 | if not os.path.exists(schemas_path): 12 | continue 13 | service_path = '.'.join(value.partition('app')[-1].split('/')) 14 | service_schemas_path = 'app{0}.schemas'.format(service_path) 15 | schemas_module = import_module(service_schemas_path) 16 | service_schemas = schemas_module.__all__ if hasattr(schemas_module, '__all__') else [] 17 | for name, attr in schemas_module.__dict__.items(): 18 | if name in service_schemas: 19 | setattr(sys.modules[__name__], name, attr) 20 | 21 | 22 | import_schemas() 23 | -------------------------------------------------------------------------------- /server/app/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/__init__.py -------------------------------------------------------------------------------- /server/app/services/alerts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/alerts/__init__.py -------------------------------------------------------------------------------- /server/app/services/alerts/views/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | 4 | bp = Blueprint('alerts', __name__) 5 | 6 | from . import alerts # noqa: E402 7 | 8 | 9 | __all__ = ['bp', 'alerts'] 10 | -------------------------------------------------------------------------------- /server/app/services/applications/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/applications/__init__.py -------------------------------------------------------------------------------- /server/app/services/applications/views/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | 4 | bp = Blueprint('applications', __name__) 5 | 6 | from . import applications # noqa: E402 7 | 8 | 9 | __all__ = ['applications'] 10 | -------------------------------------------------------------------------------- /server/app/services/base/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/app/services/base/views/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | 4 | bp = Blueprint('base', __name__) 5 | 6 | from . import users # noqa: E402 7 | from . import auth # noqa: E402 8 | from . import select_options # noqa: E402 9 | from . import logs # noqa: E402 10 | from . import messages # noqa: E402 11 | from . import roles # noqa: E402 12 | from . import system # noqa: E402 13 | from . import tenants # noqa: E402 14 | from . import tasks # noqa: E402 15 | from . import file_system # noqa: E402 16 | from . import overview # noqa: E402 17 | 18 | 19 | __all__ = [ 20 | 'bp', 'auth', 'select_options', 'logs', 'messages', 21 | 'roles', 'system', 'tenants', 'users', 'tasks', 'overview' 22 | ] 23 | -------------------------------------------------------------------------------- /server/app/services/base/views/logs.py: -------------------------------------------------------------------------------- 1 | from flask import g, jsonify, request 2 | 3 | from app import auth 4 | 5 | from app.models import User, LoginLog 6 | 7 | from . import bp 8 | 9 | 10 | @bp.route('/login_logs') 11 | @auth.login_required 12 | def list_login_logs(): 13 | query = LoginLog.query \ 14 | .join(User, LoginLog.userIntID == User.id) \ 15 | .with_entities(LoginLog, User.username) 16 | 17 | username = request.args.get('username_like') 18 | if username: 19 | query = query.filter(User.username.like(u'%{0}%'.format(username))) 20 | 21 | if g.role_id not in [1, 2, 3]: 22 | query = query.filter(User.id == g.user_id) 23 | records = query.pagination(code_list=['isLogged']) 24 | return jsonify(records) 25 | -------------------------------------------------------------------------------- /server/app/services/base/views/select_options.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from flask import jsonify, g, request 4 | from sqlalchemy import or_ 5 | 6 | from app import auth 7 | from app.models import DictCode, Role 8 | from . import bp 9 | 10 | 11 | @bp.route('/select_options/dict_code') 12 | @auth.login_required(permission_required=False) 13 | def list_dict_code(): 14 | record = defaultdict(list) 15 | query_dict_code = DictCode.query.all() 16 | for dict_code in query_dict_code: 17 | if dict_code.codeValue is not None: 18 | code_value = dict_code.codeValue 19 | else: 20 | code_value = dict_code.codeStringValue 21 | option = { 22 | 'value': code_value, 23 | 'enLabel': dict_code.enLabel, 24 | 'zhLabel': dict_code.zhLabel 25 | } 26 | record[dict_code.code].append(option) 27 | return jsonify(record) 28 | 29 | 30 | @bp.route('/select_options/app_roles') 31 | @bp.route('/select_options/roles') 32 | @auth.login_required(permission_required=False) 33 | def list_select_options_roles(): 34 | role_type = 2 if request.path.endswith('/app_roles') else 1 35 | query = Role.query \ 36 | .filter(~Role.id.in_([1, 2, 3])) \ 37 | .filter(Role.roleType == role_type) 38 | 39 | if g.role_id != 1 and g.tenant_uid: 40 | query = query \ 41 | .filter(or_(Role.tenantID == g.tenant_uid, Role.isShare == 1)) 42 | roles = [ 43 | { 44 | 'value': role.id, 45 | 'label': role.roleName 46 | } 47 | for role in query.all() 48 | ] 49 | return jsonify(roles) 50 | 51 | -------------------------------------------------------------------------------- /server/app/services/base/views/tasks.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify 2 | 3 | from app import auth 4 | from actor_libs.errors import ParameterInvalid 5 | from app.models import ActorTask 6 | from . import bp 7 | 8 | 9 | @bp.route('/task_status') 10 | @auth.login_required(permission_required=False) 11 | def get_task_scheduler_status(): 12 | task_scheduler_id = request.args.get('taskID', type=str) 13 | if not task_scheduler_id: 14 | raise ParameterInvalid(field='taskID') 15 | query_task = ActorTask.query \ 16 | .filter(ActorTask.taskID == task_scheduler_id) \ 17 | .first_or_404() 18 | result = query_task.taskResult if query_task.taskResult else {} 19 | message = result.pop('message', '') 20 | record = { 21 | 'status': query_task.taskStatus, 22 | 'progress': query_task.taskProgress, 23 | 'message': message, 24 | 'taskID': query_task.taskID, 25 | 'result': result, 26 | } 27 | return jsonify(record) 28 | -------------------------------------------------------------------------------- /server/app/services/device_data/README.md: -------------------------------------------------------------------------------- 1 | # Device data 2 | -------------------------------------------------------------------------------- /server/app/services/device_data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/device_data/__init__.py -------------------------------------------------------------------------------- /server/app/services/device_data/resources.yml: -------------------------------------------------------------------------------- 1 | devices_capability_data-view: 2 | code: 'devices_capability_data-view' 3 | enable: 1 4 | icon: null 5 | level: 4 6 | method: 'GET' 7 | order: 12 8 | parentCode: 'devices-devices' 9 | tabs: 0 10 | url: '/device_capability_data' 11 | service: product_develop 12 | 13 | -------------------------------------------------------------------------------- /server/app/services/device_data/schemas.py: -------------------------------------------------------------------------------- 1 | from actor_libs.schemas import BaseSchema 2 | from actor_libs.schemas.fields import EmqDateTime, EmqString, EmqInteger 3 | 4 | 5 | __all__ = ['DeviceEventSchema', 'DeviceEventLatestSchema', 'ConnectLogSchema'] 6 | 7 | 8 | class DeviceEventSchema(BaseSchema): 9 | class Meta: 10 | additional = ('deviceID',) 11 | 12 | msgTime = EmqDateTime(allow_none=True) 13 | streamID = EmqString(required=True) 14 | topic = EmqString(required=True) 15 | dataType = EmqInteger(required=True) 16 | data = EmqString(required=True) 17 | responseResult = EmqString(required=True) 18 | 19 | 20 | class DeviceEventLatestSchema(DeviceEventSchema): 21 | ... 22 | 23 | 24 | class ConnectLogSchema(BaseSchema): 25 | class Meta: 26 | additional = ( 27 | 'deviceID', 'connectStatus', 'IP', 'keepAlive', 'msgTime' 28 | ) 29 | -------------------------------------------------------------------------------- /server/app/services/device_data/views/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | 4 | bp = Blueprint('device_data', __name__) 5 | 6 | from . import events # noqa: E402 7 | from . import connect_logs # noqa: E402 8 | from . import capability_data # noqa: E402 9 | from . import reports # noqa: E402 10 | from . import charts # noqa: E402 11 | 12 | 13 | __all__ = [ 14 | 'bp', 'events', 'connect_logs', 15 | 'capability_data', 'reports', 'charts' 16 | ] 17 | -------------------------------------------------------------------------------- /server/app/services/device_data/views/_reports_type/__init__.py: -------------------------------------------------------------------------------- 1 | from .aggr_events import devices_event_aggr_data 2 | 3 | 4 | __all__ = ['REPORTS_TYPE_FUNC'] 5 | 6 | 7 | REPORTS_TYPE_FUNC = { 8 | 'devicesEventAggr': devices_event_aggr_data 9 | } 10 | -------------------------------------------------------------------------------- /server/app/services/device_data/views/_utils.py: -------------------------------------------------------------------------------- 1 | import arrow 2 | from flask import request 3 | 4 | from actor_libs.errors import ParameterInvalid 5 | 6 | 7 | __all__ = ['validate_time_range'] 8 | 9 | 10 | def validate_time_range(limit_days: int = 7) -> None: 11 | start_time = request.args.get('start_time', type=str) 12 | end_time = request.args.get('end_time', type=str) 13 | if not (start_time and end_time): 14 | raise ParameterInvalid('start_time or end_time') 15 | 16 | try: 17 | start_time = arrow.get(start_time) 18 | except Exception: 19 | raise ParameterInvalid('start_time') 20 | try: 21 | end_time = arrow.get(end_time) 22 | except Exception: 23 | raise ParameterInvalid('end_time') 24 | 25 | if end_time.day - start_time.day > limit_days: 26 | raise ParameterInvalid('Time range is greater than 7 days') 27 | -------------------------------------------------------------------------------- /server/app/services/device_data/views/connect_logs.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, request 2 | from sqlalchemy import text 3 | 4 | from app import auth 5 | from app.models import Device, ConnectLog 6 | from . import bp 7 | 8 | 9 | @bp.route('/devices//connect_logs') 10 | @auth.login_required 11 | def view_connect_logs(device_id): 12 | device = Device.query \ 13 | .with_entities(Device.deviceID, Device.tenantID) \ 14 | .filter(Device.id == device_id).first_or_404() 15 | query = ConnectLog.query \ 16 | .filter(ConnectLog.deviceID == device.deviceID, 17 | ConnectLog.tenantID == device.tenantID) 18 | if not (request.args.get('start_time') or not request.args.get('end_time')): 19 | # if no specified start_time or end_time, return last 7 day of data 20 | query = query.filter(ConnectLog.msgTime >= text("NOW() - INTERVAL '7 DAYS'")) 21 | records = query.pagination(code_list=['connectStatus']) 22 | return jsonify(records) 23 | -------------------------------------------------------------------------------- /server/app/services/device_data/views/events.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from app import auth 4 | from app.models import Device, DeviceEvent, DeviceEventLatest 5 | from . import bp 6 | from ._utils import validate_time_range 7 | 8 | 9 | @bp.route('/devices//events') 10 | @auth.login_required 11 | def list_device_events(device_id): 12 | validate_time_range() 13 | device = Device.query \ 14 | .with_entities(Device.deviceID, Device.tenantID) \ 15 | .filter(Device.id == device_id).first_or_404() 16 | 17 | events_query = DeviceEvent.query.filter(DeviceEvent.deviceID == device.deviceID) 18 | records = events_query.pagination(code_list=['dataType']) 19 | 20 | return jsonify(records) 21 | 22 | 23 | @bp.route('/devices//last_event') 24 | @auth.login_required 25 | def view_device_last_event(device_id): 26 | device = Device.query \ 27 | .with_entities(Device.deviceID, Device.tenantID) \ 28 | .filter(Device.id == device_id).first_or_404() 29 | 30 | event = DeviceEventLatest.query \ 31 | .filter_tenant(tenant_uid=device.tenantID) \ 32 | .filter(DeviceEventLatest.deviceID == device.deviceID) \ 33 | .first() 34 | 35 | record = [event.to_dict(code_list=['dataType'])] if event else [] 36 | return jsonify(record) 37 | -------------------------------------------------------------------------------- /server/app/services/devices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/devices/__init__.py -------------------------------------------------------------------------------- /server/app/services/devices/views/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | 4 | bp = Blueprint('devices', __name__) 5 | 6 | from . import emqx # noqa: E402 7 | from . import devices # noqa: E402 8 | from . import select_options # noqa: E402 9 | from . import groups # noqa: E402 10 | from . import security # noqa: E402 11 | 12 | 13 | __all__ = [ 14 | 'bp', 'emqx', 'devices', 'groups', 'security', 'select_options' 15 | ] 16 | -------------------------------------------------------------------------------- /server/app/services/products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/products/__init__.py -------------------------------------------------------------------------------- /server/app/services/products/views/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | 4 | bp = Blueprint('products', __name__) 5 | 6 | from . import products # noqa: E402 7 | from . import data_points # noqa: E402 8 | from . import data_streams # noqa: E402 9 | from . import stream_points # noqa: E402 10 | from . import select_options # noqa: E402 11 | from . import codec # noqa: E402 12 | 13 | 14 | __all__ = [ 15 | 'bp', 'products', 'select_options', 16 | 'data_points', 'data_streams', 'stream_points', 'codec' 17 | ] 18 | -------------------------------------------------------------------------------- /server/app/services/publish/README.md: -------------------------------------------------------------------------------- 1 | # PublishServer -------------------------------------------------------------------------------- /server/app/services/publish/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | -------------------------------------------------------------------------------- /server/app/services/publish/resources.yml: -------------------------------------------------------------------------------- 1 | device_publish-create: 2 | code: 'device_publish-create' 3 | enable: 1 4 | icon: null 5 | level: 4 6 | method: 'POST' 7 | order: 6 8 | parentCode: 'devices-devices' 9 | tabs: 0 10 | url: '/device_publish' 11 | service: devices 12 | 13 | timer_publish: 14 | code: 'timer_publish' 15 | enable: 1 16 | icon: null 17 | level: 2 18 | method: null 19 | order: 4 20 | parentCode: 'device_manage' 21 | tabs: 0 22 | url: '/timer_publish' 23 | service: devices 24 | 25 | timer_publish-view: 26 | code: 'timer_publish-view' 27 | enable: 1 28 | icon: null 29 | level: 3 30 | method: 'GET' 31 | order: 1 32 | parentCode: 'timer_publish' 33 | tabs: 0 34 | url: '/timer_publish' 35 | service: devices 36 | 37 | timer_publish-create: 38 | code: 'timer_publish-create' 39 | enable: 1 40 | icon: null 41 | level: 3 42 | method: 'POST' 43 | order: 2 44 | parentCode: 'timer_publish' 45 | tabs: 0 46 | url: '/timer_publish' 47 | service: devices 48 | 49 | timer_publish-delete: 50 | code: 'timer_publish-delete' 51 | enable: 1 52 | icon: null 53 | level: 3 54 | method: 'DELETE' 55 | order: 3 56 | parentCode: 'timer_publish' 57 | tabs: 0 58 | url: '/timer_publish' 59 | service: devices -------------------------------------------------------------------------------- /server/app/services/publish/views/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | 4 | bp = Blueprint('publish', __name__) 5 | 6 | from . import devices # noqa: E402 7 | from . import timers # noqa: E402 8 | 9 | __all__ = [ 10 | 'bp', 'devices', 'timers' 11 | ] 12 | -------------------------------------------------------------------------------- /server/app/services/rules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/rules/__init__.py -------------------------------------------------------------------------------- /server/app/services/rules/views/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | 4 | bp = Blueprint('rules', __name__) 5 | 6 | from . import rules # noqa: E402 7 | from . import actions # noqa: E402 8 | from . import select_options # noqa: E402 9 | 10 | 11 | __all__ = ['bp', 'rules', 'actions', 'select_options'] 12 | -------------------------------------------------------------------------------- /server/app/services/rules/views/select_options.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from app import auth 4 | from app.models import Action 5 | from . import bp 6 | 7 | 8 | @bp.route('/select_options/actions') 9 | @auth.login_required(permission_required=False) 10 | def list_select_options_actions(): 11 | records = Action.query \ 12 | .with_entities(Action.id.label('value'), Action.actionName.label('label')) \ 13 | .select_options() 14 | 15 | return jsonify(records) 16 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/README.md: -------------------------------------------------------------------------------- 1 | # actorcloud tasks_scheduler 2 | 3 | * async_tasks: include emqx publish,timer publish and excel import/export 4 | * timer_tasks: include actorcloud timer tasks 5 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/tasks_scheduler/__init__.py -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/async_tasks/README.md: -------------------------------------------------------------------------------- 1 | # async tasks 2 | 3 | * device_publish 4 | * excel_export 5 | * excel_import 6 | 7 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/async_tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/tasks_scheduler/async_tasks/__init__.py -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/async_tasks/app/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import app 2 | 3 | __all__ = ['app'] -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/async_tasks/app/config.py: -------------------------------------------------------------------------------- 1 | from config import AsyncTaskConfig 2 | 3 | 4 | project_config = AsyncTaskConfig().config 5 | 6 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/async_tasks/app/emqx/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth import device_auth 2 | from .callback import backend_callback 3 | from .publish import device_publish_task 4 | 5 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/async_tasks/app/excels/__init__.py: -------------------------------------------------------------------------------- 1 | from .devices_export import devices_export_task 2 | from .devices_import import devices_import_task 3 | 4 | 5 | __all__ = ['devices_export_task', 'devices_import_task'] 6 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/README.md: -------------------------------------------------------------------------------- 1 | # timer task 2 | * device count 3 | * api cunt 4 | * emqx bills aggr 5 | * device_events aggr 6 | * timer publish -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/tasks_scheduler/timer_tasks/__init__.py -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/tasks_scheduler/timer_tasks/app/__init__.py -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/api_count/__init__.py: -------------------------------------------------------------------------------- 1 | from .count_task import api_count_task 2 | 3 | 4 | __all__ = ['api_count_task'] 5 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/config.py: -------------------------------------------------------------------------------- 1 | from config import TimerTaskConfig 2 | 3 | project_config = TimerTaskConfig().config 4 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/device_count/__init__.py: -------------------------------------------------------------------------------- 1 | from .count_task import device_count_task 2 | 3 | 4 | __all__ = ['device_count_task'] 5 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/device_count/sql_statements.py: -------------------------------------------------------------------------------- 1 | devices_count_sql = """ 2 | INSERT INTO {table}("createAt", "countTime", "tenantID", 3 | "deviceCount", "deviceOnlineCount", 4 | "deviceOfflineCount", "deviceSleepCount") 5 | SELECT current_timestamp AS "createAt", 6 | date_trunc('{time_unit}', 7 | current_timestamp - INTERVAL '1 {time_unit}') AS "countTime", 8 | devices."tenantID", 9 | COUNT(*) AS "deviceCount", 10 | COUNT( 11 | CASE WHEN "deviceStatus" = 0 THEN 1 ELSE NULL END) AS "deviceOfflineCount", 12 | COUNT( 13 | CASE WHEN "deviceStatus" = 1 THEN 1 ELSE NULL END) AS "deviceOnlineCount", 14 | COUNT( 15 | CASE WHEN "deviceStatus" = 2 THEN 1 ELSE NULL END) AS "deviceSleepCount" 16 | FROM devices 17 | GROUP BY devices."tenantID" 18 | """ 19 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/device_events/__init__.py: -------------------------------------------------------------------------------- 1 | from .aggr_task import device_events_aggr_task 2 | 3 | 4 | __all__ = ['device_events_aggr_task'] 5 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/device_events/aggr_task.py: -------------------------------------------------------------------------------- 1 | import arrow 2 | from actor_libs.tasks.backend import get_task_result 3 | 4 | from actor_libs.database.async_db import db 5 | from actor_libs.types import TaskResult 6 | from .sql_statement import ( 7 | device_events_hour_aggr_sql, device_events_day_aggr_sql, 8 | device_events_month_aggr_sql 9 | ) 10 | from ..config import project_config 11 | 12 | 13 | __all__ = ['device_events_aggr_task'] 14 | 15 | 16 | async def device_events_aggr_task() -> TaskResult: 17 | aggr_result = {} 18 | aggr_device_events_hour = await db.execute( 19 | device_events_hour_aggr_sql 20 | ) 21 | aggr_result['device_events_hour'] = aggr_device_events_hour 22 | 23 | date_now = arrow.now(tz=project_config['TIMEZONE']) 24 | if date_now.hour == 0: 25 | aggr_result['device_events_day'] = await db.execute( 26 | device_events_day_aggr_sql 27 | ) 28 | if date_now.day == 1 and date_now.hour == 0: 29 | aggr_result['device_events_month'] = await db.execute( 30 | device_events_month_aggr_sql 31 | ) 32 | aggr_status = aggr_result.values() 33 | if aggr_status and all(aggr_status): 34 | task_result = get_task_result( 35 | status=3, message='Device events aggr success', result=aggr_result 36 | ) 37 | else: 38 | task_result = get_task_result( 39 | status=4, message='Device events aggr failed', result=aggr_result 40 | ) 41 | return task_result 42 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/emqx_bills/__init__.py: -------------------------------------------------------------------------------- 1 | from .aggr_task import emqx_bills_aggr_task 2 | 3 | 4 | __all__ = ['emqx_bills_aggr_task'] 5 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/timer_publish/__init__.py: -------------------------------------------------------------------------------- 1 | from .publish_task import timer_publish_task 2 | 3 | 4 | __all__ = ['timer_publish_task'] 5 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/timer_publish/_utills.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | 3 | from actor_libs.database.async_db import db 4 | 5 | 6 | __all__ = ['get_timer_tasks', 'update_crontab_tasks'] 7 | 8 | 9 | async def get_timer_tasks(due_tasks_id: List[int]) -> List[Dict]: 10 | query_due_tasks_sql = f""" 11 | SELECT timer_publish.*, users."tenantID" 12 | FROM timer_publish 13 | JOIN users ON users.id = timer_publish."userIntID" 14 | WHERE timer_publish.id = ANY ('{set(due_tasks_id)}'::int[]) 15 | """ 16 | query_timer_tasks = await db.fetch_many( 17 | sql=query_due_tasks_sql 18 | ) 19 | due_tasks = [dict(_task) for _task in query_timer_tasks] 20 | return due_tasks 21 | 22 | 23 | async def update_crontab_tasks(crontab_ids: List): 24 | update_sql = f""" 25 | UPDATE "timer_publish" 26 | SET "taskStatus"=3 27 | WHERE timer_publish.id = ANY ('{set(crontab_ids)}'::int[]) 28 | """ 29 | await db.execute(update_sql) 30 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/app/timer_publish/publish_task.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | 3 | from ._device import get_devices_info, build_device_publish_info, devices_publish 4 | from ._parse import get_due_tasks 5 | from ._utills import get_timer_tasks 6 | 7 | 8 | __all__ = ['timer_publish_task'] 9 | 10 | 11 | async def timer_publish_task(): 12 | due_tasks_id = await get_due_tasks() 13 | if not due_tasks_id: 14 | # not due task, return 15 | return {} 16 | publish_info = [] 17 | due_tasks: List[Dict] = await get_timer_tasks(due_tasks_id) 18 | device_ids = [] 19 | for time_task in due_tasks: 20 | device_ids.append(time_task.get('deviceIntID')) 21 | devices_info = await get_devices_info(device_ids) 22 | for time_task in due_tasks: 23 | device_info = devices_info[time_task.get('deviceIntID')] 24 | _info = await build_device_publish_info(time_task, device_info) 25 | publish_info.append(_info) 26 | task_result = await devices_publish(publish_info=publish_info) 27 | return task_result 28 | -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/app/services/tasks_scheduler/timer_tasks/test/__init__.py -------------------------------------------------------------------------------- /server/app/services/tasks_scheduler/timer_tasks/test/timer_publish.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import asyncpg 4 | from app.timer_publish import timer_publish_task 5 | 6 | from actor_libs.database.async_db import db 7 | 8 | 9 | project_config = {} 10 | 11 | 12 | async def timer_publish_t(): 13 | _pool = await asyncpg.create_pool( 14 | host=project_config.get('POSTGRES_HOST', 'localhost'), 15 | port=project_config.get('POSTGRES_PORT', 5432), 16 | user=project_config.get('POSTGRES_USER', 'root'), 17 | password=project_config.get('POSTGRES_PASSWORD', 'public'), 18 | database=project_config.get('POSTGRES_DATABASE', 'actorcloud'), 19 | min_size=5, max_size=10 20 | ) 21 | db.pool = _pool 22 | result = await timer_publish_task() 23 | print(result) 24 | 25 | 26 | if __name__ == '__main__': 27 | loop = asyncio.get_event_loop() 28 | loop.run_until_complete(timer_publish_t()) 29 | -------------------------------------------------------------------------------- /server/config/.gitignore: -------------------------------------------------------------------------------- 1 | gunicorn.py 2 | actorcloud_supervisord.conf 3 | -------------------------------------------------------------------------------- /server/config/base/lwm2m_obj/2049.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ActiveCmdhPolicy 4 | 5 | 2049 6 | urn:oma:lwm2m:ext:20491.0 7 | 1.0Single 8 | Optional 9 | 10 | ActiveLink 11 | RW 12 | Single 13 | Mandatory 14 | Objlnk 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /server/config/base/lwm2m_obj/2054.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | CmdhNetworkAccessRules 4 | 5 | 2054 6 | urn:oma:lwm2m:ext:20541.0 7 | 1.0Multiple 8 | Optional 9 | 10 | ApplicableEventCategories 11 | RW 12 | Multiple 13 | Mandatory 14 | Integer 15 | 16 | 17 | 18 | 19 | NetworkAccessRule 20 | RW 21 | Multiple 22 | Optional 23 | Objlnk 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /server/config/certs/actorcloud/my_ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICZzCCAdACCQCPTgPRluRd6jANBgkqhkiG9w0BAQUFADB4MQswCQYDVQQGEwJD 3 | TjELMAkGA1UECAwCWkoxCzAJBgNVBAcMAkhaMQwwCgYDVQQKDANFTVExDDAKBgNV 4 | BAsMA0VNUTETMBEGA1UEAwwKQWN0b3JDbG91ZDEeMBwGCSqGSIb3DQEJARYPemhh 5 | bmd3aEBlbXF4LmlvMB4XDTE5MDQxNTA3MzUwOFoXDTE5MDUxNTA3MzUwOFoweDEL 6 | MAkGA1UEBhMCQ04xCzAJBgNVBAgMAlpKMQswCQYDVQQHDAJIWjEMMAoGA1UECgwD 7 | RU1RMQwwCgYDVQQLDANFTVExEzARBgNVBAMMCkFjdG9yQ2xvdWQxHjAcBgkqhkiG 8 | 9w0BCQEWD3poYW5nd2hAZW1xeC5pbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC 9 | gYEAvixnXcPTnTcdDV2XPV/dH3aFBoK5igZwTVQCj9tLddytTMfDkr2rizM6y0PH 10 | R4SPKiYLFJGBLPcD8J5Mn9ZyUcozI2THoWxkCHVVaGZkGdbM7aZbj4nQgg4SvPnW 11 | yl0N2miDj0YkFXwEZyFb/wzjeN3J7A9nYmuWECjKnTf/nxMCAwEAATANBgkqhkiG 12 | 9w0BAQUFAAOBgQAV4YkvqklwRhKVDHrfH9Lm3pPJRrHoBrxL6QlMuK3UyLOFpiAp 13 | sBstf2LXmY8So4HB7KLILw5mmeiHVKZDODPHr5nxGZ0oTNCAvqzzZjnMZxZWq+VQ 14 | Ls8DrVzxooLCeJEzejIO3MrjFbHp7kwB725EuDfwoMQcfuzEKralFcrzLQ== 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /server/config/certs/actorcloud/my_ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC+LGddw9OdNx0NXZc9X90fdoUGgrmKBnBNVAKP20t13K1Mx8OS 3 | vauLMzrLQ8dHhI8qJgsUkYEs9wPwnkyf1nJRyjMjZMehbGQIdVVoZmQZ1sztpluP 4 | idCCDhK8+dbKXQ3aaIOPRiQVfARnIVv/DON43cnsD2dia5YQKMqdN/+fEwIDAQAB 5 | AoGAY2ls43wh2/ZajokVLXU2b2MSaPPfL8Lr49B9wlu9l7AzO4bVupjB2vN90ZHt 6 | 03n9KOhYNHlXkHzUJ21o2slMWoL8bo9tr1MNgW32XM+e6BflcIrYvgHdAeVXLcfO 7 | MOvBu99iCjI9r9NKbdYT2bLGvCP+iMqP2JqONltLvYyK/GECQQD2r7c2hYEOd8Th 8 | kJPKpVtjJLfjzik2bM+uaRTTvMldtJxhPaxq1uDZtE0rK7S3Oh1H1dxlr7Y0LC+9 9 | bUZvEFTjAkEAxVp6Cw+hEzMEcjYjNix1CTratJ8aZpYOQLf1UT2PXwV+UDhXeF/K 10 | CBvUlVM51pD0Bsw/1l5JGxge7Ed9mErUEQJAE7wtARyfHYQxaQZC/JRMAlt+pqfo 11 | niUIXGClvOa+iVOYqyLe91EnfeMxxUFi3MG2c/fFARGUlwnt686hMuNwlwJBALfS 12 | DUkONrb+Vw7WlDeacyy0sA96/ok9DWuErTANieQ05rFruTV662BgI8MusPnFLDbE 13 | ulU+nxNohS0JbdeTIzECQF7lFQ/Ses1a9E6vSNk99JOj4ryP8XPaSMwNh3zMQv0O 14 | 7alSA1LZPhrnhVLPRw832wA2c0n+iMN/+ivnmQ/hCs0= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /server/config/certs/actorcloud/root_ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICZzCCAdACCQCPTgPRluRd6jANBgkqhkiG9w0BAQUFADB4MQswCQYDVQQGEwJD 3 | TjELMAkGA1UECAwCWkoxCzAJBgNVBAcMAkhaMQwwCgYDVQQKDANFTVExDDAKBgNV 4 | BAsMA0VNUTETMBEGA1UEAwwKQWN0b3JDbG91ZDEeMBwGCSqGSIb3DQEJARYPemhh 5 | bmd3aEBlbXF4LmlvMB4XDTE5MDQxNTA3MzUwOFoXDTE5MDUxNTA3MzUwOFoweDEL 6 | MAkGA1UEBhMCQ04xCzAJBgNVBAgMAlpKMQswCQYDVQQHDAJIWjEMMAoGA1UECgwD 7 | RU1RMQwwCgYDVQQLDANFTVExEzARBgNVBAMMCkFjdG9yQ2xvdWQxHjAcBgkqhkiG 8 | 9w0BCQEWD3poYW5nd2hAZW1xeC5pbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC 9 | gYEAvixnXcPTnTcdDV2XPV/dH3aFBoK5igZwTVQCj9tLddytTMfDkr2rizM6y0PH 10 | R4SPKiYLFJGBLPcD8J5Mn9ZyUcozI2THoWxkCHVVaGZkGdbM7aZbj4nQgg4SvPnW 11 | yl0N2miDj0YkFXwEZyFb/wzjeN3J7A9nYmuWECjKnTf/nxMCAwEAATANBgkqhkiG 12 | 9w0BAQUFAAOBgQAV4YkvqklwRhKVDHrfH9Lm3pPJRrHoBrxL6QlMuK3UyLOFpiAp 13 | sBstf2LXmY8So4HB7KLILw5mmeiHVKZDODPHr5nxGZ0oTNCAvqzzZjnMZxZWq+VQ 14 | Ls8DrVzxooLCeJEzejIO3MrjFbHp7kwB725EuDfwoMQcfuzEKralFcrzLQ== 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /server/instance/certs/actorcloud/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /server/manage.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from app import create_app, project_manage 4 | 5 | 6 | app = create_app() 7 | 8 | 9 | @app.cli.command('deploy', short_help='Deploy project') 10 | def actorcloud_deploy(): 11 | project_manage.project_deploy() 12 | 13 | 14 | @app.cli.command('upgrade', short_help='Upgrade project') 15 | def actorcloud_upgrade(): 16 | project_manage.project_upgrade() 17 | 18 | 19 | if __name__ != '__main__': 20 | gunicorn_logger = logging.getLogger('gunicorn.error') 21 | app.logger.handlers = gunicorn_logger.handlers 22 | app.logger.setLevel(gunicorn_logger.level) 23 | -------------------------------------------------------------------------------- /server/migrations/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .ropeproject 4 | *.pyc 5 | orm 6 | -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # python_version = "3.6" 3 | ################################################################################ 4 | 5 | aiohttp==3.5.4 6 | arrow==0.14.1 7 | asyncpg==0.18.3 8 | bcrypt==3.1.6 9 | croniter==0.3.30 10 | flask-cors==3.0.7 11 | flask-httpauth==3.3.0 12 | flask-mail==0.9.1 13 | flask-migrate==2.5.2 14 | flask-sqlalchemy==2.4.0 15 | flask-uploads==0.2.1 16 | flask==1.0.3 17 | gunicorn==19.9.0 18 | immutables==0.9 19 | itsdangerous==1.1.0 20 | lxml==4.3.3 21 | marshmallow==2.19.2 22 | mode==4.0.0 23 | numpy==1.16.4 24 | pandas==0.24.2 25 | paramiko==2.4.2 26 | psycopg2-binary==2.8.2 27 | pyopenssl==19.0.0 28 | pytest==4.6.2 29 | python-multipart==0.0.5 30 | pytz==2019.1 31 | pyyaml==5.1 32 | requests==2.22.0 33 | sqlalchemy==1.3.4 34 | sqlparse==0.3.0 35 | starlette==0.12.0 36 | uvicorn==0.7.1 37 | uvloop==0.12.2 38 | xlrd==1.2.0 39 | xlsxwriter==1.1.8 -------------------------------------------------------------------------------- /server/run.py: -------------------------------------------------------------------------------- 1 | import click 2 | from config import BaseConfig 3 | 4 | 5 | @click.group() 6 | def actorcloud_run(): 7 | pass 8 | 9 | 10 | @actorcloud_run.command() 11 | def backend(): 12 | from manage import app 13 | 14 | base_config = BaseConfig().config 15 | _port = int(base_config['BACKEND_NODE'].split(':')[-1]) 16 | log_level = base_config['LOG_LEVEL'] 17 | if log_level == 'debug': 18 | debug = True 19 | else: 20 | debug = False 21 | app.run(host='0.0.0.0', port=_port, debug=debug) 22 | 23 | 24 | @actorcloud_run.command() 25 | def async_tasks(): 26 | import uvicorn 27 | 28 | from app.services.tasks_scheduler.async_tasks.app import app 29 | 30 | base_config = BaseConfig().config 31 | _port = int(base_config['ASYNC_TASKS_NODE'].split(':')[-1]) 32 | log_level = base_config['LOG_LEVEL'] 33 | uvicorn.run( 34 | app, 35 | host='0.0.0.0', 36 | port=_port, 37 | loop='uvloop', 38 | log_level=log_level 39 | ) 40 | 41 | 42 | @actorcloud_run.command() 43 | def timer_tasks(): 44 | import asyncio 45 | 46 | import uvloop 47 | from mode import Worker 48 | 49 | from app.services.tasks_scheduler.timer_tasks.app.base import app 50 | 51 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 52 | loop = asyncio.get_event_loop() 53 | 54 | base_config = BaseConfig().config 55 | log_level = base_config['LOG_LEVEL'] 56 | worker = Worker(app, loglevel=log_level, loop=loop) 57 | worker.execute_from_commandline() 58 | 59 | 60 | if __name__ == '__main__': 61 | actorcloud_run() 62 | -------------------------------------------------------------------------------- /server/static/download/export_excels/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /server/static/download/templates/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore -------------------------------------------------------------------------------- /server/static/download/templates/devices_template_en.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/static/download/templates/devices_template_en.xlsx -------------------------------------------------------------------------------- /server/static/download/templates/devices_template_zh.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/static/download/templates/devices_template_zh.xlsx -------------------------------------------------------------------------------- /server/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/static/images/favicon.ico -------------------------------------------------------------------------------- /server/static/images/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/static/images/logo-dark.png -------------------------------------------------------------------------------- /server/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/static/images/logo.png -------------------------------------------------------------------------------- /server/static/images/sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/server/static/images/sign.png -------------------------------------------------------------------------------- /server/static/upload/excels/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /server/static/upload/images/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /server/static/upload/packages/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/airbnb', 9 | ], 10 | rules: { 11 | semi: 0, 12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 14 | 'no-shadow': 0, 15 | 'no-param-reassign': 0, 16 | 'new-cap': 0, 17 | 'consistent-return': 0, 18 | 'import/no-extraneous-dependencies': [2, { devDependencies: true }], 19 | 'import/extensions': ['error', 'always', { 20 | js: 'never', 21 | vue: 'never', 22 | }], 23 | 'object-curly-newline': 0, 24 | 'arrow-body-style': 0, 25 | 'prefer-destructuring': 0, 26 | 'no-tabs': 0, 27 | 'vue/no-use-v-if-with-v-for': 0, 28 | 'max-len': [2, { code: 150, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true }], 29 | }, 30 | parserOptions: { 31 | parser: 'babel-eslint', 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /src/assets/micro.apps.json 5 | /src/assets/micro.private.json 6 | /src/private_apps 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw* 25 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # ActorCloud FE 2 | 3 | > The view part of the actorcloud 4 | 5 | ## Usage 6 | 7 | ```bash 8 | # Install dependencies 9 | $ yarn 10 | # Start on localhost 11 | $ yarn dev-localhost 12 | # Build the production 13 | $ yarn build 14 | # Unit test 15 | $ yarn test:unit 16 | # Get directory of apps 17 | $ yarn get-apps 18 | ``` 19 | -------------------------------------------------------------------------------- /ui/apps.config.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Get directory of apps 4 | const fs = require('fs') 5 | const path = require('path') 6 | 7 | const getAppsDirs = () => { 8 | const fsExistsSync = (path) => { 9 | try { 10 | fs.accessSync(path) 11 | } catch (e) { 12 | return false 13 | } 14 | return true 15 | } 16 | 17 | const dirs = {} 18 | 19 | const appsRoot = path.join(`${__dirname}/src/apps`) 20 | dirs.apps = fs.readdirSync(appsRoot) 21 | .filter(p => !p.includes('.')) 22 | 23 | const privateRoot = path.join(`${__dirname}/src/private_apps`) 24 | if (fsExistsSync(privateRoot)) { 25 | dirs.private = fs.readdirSync(privateRoot) 26 | .filter(p => !p.includes('.')) 27 | } 28 | return dirs 29 | } 30 | 31 | const setApps = (dirs) => { 32 | fs.writeFile( 33 | './src/assets/micro.apps.json', 34 | JSON.stringify(dirs.apps), 35 | (err) => { 36 | if (err) { 37 | console.log(err) 38 | } else { 39 | fs.writeFile( 40 | './src/assets/micro.private.json', 41 | JSON.stringify(dirs.private || []), 42 | (err) => { 43 | if (err) { 44 | console.log(err) 45 | } 46 | console.log('✅ Get apps successfully') 47 | }, 48 | ) 49 | } 50 | }, 51 | ) 52 | } 53 | 54 | setApps(getAppsDirs()) 55 | -------------------------------------------------------------------------------- /ui/babel.config.js: -------------------------------------------------------------------------------- 1 | const plugins = [ 2 | [ 3 | 'component', 4 | { 5 | libraryName: 'element-ui', 6 | styleLibraryName: 'theme-chalk', 7 | }, 8 | ], 9 | ] 10 | if (process.env.NODE_ENV === 'development') { 11 | plugins.push('dynamic-import-node') 12 | } 13 | module.exports = { 14 | presets: [ 15 | '@vue/app', 16 | ], 17 | plugins, 18 | } 19 | -------------------------------------------------------------------------------- /ui/commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'subject-case': [0, 'always', 72], 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | actorcloud 6 | 7 | 8 | 9 | 10 | 11 | 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ui/public/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/public/static/.gitkeep -------------------------------------------------------------------------------- /ui/public/static/css/fullLoading.css: -------------------------------------------------------------------------------- 1 | #full-loading.loading-mask{position:absolute;z-index:2000;background-color:#fff;margin:0;top:0;right:0;bottom:0;left:0;-webkit-transition:opacity .3s;transition:opacity .3s}#full-loading .loading-spinner{top:50%;margin-top:-21px;width:100%;text-align:center;position:absolute}#full-loading .loading-spinner .circular{height:50px;width:50px;-webkit-animation:loading-rotate 2s linear infinite;animation:loading-rotate 2s linear infinite}#full-loading .loading-spinner .path{-webkit-animation:loading-dash 1.5s ease-in-out infinite;animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:3;stroke:#2fc285;stroke-linecap:round}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}@keyframes loading-rotate{100%{transform:rotate(360deg)}} -------------------------------------------------------------------------------- /ui/public/static/css/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/public/static/css/iconfont.eot -------------------------------------------------------------------------------- /ui/public/static/css/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/public/static/css/iconfont.ttf -------------------------------------------------------------------------------- /ui/public/static/css/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/public/static/css/iconfont.woff -------------------------------------------------------------------------------- /ui/public/static/css/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/public/static/css/iconfont.woff2 -------------------------------------------------------------------------------- /ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 17 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /ui/src/apps/accounts/assets/images/bg-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/accounts/assets/images/bg-circle.png -------------------------------------------------------------------------------- /ui/src/apps/accounts/assets/images/bg-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/accounts/assets/images/bg-large.png -------------------------------------------------------------------------------- /ui/src/apps/accounts/assets/images/bg-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/accounts/assets/images/bg-small.png -------------------------------------------------------------------------------- /ui/src/apps/accounts/assets/images/logo-actor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/accounts/assets/images/logo-actor.png -------------------------------------------------------------------------------- /ui/src/apps/accounts/components/Roles.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 51 | -------------------------------------------------------------------------------- /ui/src/apps/accounts/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { path: '', component: () => import('./views/Dashboard') }, 3 | { path: '/users/users', component: () => import('./views/Users') }, 4 | { path: '/users/invitations', component: () => import('./views/Invitations') }, 5 | { path: '/users/users/:id', component: () => import('./views/UserDetails') }, 6 | { path: '/applications', component: () => import('./views/Applications') }, 7 | { path: '/applications/:id', component: () => import('./views/ApplicationDetails') }, 8 | { path: '/tenants', component: () => import('./views/Tenants') }, 9 | { path: '/tenants/:id', component: () => import('./views/TenantDetails') }, 10 | { path: '/roles', component: () => import('./views/UserRoles') }, 11 | { path: '/roles/:id', component: () => import('./views/UserRoleDetails') }, 12 | { path: '/app_roles', component: () => import('./views/AppRoles') }, 13 | { path: '/app_roles/:id', component: () => import('./views/AppRoleDetails') }, 14 | { path: '/login_logs', component: () => import('./views/LoginLogs') }, 15 | { path: '/messages', component: () => import('./views/MessagesCenter') }, 16 | { path: '/messages/:id', component: () => import('./views/MessageCenterDetails') }, 17 | { path: 'operate_reporting/operate_detail', component: () => import('./views/Operation'), meta: { requiresAdmin: true } }, 18 | { path: 'operate_reporting/operate_detail/:id', component: () => import('./views/OperationDetails'), meta: { requiresAdmin: true } }, 19 | { path: 'operate_reporting/operate_overview', component: () => import('./views/Overview'), meta: { requiresAdmin: true } }, 20 | ] 21 | 22 | export default routes 23 | -------------------------------------------------------------------------------- /ui/src/apps/accounts/views/AppRoleDetails.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 15 | -------------------------------------------------------------------------------- /ui/src/apps/accounts/views/AppRoles.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 15 | -------------------------------------------------------------------------------- /ui/src/apps/accounts/views/LoginLogs.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | 59 | -------------------------------------------------------------------------------- /ui/src/apps/accounts/views/MessageCenterDetails.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 45 | 46 | 59 | -------------------------------------------------------------------------------- /ui/src/apps/accounts/views/UserRoleDetails.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 15 | -------------------------------------------------------------------------------- /ui/src/apps/accounts/views/UserRoles.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 15 | -------------------------------------------------------------------------------- /ui/src/apps/alerts/assets/tag.scss: -------------------------------------------------------------------------------- 1 | .el-tag--danger { 2 | background-color: rgba(211, 31, 31, 0.1); 3 | border-color: rgba(211, 31, 31, 0.2); 4 | color: #d31f1f; 5 | } 6 | .el-tag--warning { 7 | background-color: rgba(241, 105, 60, 0.1); 8 | border-color: rgba(241, 105, 60, 0.2); 9 | color: #f1693c; 10 | } 11 | .el-tag--info { 12 | background-color: rgba(241, 159, 57, 0.1); 13 | border-color: rgba(241, 159, 57, 0.2); 14 | color: #f19f39; 15 | } 16 | .el-tag--success { 17 | background-color: rgba(227, 195, 43, 0.1); 18 | border-color: rgba(227, 195, 43, 0.2); 19 | color: #e3c32b; 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/apps/alerts/lang/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | alerts: { 3 | currentAlerts: 'Current Alerts', 4 | historyAlerts: 'History Alerts', 5 | alertName: 'Alert Name', 6 | rules: 'Rules', 7 | alertContent: 'Alert Content', 8 | alertTimes: 'Alert Times', 9 | alertSeverity: 'Alert Severity', 10 | startTime: 'Start Time', 11 | endTime: 'End Time', 12 | emergency: 'Emergency', 13 | main: 'Main', 14 | secondary: 'Secondary', 15 | warning: 'Warning', 16 | alertDetail: 'Alert Detail', 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/apps/alerts/lang/zh_CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | alerts: { 3 | currentAlerts: '当前告警', 4 | historyAlerts: '历史告警', 5 | alertName: '告警名称', 6 | rules: '规则', 7 | alertContent: '告警内容', 8 | alertTimes: '告警次数', 9 | alertSeverity: '告警等级', 10 | startTime: '开始时间', 11 | endTime: '结束时间', 12 | emergency: '紧急', 13 | main: '主要', 14 | secondary: '次要', 15 | warning: '警告', 16 | alertDetail: '告警详情', 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/apps/alerts/routes.js: -------------------------------------------------------------------------------- 1 | 2 | const routes = [ 3 | { path: 'current_alerts', component: () => import('./views/CurrentAlerts') }, 4 | { path: 'current_alerts/:id', component: () => import('./views/CurrentAlertDetails') }, 5 | { path: 'history_alerts', component: () => import('./views/HistoryAlerts') }, 6 | { path: 'history_alerts/:id', component: () => import('./views/HistoryAlertDetails') }, 7 | ] 8 | 9 | export default routes 10 | -------------------------------------------------------------------------------- /ui/src/apps/alerts/store.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/alerts/store.js -------------------------------------------------------------------------------- /ui/src/apps/devices/assets/images/alert-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/devices/assets/images/alert-dark.png -------------------------------------------------------------------------------- /ui/src/apps/devices/assets/images/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/devices/assets/images/alert.png -------------------------------------------------------------------------------- /ui/src/apps/devices/assets/images/log-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/devices/assets/images/log-dark.png -------------------------------------------------------------------------------- /ui/src/apps/devices/assets/images/log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/devices/assets/images/log.png -------------------------------------------------------------------------------- /ui/src/apps/devices/assets/images/noData.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/devices/assets/images/noData.png -------------------------------------------------------------------------------- /ui/src/apps/devices/assets/images/noTrack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/devices/assets/images/noTrack.png -------------------------------------------------------------------------------- /ui/src/apps/devices/components/GroupDetailTabs.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 26 | -------------------------------------------------------------------------------- /ui/src/apps/devices/store.js: -------------------------------------------------------------------------------- 1 | const currentDevices = JSON.parse(localStorage.getItem('currentDevices')) || [] 2 | 3 | const state = { 4 | currentDevices, 5 | } 6 | 7 | const STORE_DEVICES = 'STORE_DEVICES' 8 | 9 | const actions = { 10 | [STORE_DEVICES]({ commit }, payload) { 11 | localStorage.setItem('currentDevices', JSON.stringify(payload.currentDevices)) 12 | commit(STORE_DEVICES, payload.currentDevices) 13 | }, 14 | } 15 | 16 | const mutations = { 17 | [STORE_DEVICES](state, currentDevices) { 18 | state.currentDevices = currentDevices.slice() 19 | }, 20 | } 21 | 22 | export default { 23 | state, 24 | actions, 25 | mutations, 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/apps/devices/views/DeviceCapabilityData.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 45 | -------------------------------------------------------------------------------- /ui/src/apps/devices/views/DeviceDetailsCharts.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 50 | -------------------------------------------------------------------------------- /ui/src/apps/devices/views/DeviceDetailsEvents.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 45 | -------------------------------------------------------------------------------- /ui/src/apps/devices/views/GatewayDetailsConnect.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 56 | -------------------------------------------------------------------------------- /ui/src/apps/devices/views/GatewayDetailsDevicesData.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 52 | -------------------------------------------------------------------------------- /ui/src/apps/devices/views/GroupDetailsDeviceData.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 49 | -------------------------------------------------------------------------------- /ui/src/apps/lang.js: -------------------------------------------------------------------------------- 1 | import MicroApps from '../assets/micro.apps.json' 2 | import PrivateApps from '../assets/micro.private.json' 3 | 4 | const appsZhLang = {} 5 | const appsEnLang = {} 6 | 7 | MicroApps.forEach((app) => { 8 | // eslint-disable-next-line 9 | const enLang = require(`./${app}/lang/en`) 10 | // eslint-disable-next-line 11 | const zhLang = require(`./${app}/lang/zh_CN`) 12 | Object.assign(appsEnLang, enLang.default) 13 | Object.assign(appsZhLang, zhLang.default) 14 | }) 15 | 16 | PrivateApps.forEach((app) => { 17 | // eslint-disable-next-line 18 | const context = require.context('../', true, /\private_apps/) 19 | 20 | const enLang = context(`./private_apps/${app}/lang/en.js`) 21 | if (enLang.default) { 22 | Object.assign(appsEnLang, enLang.default) 23 | } 24 | const zhLang = context(`./private_apps/${app}/lang/zh_CN.js`) 25 | if (zhLang.default) { 26 | Object.assign(appsZhLang, zhLang.default) 27 | } 28 | }) 29 | 30 | export { appsEnLang, appsZhLang } 31 | -------------------------------------------------------------------------------- /ui/src/apps/products/assets/images/noData.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/products/assets/images/noData.png -------------------------------------------------------------------------------- /ui/src/apps/products/components/ProductDetailTabs.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 53 | -------------------------------------------------------------------------------- /ui/src/apps/products/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { path: 'products', component: () => import('./views/Products') }, 3 | { path: 'products/:id', component: () => import('./views/ProductDetails') }, 4 | { path: 'products/:id/devices', component: () => import('./views/ProductDevices') }, 5 | { path: 'products/:id/definition', component: () => import('./views/ProductDefinition') }, 6 | { path: 'products/:id/definition/:streamID', component: () => import('./views/DataStreamDetails') }, 7 | { path: 'products/:id/codec', component: () => import('./views/ProductCodec.vue') }, 8 | { path: 'codec', component: () => import('./views/ProfileReviews.vue') }, 9 | ] 10 | 11 | export default routes 12 | -------------------------------------------------------------------------------- /ui/src/apps/products/store.js: -------------------------------------------------------------------------------- 1 | const currentProducts = JSON.parse(localStorage.getItem('currentProducts')) || [] 2 | 3 | const state = { 4 | currentProducts, 5 | } 6 | 7 | const STORE_PRODUCTS = 'STORE_PRODUCTS' 8 | 9 | const actions = { 10 | [STORE_PRODUCTS]({ commit }, payload) { 11 | localStorage.setItem('currentProducts', JSON.stringify(payload.currentProducts)) 12 | commit(STORE_PRODUCTS, payload.currentProducts) 13 | }, 14 | } 15 | 16 | const mutations = { 17 | [STORE_PRODUCTS](state, currentProducts) { 18 | state.currentProducts = currentProducts.slice() // Array assignment to state needs to return a new array assignment using slice 19 | }, 20 | } 21 | 22 | export default { 23 | state, 24 | actions, 25 | mutations, 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/apps/routes.js: -------------------------------------------------------------------------------- 1 | import MicroApps from '../assets/micro.apps.json' 2 | import PrivateApps from '../assets/micro.private.json' 3 | import forbidden from '@/components/403' 4 | 5 | const authRoutes = [ 6 | { path: 'forbidden', component: forbidden }, 7 | ] 8 | 9 | MicroApps.forEach((app) => { 10 | // eslint-disable-next-line 11 | const router = require(`./${app}/routes`) 12 | authRoutes.push(...router.default) 13 | }) 14 | 15 | PrivateApps.forEach((app) => { 16 | // eslint-disable-next-line 17 | const context = require.context('../', true, /\private_apps/) 18 | const router = context(`./private_apps/${app}/routes.js`) 19 | authRoutes.push(...router.default) 20 | }) 21 | 22 | export default authRoutes 23 | -------------------------------------------------------------------------------- /ui/src/apps/rules/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { path: '/business_rules', component: () => import('./views/BusinessRules') }, 3 | { path: '/business_rules/:id', component: () => import('./views/BusinessRuleDetails') }, 4 | { path: '/scope_rules', component: () => import('./views/ScopeRules') }, 5 | { path: '/scope_rules/:id', component: () => import('./views/ScopeRuleDetails') }, 6 | { path: '/actions', component: () => import('./views/Actions') }, 7 | { path: '/actions/:id', component: () => import('./views/ActionDetails') }, 8 | { path: '/timer_publish', component: () => import('./views/TimerPublish') }, 9 | { path: '/timer_publish/:id', component: () => import('./views/TimerPublishDetails') }, 10 | ] 11 | 12 | export default routes 13 | -------------------------------------------------------------------------------- /ui/src/apps/rules/store.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /ui/src/apps/rules/views/Actions.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 48 | -------------------------------------------------------------------------------- /ui/src/apps/store.js: -------------------------------------------------------------------------------- 1 | import MicroApps from '../assets/micro.apps.json' 2 | import PrivateApps from '../assets/micro.private.json' 3 | 4 | const stores = {} 5 | 6 | // Capitalization 7 | const lowerCamelCase = (str) => { 8 | const reg = /-(\w)/g 9 | return str.replace(reg, ($0, $1) => $1.toUpperCase()) 10 | } 11 | 12 | // Add the attribute for store 13 | const storeAssign = (app, store) => { 14 | const appName = lowerCamelCase(app) 15 | const hasStore = store.default && JSON.stringify(store.default) !== '{}' 16 | if (hasStore) { 17 | stores[appName] = store.default 18 | } 19 | } 20 | 21 | MicroApps.forEach((app) => { 22 | // eslint-disable-next-line 23 | const store = require(`./${app}/store`) 24 | storeAssign(app, store) 25 | }) 26 | 27 | PrivateApps.forEach((app) => { 28 | // eslint-disable-next-line 29 | const context = require.context('../', true, /\private_apps/) 30 | const store = context(`./private_apps/${app}/store.js`) 31 | storeAssign(app, store) 32 | }) 33 | 34 | export default stores 35 | -------------------------------------------------------------------------------- /ui/src/apps/system/lang/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | systems: { 3 | systemInfo: 'System', 4 | mqttTCP: 'MQTT server address (TCP)', 5 | mqttTCPRequired: 'MQTT server address (TCP) is required', 6 | mqttTLS: 'MQTT server address (TLS)', 7 | mqttTLSRequired: 'MQTT server address (TLS) is required', 8 | mqttCert: 'MQTT server address (Cert)', 9 | mqttCertRequired: 'MQTT server address (Cert) is required', 10 | CoAPUDP: 'CoAP server address (UDP)', 11 | CoAPUDPRequired: 'CoAP server address (UDP) is required', 12 | CoAPDTLS: 'CoAP server address (DTLS)', 13 | CoAPDTLSRequired: 'CoAP server address (DTLS) is required', 14 | CoAPCert: 'CoAP server address (Cert)', 15 | CoAPCertRequired: 'CoAP server address (Cert) is required', 16 | webSocketTCP: 'WebSocket server address (TCP)', 17 | webSocketTCPRequired: 'WebSocket server address (TCP) is required', 18 | webSocketCert: 'WebSocket server address (Cert)', 19 | webSocketCertRequired: 'WebSocket server address (Cert) is required', 20 | }, 21 | logos: { 22 | logoInfo: 'Logo', 23 | webIcon: 'Website Icon (recommended 48 * 48)', 24 | webLogoLight: 'Website Logo (light theme use, recommended 360 * 72)', 25 | webLogoDark: 'Website Logo (dark theme use, recommended 360 * 72)', 26 | loginLogo: 'Register and login Logo (recommended 416 * 550)', 27 | setSuccess: 'Setting Success, Refreshing...', 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/apps/system/lang/zh_CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | systems: { 3 | systemInfo: '系统信息', 4 | mqttTCP: 'MQTT 服务器地址(TCP)', 5 | mqttTCPRequired: '请输入 MQTT 服务器地址(TCP)', 6 | mqttTLS: 'MQTT 服务器地址(TLS)', 7 | mqttTLSRequired: '请输入 MQTT 服务器地址(TLS)', 8 | mqttCert: 'MQTT 服务器地址(证书)', 9 | mqttCertRequired: '请输入 MQTT 服务器地址(证书)', 10 | CoAPUDP: 'CoAP 服务器地址(UDP)', 11 | CoAPUDPRequired: '请输入 CoAP 服务器地址(UDP)', 12 | CoAPDTLS: 'CoAP 服务器地址(DTLS)', 13 | CoAPDTLSRequired: '请输入 CoAP 服务器地址(DTLS)', 14 | CoAPCert: 'CoAP 服务器地址(证书)', 15 | CoAPCertRequired: '请输入 CoAP 服务器地址(证书)', 16 | webSocketTCP: 'WebSocket 服务器地址(TCP)', 17 | webSocketTCPRequired: '请输入 WebSocket 服务器地址(TCP)', 18 | webSocketCert: 'WebSocket 服务器地址(证书)', 19 | webSocketCertRequired: '请输入 WebSocket 服务器地址(证书)', 20 | }, 21 | logos: { 22 | logoInfo: '图标信息', 23 | webIcon: '网站Icon(推荐48 * 48)', 24 | webLogoLight: '网站Logo (浅色主题使用,推荐360 * 72)', 25 | webLogoDark: '网站Logo (深色主题使用,推荐360 * 72)', 26 | loginLogo: '注册登录Logo (推荐416 * 550)', 27 | setSuccess: '设置成功!正在刷新浏览器...', 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/apps/system/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { 3 | path: 'system_info', 4 | component: () => import('./views/SystemInfo'), 5 | meta: { requiresAdmin: true }, 6 | }, 7 | { 8 | path: 'logo_info', 9 | component: () => import('./views/Logo'), 10 | meta: { requiresAdmin: true }, 11 | }, 12 | ] 13 | 14 | export default routes 15 | -------------------------------------------------------------------------------- /ui/src/apps/system/store.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/system/store.js -------------------------------------------------------------------------------- /ui/src/apps/test-center/lang/zh_CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | testCenter: { 3 | reportData: '上报数据', 4 | reportedData: '已上报数据', 5 | receivedData: '已接收数据', 6 | message: '消息', 7 | subscribe: '订阅', 8 | subscribeTopic: '订阅主题', 9 | topicPlaceholder: '请输入想要订阅的主题', 10 | CoAPClient: 'CoAP 客户端', 11 | searchDevice: '请输入待测试设备名检索', 12 | connectSuccess: '连接成功!', 13 | connectFail: '连接失败!', 14 | connectError: '连接错误!', 15 | reportSuccess: '数据上报成功!', 16 | reportFail: '连接错误,数据上报失败!', 17 | selectDevice: '请先选择设备!', 18 | topicLimit: '最多只可订阅10个主题!', 19 | subscribeError: '订阅失败', 20 | subscribeSuccess: '订阅成功!', 21 | subscribeFail: '连接错误,主题订阅失败!', 22 | MQTTClient: 'MQTT 客户端', 23 | searchDeviceGateway: '输入设备/网关名称搜索', 24 | connecting: '启动中', 25 | connect: '启动设备', 26 | disconnecting: '断开中', 27 | disconnect: '断开设备', 28 | host: '服务器:', 29 | username: '用户名:', 30 | clientId: '客户端ID:', 31 | password: '密码:', 32 | qos: '服务质量', 33 | unsubscribe: '取消订阅?', 34 | illegalError: '向非法主题发布消息,连接已断开!', 35 | disconnected: '已断开连接!', 36 | operFail: '操作失败!', 37 | connectFirst: '请先连接!', 38 | subscribeRepeat: '已订阅该主题,无需重复订阅', 39 | unsubscribeFail: '取消订阅失败', 40 | unsubscribeSuccess: '取消订阅成功', 41 | publishSuccess: '发布成功!', 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /ui/src/apps/test-center/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { path: '/mqtt_client', component: () => import('./views/MQTTClient') }, 3 | { path: '/coap_client', component: () => import('./views/CoAP') }, 4 | ] 5 | 6 | export default routes 7 | -------------------------------------------------------------------------------- /ui/src/apps/test-center/store.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/apps/test-center/store.js -------------------------------------------------------------------------------- /ui/src/assets/images/403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/assets/images/403.png -------------------------------------------------------------------------------- /ui/src/assets/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/assets/images/404.png -------------------------------------------------------------------------------- /ui/src/assets/images/created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/assets/images/created.png -------------------------------------------------------------------------------- /ui/src/assets/images/empty-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/assets/images/empty-page.png -------------------------------------------------------------------------------- /ui/src/assets/images/logo-darkTheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/assets/images/logo-darkTheme.png -------------------------------------------------------------------------------- /ui/src/assets/images/select-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorcloud/ActorCloud/9c34b371c23464981323ef9865d9913bde1fe09c/ui/src/assets/images/select-file.png -------------------------------------------------------------------------------- /ui/src/assets/scss/element-variables.scss: -------------------------------------------------------------------------------- 1 | /* Change element-ui theme variable */ 2 | $--color-primary: #34C388; 3 | $--color-success: #34C388; 4 | 5 | /* Change icon font path,required */ 6 | $--font-path: '~element-ui/lib/theme-chalk/fonts'; 7 | 8 | @import "~element-ui/packages/theme-chalk/src/index"; 9 | -------------------------------------------------------------------------------- /ui/src/assets/scss/emqCardList.scss: -------------------------------------------------------------------------------- 1 | .emq-card-list-view { 2 | .box-card { 3 | height: 137px; 4 | transition: .5s; 5 | margin-bottom: 20px; 6 | border-radius: 6px; 7 | box-shadow: 0 2px 12px 1px var(--color-shadow); 8 | &:hover { 9 | box-shadow: 0 2px 15px 14px var(--color-shadow); 10 | } 11 | cursor: pointer; 12 | .el-card__header { 13 | font-size: 14px; 14 | font-weight: 500; 15 | color: var(--color-text-lighter) 16 | } 17 | .el-card__body { 18 | font-size: 14px; 19 | color: var(--color-text-light); 20 | .text { 21 | height: 40px; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | display: -webkit-box; 25 | -webkit-line-clamp: 2; 26 | -webkit-box-orient: vertical; 27 | } 28 | } 29 | .card-dropdown { 30 | .el-button { 31 | padding: 0; 32 | color: var(--color-text-lighter); 33 | } 34 | float: right; 35 | i { 36 | cursor: pointer; 37 | margin-right: -10px; 38 | } 39 | } 40 | } 41 | } 42 | .el-dropdown-menu { 43 | .popper__arrow { 44 | display: none; 45 | } 46 | .el-dropdown-menu__item { 47 | color: var(--color-text-light); 48 | &:not(.is-disabled):hover { 49 | background-color: transparent; 50 | color: var(--color-main-green); 51 | } 52 | img { 53 | width: 20px; 54 | position: relative; 55 | top: 4px; 56 | right: 5px; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ui/src/assets/scss/themes/dark.scss: -------------------------------------------------------------------------------- 1 | // Dark theme,use css variable 2 | 3 | .dark-theme { 4 | // Text 5 | --color-text-default: #696F7B; 6 | --color-text-light: #9EA3B1; 7 | --color-text-lighter: #CED4E2; 8 | // Accent color 9 | --color-main-green: #34C388; 10 | --color-main-pink: #E54C95; 11 | --color-main-purple: #B24DE4; 12 | --color-main-yellow: #F2AF3D; 13 | --color-minor-green: #DAEBE9; 14 | // Alert status 15 | --alert-urgent-color: #d31f1f; 16 | --alert-main-color: #f1693c; 17 | --alert-risk-color: #f19f39; 18 | --alert-waring-color: #e3c32b; 19 | // Background、shadow 20 | --color-bg-gray: #2B2F3A; 21 | --color-bg-tag: #474B57; 22 | --color-bg-card: #333844; 23 | --color-bg-table-head: #333844; 24 | --color-bg-table-expand: #454A58; 25 | --color-shadow: #272A31; 26 | // Line 27 | --color-line-bg: #505664; 28 | --color-line-card: #404550; 29 | 30 | // Other 31 | --color-bg-hover: #3D4350; 32 | // Topbar 33 | --color-topbar-bg: #333844; 34 | --color-topbar-input-bg: rgba(105, 111, 123, 0.14); 35 | --color-topbar-text: #9EA3B1; 36 | // Homepage dashboard 37 | --color-bg-icon: rgba(34, 187, 122, 0.08); 38 | // Overlay background color 39 | --color-bg-upper: rgba(255, 255, 255, 0.05); 40 | // Loading 41 | --color-bg-loading: #333844d4; 42 | // Input 43 | --color-input-bg: #454A58; 44 | --color-input-disabled-bg: #333844; 45 | // Checkbox 46 | --color-checkbox-inner: #949AA8; 47 | // Mall-icon 48 | --color-product-mall-icon-bg: #3D4350; 49 | 50 | // Charts(echarts plugin) 51 | --color-echarts-line-axis_line: #666c7a; 52 | --color-echarts-line-axis_label: #666c7a; 53 | --color-echarts-line-split_line: #3b404b; 54 | } 55 | -------------------------------------------------------------------------------- /ui/src/assets/scss/themes/light.scss: -------------------------------------------------------------------------------- 1 | // Light theme,use css variable 2 | 3 | .light-theme { 4 | // Text 5 | --color-text-default: #B6BAC5; 6 | --color-text-light: #A0A3AE; 7 | --color-text-lighter: #646775; 8 | // Accent color 9 | --color-main-green: #34C388; 10 | --color-main-pink: #E86AA6; 11 | --color-main-purple: #A57EBC; 12 | --color-main-yellow: #F2AF3D; 13 | --color-minor-green: #DAEBE9; 14 | // Alert status 15 | --alert-urgent-color: #d31f1f; 16 | --alert-main-color: #f1693c; 17 | --alert-risk-color: #f19f39; 18 | --alert-waring-color: #e3c32b; 19 | // Background、shadow 20 | --color-bg-gray: #F0F1F7; 21 | --color-bg-tag: #D7DAE1; 22 | --color-bg-card: #FFFFFF; 23 | --color-bg-table-head: #FCFDFF; 24 | --color-bg-table-expand: #F8F9FC; 25 | --color-shadow: #E6E8F1; 26 | // Line 27 | --color-line-bg: #E3E4E9; 28 | --color-line-card: #F1F1F6; 29 | 30 | // Other 31 | --color-bg-hover: #F6F7FB; 32 | // Topbar 33 | --color-topbar-bg: #23c88e; 34 | --color-topbar-input-bg: rgba(255, 255, 255, 0.14); 35 | --color-topbar-text: #FFFFFF; 36 | // Homepage dashboard 37 | --color-bg-icon: #f6f7fb; 38 | // Overlay background color for date selector to select time interval background 39 | --color-bg-upper: #f2f6fc; 40 | // Loading 41 | --color-bg-loading: #fff9; 42 | // Input 43 | --color-input-bg: #fff; 44 | --color-input-disabled-bg: #f5f7fa; 45 | // Checkbox 46 | --color-checkbox-inner: #dcdfe6; 47 | // mall-icon 48 | --color-product-mall-icon-bg: #F0F1F5; 49 | 50 | // charts(echarts plugin) 51 | --color-echarts-line-axis_line: #a8a8a8; 52 | --color-echarts-line-axis_label: #a8a8a8; 53 | --color-echarts-line-split_line: #f1f2f3; 54 | } 55 | -------------------------------------------------------------------------------- /ui/src/assets/scss/variable.scss: -------------------------------------------------------------------------------- 1 | $topbar-height: 80px; 2 | $topbar-height-small: 56px; 3 | 4 | $topbar-font-size: 18px; 5 | $topbar-font-size-small: 14px; 6 | 7 | $leftbar-width: 100px; 8 | $leftbar-width-small: 80px; 9 | -------------------------------------------------------------------------------- /ui/src/bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default new Vue() 4 | -------------------------------------------------------------------------------- /ui/src/components/403.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 14 | 15 | 16 | 37 | -------------------------------------------------------------------------------- /ui/src/components/404.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 15 | 16 | 17 | 49 | -------------------------------------------------------------------------------- /ui/src/components/EditToggleButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 43 | -------------------------------------------------------------------------------- /ui/src/components/EmptyPage.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 34 | 35 | 51 | -------------------------------------------------------------------------------- /ui/src/components/EmqDetailsPageHead.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 22 | 23 | 24 | 58 | -------------------------------------------------------------------------------- /ui/src/components/EmqTag.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 17 | 18 | 19 | 54 | -------------------------------------------------------------------------------- /ui/src/components/RightbarPop.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 14 | 15 | 16 | 34 | -------------------------------------------------------------------------------- /ui/src/components/TabsCardHead.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 40 | 41 | 42 | 67 | -------------------------------------------------------------------------------- /ui/src/components/TempPage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /ui/src/components/charts/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { ResizeSensor } from 'css-element-queries' 2 | 3 | 4 | export default { 5 | watch: { 6 | leftbar() { 7 | try { 8 | new ResizeSensor(document.querySelector('.leftbar'), () => { 9 | this.chart.resize() 10 | })() 11 | } catch (e) { 12 | return 0 13 | } 14 | }, 15 | }, 16 | 17 | mounted() { 18 | setTimeout(() => { 19 | window.addEventListener('resize', this.chart.resize) 20 | }, 200) 21 | }, 22 | beforeDestroy() { 23 | window.removeEventListener('resize', this.chart.resize) 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/filters.js: -------------------------------------------------------------------------------- 1 | import lang from '@/lang' 2 | import store from '@/store' 3 | 4 | const dateformat = require('dateformat') 5 | 6 | function pluralize(time, label) { 7 | if (time === 1) { 8 | return time + label 9 | } 10 | return `${time} ${label} s` 11 | } 12 | 13 | export function timeAgo(time) { 14 | const between = (Date.now() / 1000) - Number(time) 15 | if (between < 3600) { 16 | return pluralize(Math.floor(between / 60), ' minute') 17 | } 18 | if (between < 86400) { 19 | return pluralize(Math.floor(between / 3600), ' hour') 20 | } 21 | return pluralize(Math.floor(between / 86400), ' day') 22 | } 23 | 24 | export function dateFormat(date, format) { 25 | if (!date) { 26 | return false 27 | } 28 | return dateformat(date, format || 'yyyy-mm-dd HH:MM:ss') 29 | } 30 | 31 | // Translation system default role name 32 | export const convertRoleName = (name) => { 33 | const locale = store.state.accounts.lang 34 | const $t = lang[locale] 35 | const roleNameDict = [ 36 | 'super_admin_role', 37 | 'company_role', 38 | 'personal_role', 39 | 'system_user_role', 40 | 'common_user_role', 41 | 'device_user_role', 42 | 'admin_app_role', 43 | 'display_app_role', 44 | 'device_app_role', 45 | ] 46 | return roleNameDict.includes(name) ? $t.roles[name] : name 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/lang/index.js: -------------------------------------------------------------------------------- 1 | import enLocale from 'element-ui/lib/locale/lang/en' 2 | import zhLocale from 'element-ui/lib/locale/lang/zh-CN' 3 | 4 | import commonEnLang from '@/lang/en' 5 | import commonZhLang from '@/lang/zh_CN' 6 | import { layoutEnLang, layoutZhLang } from '@/layout/lang' 7 | import { appsEnLang, appsZhLang } from '@/apps/lang' 8 | 9 | export default { 10 | en: { ...enLocale, ...commonEnLang, ...appsEnLang, ...layoutEnLang }, 11 | zh: { ...zhLocale, ...commonZhLang, ...appsZhLang, ...layoutZhLang }, 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/layout/lang/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | topBar: { 3 | searchPlaceholder: 'Search by device/gateway name', 4 | productMall: 'Product Mall', 5 | document: 'Document', 6 | companyInfo: 'Company Info', 7 | companyName: 'Company', 8 | contactEmail: 'Contact Email', 9 | contactPerson: 'Contact Person', 10 | contactPhone: 'Phone', 11 | logoLight: 'Logo (for light theme, recommended size 360 * 72)', 12 | logoDark: 'Logo (for dark theme, recommended size 360 * 72)', 13 | emailIllegal: 'Email Illegal', 14 | emailRequired: 'Email is required', 15 | changePassword: 'Change Password', 16 | oldPassword: 'Old Password', 17 | newPassword: 'New Password', 18 | confirmPassword: 'Confirm Password', 19 | confirmPasswordRequired: 'Confirm Password is required', 20 | oldPasswordRequired: 'Old Password is required', 21 | newPasswordRequired: 'New Password is required', 22 | passwordLength: 'The password length must be more than 6', 23 | passwordInconsistent: 'Password Inconsistent', 24 | editSuccess: 'Edit Success', 25 | switchTheme: 'Appearance', 26 | lightTheme: 'Light', 27 | darkTheme: 'Dark', 28 | switchLanguage: 'Language', 29 | en: 'English', 30 | zh: 'Chinese', 31 | logout: 'Logout', 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/layout/lang/index.js: -------------------------------------------------------------------------------- 1 | import layoutEnLang from './en' 2 | import layoutZhLang from './zh_CN' 3 | 4 | export { layoutEnLang, layoutZhLang } 5 | -------------------------------------------------------------------------------- /ui/src/layout/lang/zh_CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | topBar: { 3 | searchPlaceholder: '设备/网关 名称搜索', 4 | productMall: '产品商城', 5 | document: '文 档', 6 | companyInfo: '公司信息', 7 | companyName: '公司名称', 8 | contactEmail: '联系邮箱', 9 | contactPerson: '联系人', 10 | contactPhone: '联系电话', 11 | logoLight: '网站Logo (浅色主题使用,推荐360 * 72)', 12 | logoDark: '网站Logo (深色主题使用,推荐360 * 72)', 13 | emailIllegal: '请输入正确的邮箱地址', 14 | emailRequired: '请输入邮箱', 15 | changePassword: '修改密码', 16 | oldPassword: '旧密码', 17 | newPassword: '新密码', 18 | confirmPassword: '确认密码', 19 | confirmPasswordRequired: '请输入确认密码', 20 | oldPasswordRequired: '请输入旧密码', 21 | newPasswordRequired: '请输入新密码', 22 | passwordLength: '密码长度必须为6位以上字符', 23 | passwordInconsistent: '前后密码不一致', 24 | editSuccess: '修改成功', 25 | switchTheme: '切换主题', 26 | lightTheme: '浅色主题', 27 | darkTheme: '深色主题', 28 | switchLanguage: '切换语言', 29 | en: '英文', 30 | zh: '中文', 31 | logout: '登出', 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/layout/views/Home.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 44 | 45 | 46 | 65 | -------------------------------------------------------------------------------- /ui/src/mixins/loadDictCode.js: -------------------------------------------------------------------------------- 1 | import { mapActions } from 'vuex' 2 | 3 | export default { 4 | computed: { 5 | dictCode() { 6 | return this.$store.state.accounts.dictCode 7 | }, 8 | }, 9 | methods: { 10 | ...mapActions(['GET_DICT_CODE']), 11 | loadData() {}, 12 | }, 13 | // Dict is loaded from the serve when it is not found in the vuex 14 | created() { 15 | if (this.dictCode && this.dictCode[this.field.key]) { 16 | this.loadData() 17 | } else if (!this.field.url && !this.field.options) { 18 | // Remote load and fixed options select does not trigger a reload 19 | this.GET_DICT_CODE().then(() => { 20 | this.loadData() 21 | }) 22 | } else { 23 | this.loadData() 24 | } 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import store from '@/store' 5 | import authRoutes from '@/apps/routes' 6 | 7 | const routes = [ 8 | { path: '/login', component: () => import('@/apps/accounts/views/Login'), meta: { requiresAuth: false } }, 9 | { path: '/signup', component: () => import('@/apps/accounts/views/Signup'), meta: { requiresAuth: false } }, 10 | { path: '/', component: () => import('@/layout/views/Home'), children: authRoutes }, 11 | { path: '*', component: () => import('@/components/404'), meta: { requiresAuth: false } }, 12 | { path: '/temp_page', component: () => import('@/components/TempPage'), meta: { requiresAuth: false } }, 13 | ] 14 | 15 | Vue.use(VueRouter) 16 | 17 | const router = new VueRouter({ 18 | mode: 'history', 19 | routes, 20 | }) 21 | 22 | router.beforeEach((to, from, next) => { 23 | const { requiresAuth = true, requiresAdmin = false } = to.meta 24 | document.body.scrollTop = 0 25 | document.documentElement.scrollTop = 0 26 | if (requiresAuth) { 27 | if (!store.state.accounts.user.token) { 28 | next({ 29 | path: '/login', 30 | query: { redirect: to.fullPath }, 31 | }) 32 | } else if (requiresAdmin && store.state.accounts.user.tenantType !== 0) { 33 | next('/forbidden') 34 | } else { 35 | next() 36 | } 37 | } else { 38 | next() 39 | } 40 | }) 41 | 42 | export default router 43 | -------------------------------------------------------------------------------- /ui/src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import createMutationsSharer from 'vuex-shared-mutations' 4 | 5 | import storeModules from '@/apps/store' 6 | 7 | Vue.use(Vuex) 8 | 9 | export default new Vuex.Store({ 10 | strict: process.env.NODE_ENV !== 'production', 11 | modules: storeModules, 12 | // Sync the state of tabs and windows 13 | plugins: [createMutationsSharer({ predicate: ['STORE_DEVICES', 'STORE_PRODUCTS'] })], 14 | }) 15 | -------------------------------------------------------------------------------- /ui/src/utils/MQTTConnect.js: -------------------------------------------------------------------------------- 1 | export const MQTTClient = { 2 | client: {}, 3 | options: { 4 | host: '127.0.0.1', 5 | port: '8083', 6 | username: '', 7 | password: '', 8 | keepalive: 60, 9 | clean: true, 10 | clientId: `mqttjs_${Math.random().toString(16).substr(2, 10)}`, 11 | subTopic: '/World', 12 | subQos: 0, 13 | publishTopic: '/World', 14 | publishQos: 0, 15 | publishMessage: 'Hello world!', 16 | publishRetain: false, 17 | receivedMessages: [], 18 | publishedMessages: [], 19 | subscriptions: [], 20 | connectPartCtl: false, 21 | activeStatus: 'addtopic', 22 | }, 23 | } 24 | 25 | export const virtualDevice = { 26 | options: { 27 | connect: {}, 28 | published: false, 29 | subscribe: {}, 30 | publish: {}, 31 | receivedMessages: [], 32 | publishedMessages: [], 33 | subscriptions: [], 34 | options: [], 35 | selectedDeviceID: undefined, 36 | timer: 0, 37 | broker: {}, 38 | }, 39 | client: {}, 40 | } 41 | 42 | export const socketClient = { 43 | client: {}, 44 | } 45 | -------------------------------------------------------------------------------- /ui/src/utils/chartUtils.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | 3 | export function hex2rgba(hex, alpha) { 4 | const BASE = 16 5 | const HEX_REGEX = /^#?[a-fA-F0-9]+$/ 6 | const HEX_SHORTHAND_LENGTH = 3 7 | const HEX_LENGTH = 6 8 | 9 | if (!HEX_REGEX.test(hex)) { 10 | throw Error('invalid hexadecimal') 11 | } 12 | 13 | if (hex[0] === '#') { 14 | hex = hex.slice(1) 15 | } 16 | 17 | if (hex.length === HEX_SHORTHAND_LENGTH) { 18 | hex = hex.split('') 19 | hex.splice(2, 0, hex[2]) 20 | hex.splice(1, 0, hex[1]) 21 | hex.splice(0, 0, hex[0]) 22 | hex = hex.join('') 23 | } 24 | 25 | if (hex.length !== HEX_LENGTH) { 26 | throw Error('invalid hexadecimal length') 27 | } 28 | 29 | const values = [ 30 | parseInt(hex.slice(0, 2), BASE), 31 | parseInt(hex.slice(2, 4), BASE), 32 | parseInt(hex.slice(4, 6), BASE), 33 | ] 34 | 35 | alpha = typeof alpha === 'number' ? alpha : parseFloat(alpha) 36 | if (alpha >= 0 && alpha <= 1) { 37 | values.push(alpha) 38 | } else { 39 | values.push(1) 40 | } 41 | 42 | return `rgba( ${values.join(',')} )` 43 | } 44 | -------------------------------------------------------------------------------- /ui/src/utils/installer.js: -------------------------------------------------------------------------------- 1 | import EmqSelect from '@/components/EmqSelect' 2 | import EmqButton from '@/components/EmqButton' 3 | import TabsCardHead from '@/components/TabsCardHead' 4 | import EmqDetailsPageHead from '@/components/EmqDetailsPageHead' 5 | import EditToggleButton from '@/components/EditToggleButton' 6 | import variable from './variable' 7 | 8 | // Function install 9 | export default (Vue) => { 10 | Vue.component(EmqSelect.name, EmqSelect) 11 | Vue.component(EmqButton.name, EmqButton) 12 | Vue.component(TabsCardHead.name, TabsCardHead) 13 | Vue.component(EmqDetailsPageHead.name, EmqDetailsPageHead) 14 | Vue.component(EditToggleButton.name, EditToggleButton) 15 | Vue.prototype.$variable = variable 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/utils/time.js: -------------------------------------------------------------------------------- 1 | import dateformat from 'dateformat' 2 | 3 | const getNowTimetamp = (type) => { 4 | const date = new Date() 5 | const formatBySecond = (date) => { 6 | const tmp = Date.parse(date).toString().substr(0, 10) 7 | return parseInt(tmp, 10) 8 | } 9 | if (type === 'second') { 10 | return formatBySecond(date) 11 | } 12 | return date.getTime() 13 | } 14 | 15 | const getNowDate = (format = 'yyyy-mm-dd HH:MM:ss') => { 16 | return dateformat(new Date(), format) 17 | } 18 | 19 | export { getNowTimetamp, getNowDate } 20 | -------------------------------------------------------------------------------- /ui/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /ui/tests/unit/components/TabsCardHead.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { expect } from 'chai' 3 | import { shallowMount, createLocalVue } from '@vue/test-utils' 4 | import VueRouter from 'vue-router' 5 | import TabsCardHead from '@/components/TabsCardHead' 6 | 7 | const localVue = createLocalVue() 8 | localVue.use(VueRouter) 9 | const router = new VueRouter() 10 | 11 | describe('TabsCardHead.vue', () => { 12 | const wrapper = shallowMount(TabsCardHead, { 13 | router, 14 | mocks: { 15 | $route: { 16 | path: '/', 17 | }, 18 | $t: () => { } 19 | }, 20 | propsData: { 21 | tabs: [ 22 | { code: 'test1', url: '/' }, 23 | { code: 'test2', url: '/test2' }, 24 | { code: 'test3', url: '/test3' } 25 | ] 26 | } 27 | }) 28 | it('能正确渲染 tab 的数量', () => { 29 | expect(wrapper.findAll('.crud-title').length).to.equal(3) 30 | }) 31 | it('能正确显示 tab 的高亮', () => { 32 | // $route.path: '/' === tabs.url: '/' 33 | expect(wrapper.findAll('.crud-title').at(0).classes()).contains('active') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /ui/vue.config.js: -------------------------------------------------------------------------------- 1 | const backendUrl = process.env.VUE_APP_SERVER || '' 2 | const isProduction = process.env.NODE_ENV === 'production' 3 | 4 | module.exports = { 5 | devServer: { 6 | port: 9999, 7 | proxy: { 8 | '/api/v1': { 9 | target: backendUrl, 10 | changeOrigin: true, 11 | }, 12 | }, 13 | }, 14 | chainWebpack: config => { 15 | if (isProduction) { 16 | config.optimization.minimize(true) 17 | config.optimization.splitChunks({ 18 | chunks: 'all' 19 | }) 20 | } 21 | }, 22 | // Disabled the production environment generates a sourceMap file 23 | productionSourceMap: false, 24 | css: { 25 | // Enable to use the css separation plugin ExtractTextPlugin 26 | extract: true, 27 | // Disable CSS source maps 28 | sourceMap: false, 29 | // Disabled CSS modules for all css / pre-processor files. 30 | modules: false, 31 | }, 32 | } 33 | --------------------------------------------------------------------------------