├── backend ├── __init__.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-38.pyc │ │ └── 0001_initial.cpython-38.pyc │ └── 0001_initial.py ├── README.md ├── apps.py ├── static │ ├── image │ │ └── bg.jpg │ └── css │ │ ├── login.css │ │ └── register.css ├── __pycache__ │ ├── admin.cpython-38.pyc │ ├── apps.cpython-38.pyc │ ├── forms.cpython-38.pyc │ ├── tests.cpython-38.pyc │ ├── views.cpython-38.pyc │ ├── models.cpython-38.pyc │ └── __init__.cpython-38.pyc ├── templates │ └── backend │ │ ├── tenantview.html │ │ ├── acadminview.html │ │ ├── managerview.html │ │ ├── waiterview.html │ │ ├── index.html │ │ ├── login.html │ │ └── register.html ├── const.json ├── admin.py ├── forms.py ├── views.py ├── tests.py └── models.py ├── frontendDevServer ├── server │ ├── __init__.py │ ├── websocket │ │ ├── __init__.py │ │ ├── routing.py │ │ ├── msg.py │ │ ├── databasectrl.py │ │ └── consumers.py │ ├── apps.py │ ├── admin.py │ ├── urls.py │ ├── serializers.py │ ├── models.py │ └── views.py ├── roomMonitor │ ├── __init__.py │ ├── asgi.py │ ├── routing.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── .gitignore ├── readme.md ├── manage.py └── requirements.txt ├── AirConditioningManagementSystem ├── __init__.py ├── __pycache__ │ ├── urls.cpython-38.pyc │ ├── wsgi.cpython-38.pyc │ ├── __init__.cpython-38.pyc │ └── settings.cpython-38.pyc ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── .DS_Store ├── .vscode └── settings.json ├── frontend ├── public │ ├── favicon.ico │ └── index.html ├── vue.config.js ├── api │ ├── README.md │ ├── database.js │ ├── roomAPI.js │ └── roomAPI.js.backup ├── src │ ├── assets │ │ └── logo.png │ ├── plugins │ │ └── bootstrap-vue.js │ ├── main.js │ ├── store │ │ └── index.js │ ├── components │ │ ├── Room │ │ │ └── Room.vue │ │ ├── Waiter │ │ │ ├── Waiter.vue │ │ │ └── RoomDetailForWaiter.vue │ │ ├── Manager │ │ │ ├── Manager.vue │ │ │ └── RoomDetailForManager.vue │ │ ├── Administrator │ │ │ ├── Administrator.vue │ │ │ └── RoomDetailForAdmin.vue │ │ ├── Login.vue │ │ └── Tenant │ │ │ └── Tenant.vue │ ├── router │ │ └── index.js │ └── App.vue ├── babel.config.js ├── .gitignore ├── README.md └── package.json ├── .idea ├── .gitignore ├── vcs.xml ├── dictionaries │ └── zhoufengchongyang.xml ├── modules.xml ├── misc.xml ├── dataSources.xml ├── inspectionProfiles │ └── Project_Default.xml └── AirConditioningManagementSystem.iml ├── manage.py ├── README.md └── doc ├── 分布式温控系统的解决方案.md ├── 静态结构设计-房客.md ├── 动态结构设计-房客.md ├── 分布式温控系统的需求定义及其领域模型.md └── 用例模型-房客.md /backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontendDevServer/server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AirConditioningManagementSystem/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontendDevServer/roomMonitor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontendDevServer/server/websocket/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | 个人感觉还是在前后端建立连接后初始化的时候,把const.json发给前端,这样前端就可以保存这个信息了。这些变量在重启的时候才可以被修改,女少口阿! 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/.DS_Store -------------------------------------------------------------------------------- /backend/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BackendConfig(AppConfig): 5 | name = 'backend' 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "C:\\Users\\lenovo\\AppData\\Local\\Programs\\Python\\Python38\\python.exe" 3 | } -------------------------------------------------------------------------------- /backend/static/image/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/backend/static/image/bg.jpg -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 输出目录 3 | assetsDir: 'static', 4 | // 基本路径 5 | // baseUrl: './', 6 | }; -------------------------------------------------------------------------------- /frontendDevServer/.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | .vscode 3 | client/* 4 | upload/ 5 | */__pycache__/* 6 | */migrations/* 7 | __pycache__/* 8 | -------------------------------------------------------------------------------- /frontend/api/README.md: -------------------------------------------------------------------------------- 1 | #论API的使用方法 2 | 在database.js中export的API中有两个函数 3 | * queryRoomStatus 4 | * updateRoomStatus 5 | 分别用于查询和更新单个room的温度 6 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontendDevServer/server/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ServerConfig(AppConfig): 5 | name = 'server' 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml 3 | # Datasource local storage ignored files 4 | /dataSources/ 5 | /dataSources.local.xml -------------------------------------------------------------------------------- /backend/__pycache__/admin.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/backend/__pycache__/admin.cpython-38.pyc -------------------------------------------------------------------------------- /backend/__pycache__/apps.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/backend/__pycache__/apps.cpython-38.pyc -------------------------------------------------------------------------------- /backend/__pycache__/forms.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/backend/__pycache__/forms.cpython-38.pyc -------------------------------------------------------------------------------- /backend/__pycache__/tests.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/backend/__pycache__/tests.cpython-38.pyc -------------------------------------------------------------------------------- /backend/__pycache__/views.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/backend/__pycache__/views.cpython-38.pyc -------------------------------------------------------------------------------- /backend/__pycache__/models.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/backend/__pycache__/models.cpython-38.pyc -------------------------------------------------------------------------------- /backend/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/backend/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /backend/migrations/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/backend/migrations/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /backend/migrations/__pycache__/0001_initial.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/backend/migrations/__pycache__/0001_initial.cpython-38.pyc -------------------------------------------------------------------------------- /frontendDevServer/server/websocket/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .consumers import MonitorConsumer 3 | 4 | websocket_urlpatterns=[ 5 | path('ws/monitor/',MonitorConsumer) 6 | ] -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AirConditioningManagementSystem/__pycache__/urls.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/AirConditioningManagementSystem/__pycache__/urls.cpython-38.pyc -------------------------------------------------------------------------------- /AirConditioningManagementSystem/__pycache__/wsgi.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/AirConditioningManagementSystem/__pycache__/wsgi.cpython-38.pyc -------------------------------------------------------------------------------- /AirConditioningManagementSystem/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/AirConditioningManagementSystem/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /AirConditioningManagementSystem/__pycache__/settings.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonGH0STRiley/AirConditioningManagementSystem/HEAD/AirConditioningManagementSystem/__pycache__/settings.cpython-38.pyc -------------------------------------------------------------------------------- /backend/templates/backend/tenantview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 房客 6 | 7 | 8 |

房间控制面板

9 | 10 | -------------------------------------------------------------------------------- /backend/templates/backend/acadminview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 空调管理员 6 | 7 | 8 |

这是空调管理员管理界面

9 | 10 | -------------------------------------------------------------------------------- /backend/templates/backend/managerview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 酒店经理 6 | 7 | 8 |

这是酒店经理管理界面

9 | 10 | -------------------------------------------------------------------------------- /backend/templates/backend/waiterview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 酒店前台 6 | 7 | 8 |

这是前台服务员管理界面

9 | 10 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [ 3 | [ 4 | "@vue/app", 5 | { 6 | "useBuiltIns": "entry" 7 | } 8 | ] 9 | ] 10 | }; 11 | -------------------------------------------------------------------------------- /.idea/dictionaries/zhoufengchongyang.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | acms 5 | glyphicon 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/plugins/bootstrap-vue.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import BootstrapVue from 'bootstrap-vue' 4 | import 'bootstrap/dist/css/bootstrap.min.css' 5 | import 'bootstrap-vue/dist/bootstrap-vue.css' 6 | 7 | Vue.use(BootstrapVue); 8 | -------------------------------------------------------------------------------- /backend/const.json: -------------------------------------------------------------------------------- 1 | { 2 | "RoomCapacity": { 3 | "value": 128, 4 | "type": "Number" 5 | }, 6 | "ACCapacity": { 7 | "value": 64, 8 | "type": "Number" 9 | }, 10 | "WaitTime": { 11 | "value": 30, 12 | "type": "Number" 13 | } 14 | } -------------------------------------------------------------------------------- /backend/templates/backend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 首页 6 | 7 | 8 |

{{ request.session.user_name }}! 欢迎回来!

9 |

10 | 登出 11 |

12 | 13 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /frontendDevServer/server/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Device,Software,Tracker 3 | # Register your models here. 4 | admin.site.site_header = 'ACMS 数据库管理系统' 5 | admin.site.site_title = 'ACMS 数据库管理系统' 6 | 7 | admin.site.register(Device) 8 | admin.site.register(Software) 9 | admin.site.register(Tracker) 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontendDevServer/readme.md: -------------------------------------------------------------------------------- 1 | # Frontend Development Server 2 | - server (server端代码,包括websocket和resfulapi) 3 | - frontendDevServer (django 服务器配置项) 4 | 5 | # 运行 6 | ## 安装环境 7 | pip install -r requirements.txt 8 | ## 初始化数据库 9 | pip manage.py makemigrations 10 | pip manage.py migrate 11 | 12 | ## 运行服务器 13 | pip mamage.py runserver [ip:port] 14 | 15 | ps: 运行websocket服务需要先运行redis并将端口映射至6379 16 | -------------------------------------------------------------------------------- /frontendDevServer/roomMonitor/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI entrypoint. Configures Django and then runs the application 3 | defined in the ASGI_APPLICATION setting. 4 | """ 5 | 6 | import os 7 | import django 8 | from channels.routing import get_default_application 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "roomMonitor.settings") 11 | django.setup() 12 | application = get_default_application() 13 | -------------------------------------------------------------------------------- /frontendDevServer/roomMonitor/routing.py: -------------------------------------------------------------------------------- 1 | from channels.auth import AuthMiddlewareStack 2 | from channels.routing import ProtocolTypeRouter, URLRouter 3 | import server.websocket.routing 4 | 5 | application = ProtocolTypeRouter({ 6 | # Empty for now (http->django views is added by default) 7 | 'websocket': AuthMiddlewareStack( 8 | URLRouter( 9 | server.websocket.routing.websocket_urlpatterns, 10 | ) 11 | ), 12 | }) -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontendDevServer/roomMonitor/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for roomMonitor project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'roomMonitor.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill' 2 | import 'mutationobserver-shim' 3 | import Vue from 'vue' 4 | import './plugins/bootstrap-vue' 5 | import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue' 6 | import App from './App.vue' 7 | // import roomAPI from "../api/roomAPI"; 8 | 9 | // roomAPI.setUpdateMode('websocket'); 10 | // roomAPI.initialList(); 11 | 12 | Vue.config.productionTip = false; 13 | Vue.use(BootstrapVue) 14 | Vue.use(BootstrapVueIcons) 15 | 16 | new Vue({ 17 | render: h => h(App), 18 | }).$mount('#app'); 19 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mysql.8 6 | true 7 | com.mysql.cj.jdbc.Driver 8 | jdbc:mysql://103.105.49.174:3306 9 | 10 | 11 | -------------------------------------------------------------------------------- /AirConditioningManagementSystem/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for AirConditioningManagementSystem project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'AirConditioningManagementSystem.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /AirConditioningManagementSystem/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for AirConditioningManagementSystem project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'AirConditioningManagementSystem.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from . import models 5 | admin.site.register(models.ACAdministrator) 6 | admin.site.register(models.Manager) 7 | admin.site.register(models.Waiter) 8 | admin.site.register(models.Tenant) 9 | admin.site.register(models.Room) 10 | admin.site.register(models.TemperatureSensor) 11 | admin.site.register(models.CentralAirConditioner) 12 | admin.site.register(models.requestQueue) 13 | admin.site.register(models.RequestRecord) 14 | admin.site.register(models.ServiceRecord) 15 | admin.site.register(models.RoomDailyReport) 16 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | Vue.use(Vuex); 4 | 5 | const store = new Vuex.Store({ 6 | 7 | state: { 8 | // 存储token 9 | Authorization: localStorage.getItem('Authorization') ? localStorage.getItem('Authorization') : '' 10 | }, 11 | 12 | mutations: { 13 | // 修改token,并将token存入localStorage 14 | changeLogin (state, user) { 15 | state.Authorization = user.Authorization; 16 | localStorage.setItem('Authorization', user.Authorization); 17 | } 18 | } 19 | }); 20 | 21 | export default store; 22 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | front-end 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontendDevServer/server/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path ,re_path 2 | from django.conf.urls import include 3 | from rest_framework.routers import DefaultRouter 4 | from .views import DeviceViewSet,DeviceStatusViewSet ,TrackerViewSet,updateTrackerLists,visitors 5 | 6 | #配置路由表 7 | router = DefaultRouter() 8 | router.register(prefix='room',viewset=DeviceViewSet,base_name='room') 9 | router.register(prefix='ac',viewset=DeviceStatusViewSet,base_name='ac') 10 | 11 | #API_V1=[] 12 | API_V1 = [ 13 | path('updateRoomLists/',updateRoomLists), 14 | ] 15 | 16 | API_V1.extend(router.urls) 17 | 18 | urlpatterns = [ 19 | re_path(r'^v1/',include(API_V1)), 20 | ] 21 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'AirConditioningManagementSystem.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /frontendDevServer/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'vrsoundmonitor.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | 23 | 24 | -------------------------------------------------------------------------------- /backend/static/css/login.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | background-image: url("../image/bg.jpg"); 4 | } 5 | .form-login { 6 | width: 100%; 7 | max-width: 330px; 8 | padding: 15px; 9 | margin: 0 auto; 10 | } 11 | .form-login { 12 | margin-top: 80px; 13 | font-weight: 400; 14 | } 15 | .form-login .form-control { 16 | position: relative; 17 | box-sizing: border-box; 18 | height: auto; 19 | padding: 10px; 20 | font-size: 16px; 21 | } 22 | .form-login input[type="password"] { 23 | margin-bottom: 10px; 24 | border-top-left-radius: 0; 25 | border-top-right-radius: 0; 26 | } 27 | form a{ 28 | display: inline-block; 29 | margin-top: 25px; 30 | font-size: 12px; 31 | line-height: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /backend/static/css/register.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | background-image: url("../image/bg.jpg"); 4 | } 5 | .form-register { 6 | width: 100%; 7 | max-width: 330px; 8 | padding: 15px; 9 | margin: 0 auto; 10 | } 11 | .form-register { 12 | margin-top: 80px; 13 | font-weight: 400; 14 | } 15 | .form-register .form-control { 16 | position: relative; 17 | box-sizing: border-box; 18 | height: auto; 19 | padding: 10px; 20 | font-size: 16px; 21 | } 22 | .form-register input[type="password"] { 23 | margin-bottom: 10px; 24 | border-top-left-radius: 0; 25 | border-top-right-radius: 0; 26 | } 27 | form a{ 28 | display: inline-block; 29 | margin-top: 25px; 30 | font-size: 12px; 31 | line-height: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /backend/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class UserForm(forms.Form): 5 | username = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput( 6 | attrs={'class': 'form-control', 'placeholder': "Username", 'autofocus': ''})) 7 | password = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput( 8 | attrs={'class': 'form-control', 'placeholder': "Password"})) 9 | # roomid = forms.CharField(label="房间号", max_length=128, widget=forms.TextInput( 10 | # attrs={'class': 'form-control', 'placeholder': "RoomID", 'autofocus': ''})) 11 | 12 | 13 | class RegisterForm(forms.Form): 14 | username = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'})) 15 | password1 = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'})) 16 | password2 = forms.CharField(label="确认密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'})) -------------------------------------------------------------------------------- /frontendDevServer/requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis==1.2.0 2 | asgiref==3.2.1 3 | asn1crypto==0.24.0 4 | astroid==2.2.5 5 | async-timeout==3.0.1 6 | attrs==19.1.0 7 | autobahn==19.8.1 8 | Automat==0.7.0 9 | autopep8==1.4.4 10 | certifi==2019.6.16 11 | cffi==1.12.3 12 | channels==2.2.0 13 | channels-redis==2.4.0 14 | colorama==0.4.1 15 | constantly==15.1.0 16 | cryptography==2.7 17 | daphne==2.3.0 18 | Django==2.2.13 19 | django-cors-headers==3.1.0 20 | djangorestframework==3.10.2 21 | Faker==2.0.1 22 | feedparser==5.2.1 23 | hiredis==1.0.0 24 | hyperlink==19.0.0 25 | idna==2.8 26 | incremental==17.5.0 27 | isort==4.3.21 28 | lazy-object-proxy==1.4.1 29 | mccabe==0.6.1 30 | msgpack==0.6.1 31 | pycodestyle==2.5.0 32 | pycparser==2.19 33 | PyHamcrest==1.9.0 34 | pylint==2.3.1 35 | python-dateutil==2.8.0 36 | pytz==2019.1 37 | six==1.12.0 38 | sqlparse==0.3.0 39 | text-unidecode==1.2 40 | Twisted==19.7.0 41 | txaio==18.8.1 42 | typed-ast==1.3.4 43 | wincertstore==0.2 44 | wrapt==1.11.2 45 | ws4py==0.5.1 46 | zope.interface==4.6.0 47 | -------------------------------------------------------------------------------- /frontendDevServer/roomMonitor/urls.py: -------------------------------------------------------------------------------- 1 | """roomMonitor URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path,re_path 18 | from django.conf.urls import include 19 | from django.conf.urls.static import static 20 | from django.conf import settings 21 | from server.views import index,bigFile 22 | 23 | urlpatterns = [ 24 | path('admin/', admin.site.urls), 25 | path('api/',include('server.urls')), 26 | path('',index,name='index'), 27 | re_path('^upload\/(?P.*)$',bigFile,name='bigFile') 28 | ] 29 | 30 | print(static(settings.MEDIA_URL,document_root = settings.MEDIA_ROOT)) 31 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # front-end 2 | 3 | ## Project setup with npm 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minimises for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your unit tests 19 | ``` 20 | npm run test:unit 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | 31 | 32 | ##@vue/cli 33 | Using @vue/cli to create the project. 34 | Be sure that you already installed Node.js which is above 8.9(8.11 or higher is highly recommended). 35 | 36 | Use 37 | ``` 38 | npm install -g @vue/cli 39 | # OR 40 | yarn global add @vue/cli 41 | ``` 42 | to install @vue/cli. 43 | To know more, please view the official site of vue at [传送门](https://cli.vuejs.org/) 44 | 45 | ##Get started 46 | Please get started with the following commands: 47 | ``` 48 | vue add bootstrap-vue 49 | cd frontend 50 | npm install 51 | npm run serve 52 | ``` 53 | 54 | ##DO NOT FORGET 55 | If your browser shows nothing but blank, please be sure that you have installed bootstrap-vue plugin, use following commands after installed vue-cli: 56 | ``` 57 | vue add bootstrap-vue 58 | ``` 59 | -------------------------------------------------------------------------------- /frontend/src/components/Room/Room.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | 32 | 46 | -------------------------------------------------------------------------------- /frontendDevServer/server/websocket/msg.py: -------------------------------------------------------------------------------- 1 | ''' 2 | typs for websocket message 3 | CMD - command 4 | MSG - message 5 | 6 | Channel Group Name: 7 | Devices 8 | Clients 9 | 10 | websocket msg form: 11 | simple msg: 12 | { 13 | msgType:int, 14 | message:str 15 | } 16 | 17 | device status msg: 18 | { 19 | msgType:int, 20 | message:{ 21 | to:"groupName", 22 | content:{ 23 | trackerID:xxx, 24 | ip:xxsad, 25 | name:dd, 26 | power:0.8, 27 | position:x,y,z, 28 | rotation:w,x,y,z, 29 | softwareVer:1.0, 30 | playing_scene:2 31 | } 32 | } 33 | } 34 | 35 | command msg: 36 | { 37 | msgType:int, 38 | message:{ 39 | to:'group' / 'single' , 40 | ip: xx, 41 | groupName:xx, 42 | content:{ 43 | cmd:201, 44 | cmdInfo:xxx 45 | } 46 | } 47 | } 48 | 49 | ''' 50 | 51 | # 特殊指令 52 | ADD_GROUP = 101 53 | LEAVE_GROUP = 102 54 | ADD_WHITELIST = 103 55 | QUERY_DEVICE_STATUS = 104 56 | 57 | # 命令类型 58 | COMMAND = 200 59 | CMD_CHANGE_SCENE = COMMAND + 1 60 | CMD_OPENOPTITRACK = COMMAND + 2 61 | CMD_CLOSEOPTITRACK = COMMAND + 3 62 | CMD_CLOSESERVER = COMMAND + 10 63 | 64 | # 消息类型 65 | MESSAGE = 300 66 | MESSAGE_BROCAST = MESSAGE + 1 67 | MSG_DEVICE_STATUS = MESSAGE + 2 68 | MSG_DEVICE_STATUS_ALL = MESSAGE + 3 69 | MSG_HEART_BEATS = MESSAGE + 4 70 | 71 | 72 | OK = 403 73 | ERROR = 404 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AirConditioningManagementSystem(分布式温控系统) 2 | 3 | ## 前言 4 | 5 | 某快捷廉价酒店响应节能绿色环保理念,推行自助计费式中央温控系统,使得入住的客户可以根据要求设定温度和风速的调节,同时可以显示所需支付的金额。客户退房时酒店须出具空调使用的账单及详单。空调运行期间,空调管理员能够监控各房间空调的使用状态,需要的情况下可以生成格式统计报表。 6 | 7 | 8 | 9 | --- 10 | 11 | ## start 12 | 13 | `git clone git@github.com:SimonGH0oSTRiley/AirConditioningManagementSystem.git` 14 | 15 | --- 16 | 17 | ### 关于如何使用`plantuml-markdown`插件 18 | 19 | 由于这个插件是基于`python`的,那么你需要安装`python`,这个步骤请自行解决,记得配置环境路径哦喵~。 20 | 21 | 其次,当你安装好`python`后,可以在shell里使用如下命令安装 22 | 23 | ```bash 24 | pip install plantuml-markdown 25 | ``` 26 | 27 | 或者对于软饭来说,可以利用`Chocolatey`依赖管理工具安装,那么首先需要安装它。请在拥有管理员权限的PowerShell中用如下命令安装`Chocolatey` 28 | 29 | ```powershell 30 | Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) 31 | ``` 32 | 33 | 如果你不知道如何开启管理员权限的Power Shell或者你想康康你是不是有权限,请在PowerShell中运行 `Get-ExecutionPolicy` 。如果返回的是 `Restricted`,那么就运行`Set-ExecutionPolicy AllSigned` 或者 `Set-ExecutionPolicy Bypass -Scope Process`看你心情就好了。如果不是 `Restricted`,那么恭喜你NM$L。 34 | 35 | 还有很多其他步骤,请参考相关页面哦 36 | 37 | [planetuml-markdown on Github](https://github.com/mikitex70/plantuml-markdown) 38 | 39 | [planetuml-markdown on pypi](https://pypi.org/project/plantuml-markdown/) 40 | 41 | [How to install Chocolatey on chocolatey.org](https://chocolatey.org/install) 42 | 43 | -------------------------------------------------------------------------------- /frontendDevServer/server/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Device, Software,DeviceStatus,Tracker 3 | 4 | 5 | class RoomSerializer(serializers.ModelSerializer): 6 | 7 | # software = serializers.CharField( 8 | # source='softwareVer.version', read_only=True) 9 | 10 | softwareInfo = serializers.SerializerMethodField() 11 | # 方法写法:get_ + 字段 12 | 13 | def get_softwareInfo(self, obj): 14 | # obj指这个model的对象 15 | sf = obj.softwareVer 16 | 17 | return { 18 | 'title':sf.title, 19 | 'version':sf.version, 20 | 'path':self.context['request'].build_absolute_uri(sf.filePath.url) 21 | } 22 | 23 | #software = SoftwareSerializer(many = False,read_only = True) 24 | class Meta: 25 | model = room 26 | fields = ['roomNumber', 'isActive', 'currentTemperature', 27 | 'currentSpeed', 'platform', 'softwareInfo'] 28 | 29 | 30 | class RoomListSerializer(serializers.ModelSerializer): 31 | class Meta: 32 | model = room 33 | fields = ['roomNumber', 'isActive', 'currentTemperature', 'currentSpeed', 'platform'] 34 | 35 | 36 | class SoftwareSerializer(serializers.ModelSerializer): 37 | 38 | class Meta: 39 | model = Software 40 | fields = '__all__' 41 | 42 | class ACSerializer(serializers.ModelSerializer): 43 | class Meta: 44 | model = AC 45 | fields = ['roomNumber', 'isACActive', 'targetTemperature', 'targetSpeed', 'requestStatus'] 46 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Login from "../components/Login"; 4 | import Administrator from "../components/Administrator/Administrator"; 5 | import Manager from "../components/Manager/Manager"; 6 | import Tenant from "../components/Tenant/Tenant"; 7 | import Waiter from "../components/Waiter/Waiter"; 8 | 9 | Vue.use(Router); 10 | 11 | const router = new Router({ 12 | routes: [ 13 | { 14 | path: '/', 15 | redirect: '/login' 16 | }, 17 | { 18 | path: '/login', 19 | name: 'login', 20 | component: Login 21 | }, 22 | { 23 | path: '/admin', 24 | name: 'admin', 25 | component: Administrator 26 | }, 27 | { 28 | path: '/manager', 29 | name: 'manager', 30 | component: Manager 31 | }, 32 | { 33 | path: '/tenant', 34 | name: 'tenant', 35 | component: Tenant 36 | }, 37 | { 38 | path: '/waiter', 39 | name: 'waiter', 40 | component: Waiter 41 | } 42 | ] 43 | }); 44 | 45 | // 导航守卫 46 | // 使用 router.beforeEach 注册一个全局前置守卫,判断用户是否登陆 47 | router.beforeEach((to, from, next) => { 48 | if (to.path === '/login') { 49 | next(); 50 | } else { 51 | let token = localStorage.getItem('Authorization'); 52 | 53 | if (token === 'null' || token === '') { 54 | next('/login'); 55 | } else { 56 | next(); 57 | } 58 | } 59 | }); 60 | 61 | export default router; 62 | -------------------------------------------------------------------------------- /frontend/src/components/Waiter/Waiter.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /frontend/src/components/Waiter/RoomDetailForWaiter.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 44 | 45 | 50 | -------------------------------------------------------------------------------- /AirConditioningManagementSystem/urls.py: -------------------------------------------------------------------------------- 1 | """AirConditioningManagementSystem URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | # from django.views.generic import TemplateView 19 | from backend import views 20 | 21 | urlpatterns = [ 22 | path('', views.index), 23 | path('admin/', admin.site.urls), 24 | path('index/', views.index, name='index'), 25 | path('login/', views.login, name='login'), 26 | path('register/', views.register, name='register'), 27 | path('logout/', views.logout, name='logout'), 28 | path('acadminview/', views.acadminview, name='acadminview'), 29 | path('waiterview/', views.waiterview, name='waiterview'), 30 | path('managerview/', views.managerview, name='managerview'), 31 | path('tenantview/', views.tenantview, name='tenantview'), 32 | # path('', views.index), 33 | # path('admin/', admin.site.urls), 34 | # path('index/', views.index), 35 | # path('login/', views.login), 36 | # path('register/', views.register), 37 | # path('logout/', views.logout), 38 | # path('acadminview/', views.acadminview), 39 | # path('waiterview/', views.waiterview), 40 | # path('managerview/', views.managerview), 41 | # path('tenantview/', views.tenantview), 42 | ] 43 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front-end", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "bootstrap": "^4.4.1", 12 | "bootstrap-icons": "^1.0.0-alpha3", 13 | "bootstrap-vue": "^2.1.0", 14 | "core-js": "^2.6.5", 15 | "mysql": "^2.18.1", 16 | "vue": "^2.6.10", 17 | "ws": "^7.2.5" 18 | }, 19 | "devDependencies": { 20 | "@babel/polyfill": "^7.7.0", 21 | "@vue/cli-plugin-babel": "^3.12.0", 22 | "@vue/cli-plugin-eslint": "^3.12.0", 23 | "@vue/cli-service": "^3.12.0", 24 | "axios": "^0.19.2", 25 | "babel-eslint": "^10.0.1", 26 | "bootstrap": "^4.3.1", 27 | "eslint": "^5.16.0", 28 | "eslint-plugin-vue": "^5.0.0", 29 | "mutationobserver-shim": "^0.3.3", 30 | "popper.js": "^1.16.0", 31 | "portal-vue": "^2.1.6", 32 | "sass": "^1.19.0", 33 | "sass-loader": "^8.0.0", 34 | "vue-cli-plugin-bootstrap-vue": "^0.6.0", 35 | "vue-router": "^3.3.4", 36 | "vue-template-compiler": "^2.6.10", 37 | "vuex": "^3.4.0" 38 | }, 39 | "eslintConfig": { 40 | "root": true, 41 | "env": { 42 | "node": true 43 | }, 44 | "extends": [ 45 | "plugin:vue/essential", 46 | "eslint:recommended" 47 | ], 48 | "rules": { 49 | "no-console": "off", 50 | "no-unused-vars": "off" 51 | }, 52 | "parserOptions": { 53 | "parser": "babel-eslint" 54 | } 55 | }, 56 | "postcss": { 57 | "plugins": { 58 | "autoprefixer": {} 59 | } 60 | }, 61 | "browserslist": [ 62 | "> 1%", 63 | "last 2 versions" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /frontendDevServer/server/websocket/databasectrl.py: -------------------------------------------------------------------------------- 1 | from channels.db import database_sync_to_async 2 | from server.models import IPChannel, Device, DeviceStatus 3 | 4 | @database_sync_to_async 5 | def saveIPChannelObj(_ip, _port, _channelName): 6 | results = IPChannel.objects.filter(ip=_ip, port=_port) 7 | if results.exists(): 8 | obj = results[0] 9 | obj.port = _port 10 | obj.channelName = _channelName 11 | obj.save() 12 | else: 13 | IPChannel.objects.create(ip=_ip, port=_port, channelName=_channelName) 14 | 15 | 16 | @database_sync_to_async 17 | def deleteIPChannelObj(_ip, _port): 18 | IPChannel.objects.filter(ip=_ip, port=_port).delete() 19 | 20 | 21 | @database_sync_to_async 22 | def getChannelName(_ip): 23 | results = IPChannel.objects.filter(ip=_ip) 24 | if results.exists(): 25 | obj = results[0] 26 | return obj.channelName 27 | else: 28 | return "" 29 | 30 | 31 | @database_sync_to_async 32 | def getDeviceIP(dID): 33 | results = Device.objects.filter(deviceID=dID) 34 | if results.exists(): 35 | obj = results[0] 36 | return obj.ip 37 | return "" 38 | 39 | 40 | @database_sync_to_async 41 | def updateDeviceStatus(deviceObj): 42 | results = DeviceStatus.objects.filter(deviceID=deviceObj['deviceID']) 43 | if results.exists(): 44 | obj = results[0] 45 | obj.position = deviceObj['position'] 46 | obj.rotation = deviceObj['rotation'] 47 | obj.power = deviceObj['power'] 48 | obj.currentScene = deviceObj['currentScene'] 49 | obj.isPlaying = deviceObj['isPlaying'] 50 | obj.isCharging = deviceObj['isCharging'] 51 | obj.save() 52 | else: 53 | DeviceStatus.objects.create(deviceID=deviceObj['deviceID'], position=deviceObj['position'], rotation=deviceObj['rotation'], 54 | power=deviceObj['power'], currentScene=deviceObj['currentScene'], isPlaying=deviceObj['isPlaying'],isCharging = deviceObj['isCharging']) 55 | 56 | -------------------------------------------------------------------------------- /backend/templates/backend/login.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 登录 12 | 13 | 14 |
15 |
16 | 35 |
36 |
37 | 38 | 39 | 40 | {# 以下三者的引用顺序是固定的#} 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/components/Manager/Manager.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 63 | 64 | 67 | -------------------------------------------------------------------------------- /backend/templates/backend/register.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 注册 12 | 13 | 14 |
15 |
16 |
17 | {% if message %} 18 |
{{ message }}
19 | {% endif %} 20 | {% csrf_token %} 21 |

欢迎注册

22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 直接登录 36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 | 44 | {# 以下三者的引用顺序是固定的#} 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 55 | 56 | 66 | -------------------------------------------------------------------------------- /frontend/src/components/Administrator/Administrator.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 67 | 68 | 71 | -------------------------------------------------------------------------------- /frontendDevServer/server/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from datetime import datetime,timezone 3 | 4 | 5 | PLATFORMS = (('a', 'android'), ('i', 'ios'), ('w', 'windows')) 6 | STATUS = (('ready'), ('pending'), ('resolved'), ('rejected')) 7 | 8 | # Create your models here. 9 | 10 | 11 | class room(models.Model): 12 | roomNumber = models.PositiveSmallIntegerField(primary_key=True,default=0) 13 | isActive = models.BooleanField(null=False) 14 | currentTemperature = models.PositiveSmallIntegerField(default=20) 15 | currentSpeed = models.PositiveSmallIntegerField(default=0) 16 | platform = models.CharField(max_length=1, choices=PLATFORMS, default='w') 17 | softwareVer = models.ForeignKey( 18 | 'Software', related_name='software', on_delete=models.SET_NULL, null=True) 19 | timestamp = models.DateTimeField(auto_now=True) 20 | 21 | def __str__(self): 22 | return self.name 23 | 24 | class AC(models.Model): 25 | roomNumber = models.PositiveSmallIntegerField(primary_key=True,default=0) 26 | isACActive = = models.BooleanField(null=False) 27 | targetTemperature = models.PositiveSmallIntegerField(default=20) 28 | targetSpeed = models.PositiveSmallIntegerField(default=0) 29 | requestStatus = models.CharField(max_length=7, choices=STATUS, default='ready') 30 | updateTime = models.DateTimeField(auto_now=True) 31 | 32 | @property 33 | def isACActive(self): 34 | delta = datetime.utcnow() - self.updateTime.replace(tzinfo=None) 35 | if delta.seconds > 5: 36 | return False 37 | else: 38 | return True 39 | 40 | 41 | class Software(models.Model): 42 | title = models.CharField(max_length=30, null=True) 43 | version = models.CharField(max_length=10, null=True) 44 | description = models.TextField( 45 | max_length=500, help_text='Description of this version', null=True) 46 | platform = models.CharField(max_length=1, choices=PLATFORMS, default='a') 47 | filePath = models.FileField(upload_to='tmp/') 48 | timestamp = models.DateTimeField(auto_now=True) 49 | 50 | def __str__(self): 51 | """ 52 | String for representing the Model object. 53 | """ 54 | return '%s, %s, %s' % (self.title, self.platform, self.version) 55 | 56 | 57 | ''' 58 | used for websocket p2p communication 59 | ''' 60 | #该表使用于websocket索引channel进行一对一发信使用 61 | class IPChannel(models.Model): 62 | ip = models.GenericIPAddressField() 63 | port = models.IntegerField() 64 | channelName = models.CharField(max_length=50) 65 | -------------------------------------------------------------------------------- /frontend/api/database.js: -------------------------------------------------------------------------------- 1 | let mysql = require('mysql'); 2 | 3 | let connection = mysql.createConnection({ 4 | host : '103.105.49.174:3306', 5 | user : 'root', 6 | password : 'some_pass', 7 | // TODO: 2 Be Done 8 | database : 'test_AirConditioningManagementSystem' 9 | }); 10 | 11 | 12 | 13 | // 查询房间状态 14 | function queryRoomStatus (targetRoom) { 15 | // 使用Promise更改异步操作为同步操np作,使得回调函数能正确执行 16 | let promise = new Promise( (resolve, reject) => { 17 | // 建立连接 18 | connection.connect(function (error) { 19 | if (error) { 20 | reject('[CONNECT ERROR] - ' + error.stack); 21 | } 22 | console.log('[CONNECT SUCCESS] - Connected as id ' + connection.threadId); 23 | }); 24 | 25 | let querySql = 'SELECT * FROM room WHERE room_no = ?'; 26 | let querySqlParams = [targetRoom]; 27 | connection.query(querySql, querySqlParams, function (error, result) { 28 | if (error) { 29 | reject('[SELECT ERROR] - ' + error.message); 30 | } 31 | resolve(result); 32 | }); 33 | 34 | // 终止连接 35 | connection.end(); 36 | }); 37 | 38 | promise.then(data => { 39 | console.log(data); 40 | return data; 41 | }).catch(res => { 42 | console.log(res); 43 | return 0; 44 | }); 45 | return promise; 46 | } 47 | 48 | // 更改房间状态 49 | function updateRoomStatus (targetRoom, targetTemperature, currTemperature) { 50 | // 使用Promise更改异步操作为同步操作,使得回调函数能正确执行 51 | let promise = new Promise(function (resolve, reject) { 52 | // 建立连接 53 | connection.connect(function (error) { 54 | if (error) { 55 | reject('[CONNECT ERROR] - ' + error.stack); 56 | } 57 | console.log('[CONNECT SUCCESS] - Connected as id ' + connection.threadId); 58 | }); 59 | 60 | let modSql = 'UPDATE test SET targetTemperature = ?, currTemperature = ? WHERE channel_no = ?'; 61 | let modSqlParams = [targetTemperature, currTemperature, targetRoom]; 62 | 63 | connection.query(modSql, modSqlParams,function (error, result) { 64 | if(error){ 65 | reject('[UPDATE ERROR] - ' + error.message); 66 | } 67 | resolve(result); 68 | }); 69 | 70 | // 终止连接 71 | connection.end(); 72 | }); 73 | 74 | promise.then(data => { 75 | console.log(data); 76 | return data; 77 | }).catch(res => { 78 | console.log(res); 79 | return 0; 80 | }); 81 | return promise; 82 | } 83 | 84 | function calRoomACFee (targetRoom) { 85 | // TODO: discuss about it 86 | } 87 | 88 | module.exports = {queryRoomStatus, updateRoomStatus}; 89 | -------------------------------------------------------------------------------- /frontend/src/components/Login.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /doc/分布式温控系统的解决方案.md: -------------------------------------------------------------------------------- 1 | # 分布式温控系统的解决方案 2 | 3 | ## 目标 4 | 5 | - 完全实现需求,使得房客可开关空调、设置空调风速与温度以及查看空调费;酒店收银员在房客退房时可提供空调账单及详单;酒店空调管理员可监控各客房空调使用情况;酒店经理可查看不同时间范围内的格式化统计报表。 6 | - 可靠性高,能够7*24小时稳定运行。 7 | - 系统操作界面友好,容易上手。 8 | - 高并发,高吞吐率,低延时。 9 | 10 | ## 需求 11 | 12 | - 本分布式温控系统分为服务器端和客户端。 13 | - 服务器端接收各客房空调开关状态、冷热模式、风速请求与温度设定信息,进而以最低能耗控制中央空调的开关与负载,并以指定冷热模式向各客房送风,从而响应了节能绿色环保理念。其中,风速请求分为高风、中风、低风、无风四档,耗电标准为高风1度/分钟,中风0.5度/分钟,低风0.333度/分钟,无风0度/分钟,电费1元/度,制冷模式温度范围为18-25℃,制热模式温度范围为25-30℃。 14 | - 为进一步响应绿色节能环保理念,避免冬天制冷、夏天制热这样不合理的请求,空调管理员可根据当前气温在服务器端设置制冷模式,拒绝不一致的客户端送风请求。 15 | - 服务器端还可统计指定客房的空调账单与详单,监控各客房的使用情况,查看不同时间范围内的格式化统计报表。详单包含的信息为房间号、每次送风时房间温度,结束送风时房间温度,持续时间,耗电量,电费。 16 | - 客户端可开关室内机,显示温度传感器发来的客房温度以及服务器发来的空调费,设定冷热模式、目标温度与风速,根据设定温度动态向服务端发送风速请求。客房温度达到目标温度(即制冷模式下客房温度≤目标温度,制热模式下客房温度≥目标温度),发送无风请求给服务端;当房间温度超过目标温度1℃时(即制冷模式下客房温度≥目标温度+1,制热模式下客房温度≤目标温度-1),发送设定模式送风请求给服务端,控制室内机发出设定风速。此外,客户端开关空调、调整空调制冷制热模式、调节目标温度、调节风速后,都会向服务端发送相应信息,且温度每秒最多调节1次,以一秒内最后发送的请求为准。 17 | 18 | ## 测试方案 19 | 20 | - 温度传感器的测试数据构造方法为: 21 | - 空调开机,向设定温度变化,中风模式每分钟变化0.5℃,高风模式每分钟变化0.6℃,低风模式每分钟变化0.4℃。 22 | - 空调关机或待机,向初始温度变化,每分钟变化0.5℃。 23 | - 随机生成客房空调请求,并通过查看各客房详单与账单、监控各客房空调使用状态、查看不同时间段格式化报表检查系统是否正确。 24 | 25 | ## 技术路线 26 | 27 | * 结合现有技术条件,拟定的技术路线如下: 28 | 29 | ``` mermaid 30 | graph LR 31 | A((分布式温控系统))-->B(服务器端) 32 | B-->C[服务器端与客户端的通信] 33 | B-->D[服务器端利用数据库保存当前各个房间空调状态] 34 | B-->E[服务器端应对高并发] 35 | C-->F{算法优化 模块测试} 36 | D-->F 37 | E-->F 38 | A-->G(客户端) 39 | G-->H[客户端UI] 40 | G-->I[客户端账户角色权限] 41 | G-->J[客户端非法输入过滤] 42 | H-->K{操作逻辑优化 模块测试} 43 | I-->K 44 | J-->K 45 | F-->L(组装) 46 | K-->L{集成测试 压力检验} 47 | L-->M(运行与维护系统建设) 48 | 49 | ``` 50 | 51 | * 其中,各个模块暂定选用的技术如下表所示 52 | 53 | * | | 服务器端 | 客户端 | 54 | | :------: | :---------------------------------------------------: | :----------------------------------------------------------: | 55 | | 编码语言 | 利用`django`为Web框架,采用`python`语言编码编写服务器 | 利用`vue`框架,采用经典的`html + JavaScript + CSS`编码方式编写浏览器式客户端 | 56 | | 通信方式 | `WebSocket + JSON` | `WebSocket + JSON` | 57 | | 数据库 | 拟采用`MySQL`用作保存 | 各房间空调状态读取时向服务器发起请求,修改后保存最新状态 | 58 | | 权限管理 | 拟分为客户、收银、管理三个账户权限 | 根据不同账户设计不同UI,登陆后开放相应权限 | 59 | 60 | 61 | ## 项目时间规划 62 | 63 | * 结合现有技术条件,拟定的项目时间规划如下: 64 | 65 | ```mermaid 66 | gantt 67 | dateFormat YYYY-MM-DD 68 | title 分布式温控系统开发甘特图 69 | section 设计 70 | 需求: done, designDemand, 2020-02-24, 2020-03-09 71 | 原型: active, designPrototype, 2020-03-09, 7d 72 | UI设计: designUI, after designPrototype, 7d 73 | 预留: designReserved, after designUI, 14d 74 | section 开发 75 | 学习准备: done, 2020-02-24, 7d 76 | 理解需求: crit, acitve, 2020-03-09, 7d 77 | 设计框架: crit, developeFramework, after designPrototype, 7d 78 | 编码: crit, developeCoding, after developeFramework, 30d 79 | 预留: developeReserved, after developeCoding, 30d 80 | section 测试 81 | 模块功能测试: testModule, after developeCoding, 7d 82 | 组装: testAssembling, after testModule, 5d 83 | 集成压力测试: testIntergrated, after testAssembling , 7d 84 | 测试报告: testReport, after testIntergrated. 48h 85 | ``` -------------------------------------------------------------------------------- /.idea/AirConditioningManagementSystem.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 23 | 24 | 25 | 26 | 33 | 34 | 35 | 36 | 43 | 44 | 45 | 46 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 67 | 68 | -------------------------------------------------------------------------------- /doc/静态结构设计-房客.md: -------------------------------------------------------------------------------- 1 | # 用例图 2 | 3 | 4 | 5 | ```python 6 | ::uml:: format="png" classes="uml useCaseDiagram" alt="Tenant Use Case Diagram" title="Tenant Use Case Diagram" width="1000px" height="400px" 7 | left to right direction 8 | 9 | :房客 Tenant: as Tenant 10 | 11 | (房间空调管理 Room AC Management) as (Management) 12 | 13 | Tenant -- (Management) 14 | 15 | ::end-uml:: 16 | ``` 17 | 18 | 19 | 20 | --- 21 | 22 | # 静态结构 23 | 24 | **Class: Tenant** 25 | 26 | | 类型 | 实例 | 备注 | 27 | | ---- | --------------------------------------------- | ------------------------------------------------------------ | 28 | | 属性 | roomNo: int | 房客入住的房间号。 | 29 | | | tenantWebsocket: ws.socket | 房客与服务器建立连接的套接字实例。 | 30 | | 方法 | initiateTenant(roomNo, tenantWebsocket): bool | 初始化房客对象。参数roomNo指定房间号;参数TenantWebsocket指定套接字实例。返回值为初始化函数执行状态。 | 31 | | | adjustAC (roomNo, mode, attribute): string | 调整房间空调参数。参数mode指定开关空调/调整温度/调整风速;参数attribute指定调整目标值。该请求返回值分为三个状态,设计参考javascript.Promise。返回值初始为pending状态,当后台返回数据转化为resolved状态或rejected状态。 | 32 | | | getStatus(roomNo): JSON | 获取当前房间状态信息。以JSON方式返回当前房间状态 | 33 | 34 | **Class: ACServer** 35 | 36 | | 类型 | 实例 | 备注 | 37 | | ---- | ------------------------------------------------------------ | ------------------------------------------------------------ | 38 | | 属性 | serverWebsocket: ws.socket | 服务器与房客建立连接的套接字实例。 | 39 | | | DBWebsocket: ws.socket | 服务器与数据库建立连接的套接字实例。 | 40 | | | requestQueue: dequeue | 服务器保存的房间空调调整请求队列 | 41 | | | currentRequest: JSON | 当前请求 | 42 | | 方法 | initiateACServer(serverWebsocket, DBWebsocket, requestQueue, currentRequest): int | 初始化服务器(或用于复位服务器)。参数serverWebsocket指定房客套接字实例;DBWebsocket指定数据库套接字实例;requestQueue指定请求队列;currentRequest指定当前请求。返回值为初始化函数执行状态。 | 43 | | | shiftQueue(requestQueue): JSON | 返回并移除请求队列队首元素。返回值为队首请求。 | 44 | | | popQueue(requestQueue): dequeue | 返回并移除请求队列队尾元素。返回值为队尾请求 | 45 | | | processRequest(): bool | 处理当前请求。返回值为函数执行状态。 | 46 | | | saveRequestToDB(): bool | 将resolved状态的请求保存至数据库。返回值为函数执行状态。 | 47 | 48 | **Class: DB** 49 | 50 | | 类型 | 实例 | 备注 | 51 | | ---- | ----------------------------------------------- | ------------------------------------------------------------ | 52 | | 属性 | DBWebsocket: ws.socket | 服务器与数据库建立连接的套接字实例。 | 53 | | | DB: database | 数据库实例 | 54 | | 方法 | queryDBHandler(DBWebsocket, DB, request): JSON | 数据库查询函数。参数指定了套接字实例,数据库实例与查询SQL语句。返回查询结果。 | 55 | | | updateDBHandler(DBWebsocket, DB, request): JSON | 数据库更新函数。参数指定了套接字实例,数据库实例与更新SQL语句。返回更新结果。 | -------------------------------------------------------------------------------- /frontend/src/components/Manager/RoomDetailForManager.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 81 | 82 | 87 | -------------------------------------------------------------------------------- /doc/动态结构设计-房客.md: -------------------------------------------------------------------------------- 1 | # 用例图 2 | 3 | 4 | 5 | ```python 6 | ::uml:: format="png" classes="uml useCaseDiagram" alt="Tenant Use Case Diagram" title="Tenant Use Case Diagram" width="1000px" height="400px" 7 | left to right direction 8 | 9 | :房客 Tenant: as Tenant 10 | 11 | (房间空调管理 Room AC Management) as (Management) 12 | 13 | Tenant -- (Management) 14 | 15 | ::end-uml:: 16 | ``` 17 | 18 | 19 | 20 | --- 21 | 22 | # 系统事件列表 23 | 24 | | 系统事件名称 | 用例 | 参数说明 | 备注 | 25 | | ---------------------------- | ------------ | ------------------------------------------------------------ | --------------------------- | 26 | | getRoomStatus(roomNo) | 房间空调管理 | roomNo.validator(Number) | get请求从DB中读取状态并返回 | 27 | | adjustAC(roomNo,mode, value) | 房间空调管理 | roomNo.validator(Number), mode.validator(String), value.validator(Number) | adjust请求后保存状态至DB | 28 | 29 | --- 30 | 31 | # 系统交互图 32 | 33 | 34 | 35 | ```python 36 | ::uml:: format="png" classes="uml useCaseDiagram" alt="Tenant Use Case Diagram" title="Tenant Use Case Diagram" width="1000px" height="400px" 37 | actor "房客 Tenant" as Client 38 | entity "服务器 Server" as Server 39 | database "数据库 Database" as DB 40 | 41 | alt 房间空调管理 Room AC Management 42 | 43 | == 开启空调 Enable AC == 44 | 45 | Client->Server: 开启空调 adjustAC(roomNo, power, 1) 46 | Server->DB: 记录操作 updateRoomStatus(roomNo, 1, null, null) 47 | DB-->Server: 记录成功 updateRoomStatusSucceed(msgJSON) 48 | Server-->Client: 空调已开启 adjustACSucceed(msgJSON) 49 | 50 | == 房间空调参数调整 Room AC Variable Adjustment == 51 | 52 | loop 53 | 54 | Client->Server: 调整参数 adjustAC(roomNo, mode, value) 55 | Server->DB: 记录操作 updateRoomStatus(roomNo, 1, mode, value) 56 | DB-->Server: 记录成功 updateRoomStatusSucceed(msgJSON) 57 | Server-->Client: 温度已调整adjustACSucceed(msgJSON) 58 | 59 | end 60 | 61 | == 关闭空调 Disable AC == 62 | 63 | Client->Server: 关闭空调 adjustAC(roomNo, power, 0) 64 | Server->DB: 记录操作 updateRoomStatus(roomNo, 0, null, null) 65 | DB-->Server: 记录成功 updateRoomStatusSucceed(msgJSON) 66 | Server-->Client: 空调已关闭 adjustACSucceed(msgJSON) 67 | 68 | end 69 | ::end-uml:: 70 | ``` 71 | 72 | 73 | 74 | ```python 75 | ::uml:: format="png" classes="uml useCaseDiagram" alt="Tenant Use Case Diagram" title="Tenant Use Case Diagram" width="1000px" height="400px" 76 | actor "管理员 Administrator" as Client 77 | entity "服务器 Server" as Server 78 | database "数据库 Database" as DB 79 | 80 | alt 房间空调管理 Room AC Management 81 | 82 | == 获取房间状态 Get Room Status == 83 | 84 | Client->Server: 获取房间状态 getRoomStatus(roomNo) 85 | Server->DB: 读取操作 queryRoomStatus(roomNo) 86 | DB-->Server: 读取成功 queryRoomStatusSucceed(msgJSON) 87 | Server-->Client: 返回房间状态 getRoomStatusSucceed(msgJSON) 88 | 89 | end 90 | ::end-uml:: 91 | ``` 92 | 93 | -------------------------------------------------------------------------------- /backend/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Sum 2 | from django.shortcuts import render, HttpResponse 3 | from django.shortcuts import redirect 4 | from django.shortcuts import get_object_or_404 5 | from . import forms 6 | from . import models 7 | 8 | 9 | # # Create your views here. 10 | def index(request): 11 | if not request.session.get('is_login', None): 12 | return redirect('/login/') 13 | return render(request, 'backend/index.html') 14 | 15 | 16 | def login(request): 17 | if request.session.get('is_login', None): # 不允许重复登录 18 | return redirect('/index/') 19 | if request.method == "POST": 20 | login_form = forms.UserForm(request.POST) 21 | message = '请检查填写的内容!' 22 | if login_form.is_valid(): 23 | # 验证成功后可以从表单对象的cleaned_data数据字典中获取表单的具体值 24 | username = login_form.cleaned_data.get('username') 25 | password = login_form.cleaned_data.get('password') 26 | try: 27 | tenant = models.Tenant.objects.get(name=username) 28 | except: 29 | message = '不存在此房客!' 30 | return render(request, 'backend/login.html', locals()) 31 | if tenant.password == password: 32 | request.session['is_login'] = True 33 | request.session['user_id'] = tenant.id 34 | request.session['user_name'] = tenant.name 35 | return redirect('/tenantview/') 36 | 37 | else: 38 | message = '密码不正确!' 39 | return render(request, 'backend/login.html', locals()) 40 | else: 41 | return render(request, 'backend/login.html', locals()) 42 | # 对于非POST方法发送数据时(比如GET方法请求页面),返回空的表单,让用户可以填入数据 43 | login_form = forms.UserForm() 44 | return render(request, 'backend/login.html', locals()) 45 | 46 | 47 | def register(request): 48 | if request.session.get('is_login', None): 49 | return redirect('/index/') 50 | if request.method == 'POST': 51 | register_form = forms.RegisterForm(request.POST) 52 | message = "请检查填写的内容!" 53 | if register_form.is_valid(): 54 | username = register_form.cleaned_data.get('username') 55 | password1 = register_form.cleaned_data.get('password1') 56 | password2 = register_form.cleaned_data.get('password2') 57 | if password1 != password2: 58 | message = '两次输入的密码不同!' 59 | return render(request, 'backend/register.html', locals()) 60 | else: 61 | same_name_tenant = models.Tenant.objects.filter(name=username) 62 | if same_name_tenant: 63 | message = '用户名已经存在' 64 | return render(request, 'backend/register.html', locals()) 65 | 66 | new_tenant = models.Tenant() 67 | new_tenant.name = username 68 | new_tenant.password = password1 69 | new_tenant.save() 70 | 71 | return redirect('/login/') 72 | else: 73 | return render(request, 'backend/register.html', locals()) 74 | register_form = forms.RegisterForm() 75 | return render(request, 'backend/register.html', locals()) 76 | 77 | 78 | def logout(request): 79 | if not request.session.get('is_login', None): 80 | # 如果本来就未登录,也就没有登出一说 81 | return redirect("/login/") 82 | request.session.flush() 83 | return redirect("/login/") 84 | 85 | 86 | def acadminview(request): 87 | # if not request.session.get('is_login', None): 88 | # return redirect('/login/') 89 | return render(request, 'backend/acadminview.html') 90 | 91 | 92 | def waiterview(request): 93 | # if not request.session.get('is_login', None): 94 | # return redirect('/login/') 95 | return render(request, 'backend/waiterview.html') 96 | 97 | 98 | def managerview(request): 99 | # if not request.session.get('is_login', None): 100 | # return redirect('/login/') 101 | return render(request, 'backend/managerview.html') 102 | 103 | 104 | def tenantview(request, room_id): 105 | if not request.session.get('is_login', None): 106 | return redirect('/login/') 107 | room = get_object_or_404(models.Room, id=room_id) 108 | return render(request, 'backend/tenantview.html') 109 | -------------------------------------------------------------------------------- /frontendDevServer/server/websocket/consumers.py: -------------------------------------------------------------------------------- 1 | from channels.generic.websocket import AsyncWebsocketConsumer 2 | import json 3 | from .msg import * 4 | from .databasectrl import * 5 | import logging 6 | logger = logging.getLogger('') 7 | 8 | #Django Channel 的Consumer类,用于web socket 通信和信号分发 9 | class MonitorConsumer(AsyncWebsocketConsumer): 10 | 11 | clientIP = '' 12 | clientPort = '' 13 | 14 | async def connect(self): 15 | ''' 16 | 这里要记录下 device(主要是IP绑定) 和channel_name的对应关系 17 | ''' 18 | self.clientIP = self.scope['client'][0] 19 | self.clientPort = self.scope['client'][1] 20 | logger.info(' [ws] client: {} connected '.format(self.scope['client'])) 21 | await saveIPChannelObj(self.clientIP, self.clientPort, self.channel_name) 22 | await self.accept() 23 | 24 | async def disconnect(self, close_code): 25 | # Leave room group 26 | if hasattr(self, 'groupName'): 27 | await self.channel_layer.group_discard( 28 | self.groupName, 29 | self.channel_name 30 | ) 31 | logger.info(' [ws] client: {} disconnected '.format(self.scope['client'])) 32 | await deleteIPChannelObj(self.clientIP, self.clientPort) 33 | 34 | async def receive(self, text_data): 35 | actions = { 36 | ADD_GROUP: self.joinGroup, 37 | MESSAGE_BROCAST: self.brocastDeviceStatus, 38 | COMMAND: self.sendCmdToDevices, 39 | MSG_HEART_BEATS: self.heartBeats 40 | } 41 | textDataJson = json.loads(text_data) 42 | 43 | msgType = textDataJson['msgType'] 44 | message = textDataJson['message'] 45 | logger.debug(' [ws] receive wsmsg from: {0}:{1}, msgType:{2}'.format( self.clientIP, self.clientPort,msgType)) 46 | if msgType in actions: 47 | await actions[msgType](message) 48 | 49 | async def heartBeats(self,message): 50 | print("heart beats!") 51 | await self.send(text_data=json.dumps({ 52 | 'msgType': MSG_HEART_BEATS, 53 | 'message': 'Beats' 54 | })) 55 | 56 | async def joinGroup(self, groupName): 57 | self.groupName = groupName 58 | await self.channel_layer.group_add( 59 | self.groupName, 60 | self.channel_name 61 | ) 62 | logger.info('join group ' + self.groupName) 63 | await self.send(text_data=json.dumps({ 64 | 'msgType': OK, 65 | 'message': 'join group ' + self.groupName 66 | })) 67 | 68 | async def brocastDeviceStatus(self, message): 69 | toGroup = message['to'] 70 | status = message['content'] 71 | 72 | #print('start update database') 73 | ''' 74 | updata database here 75 | ''' 76 | await updateDeviceStatus(status) 77 | # status['ip'] = await getDeviceIP(status['deviceID']) 78 | status_msg = { 79 | 'msgType': MSG_DEVICE_STATUS, 80 | 'message': status 81 | 82 | } 83 | 84 | await self.channel_layer.group_send(toGroup, { 85 | 'type': 'deployMessage', 86 | 'message': status_msg 87 | }) 88 | 89 | async def sendCmdToDevices(self, message): 90 | target = message['to'] 91 | content = message['content'] 92 | cmd_msg = { 93 | 'msgType': COMMAND, 94 | 'message': content 95 | } 96 | if str.lower(target) == 'group': 97 | toGroup = message['groupName'] 98 | await self.channel_layer.group_send(toGroup, { 99 | 'type': 'deployMessage', 100 | 'message': cmd_msg 101 | }) 102 | else: 103 | # 根据ip查询对应的channel_name 104 | targetIP = message['ip'] 105 | targetChannel = await getChannelName(targetIP) 106 | if targetChannel == '': 107 | logger.warn(' [ws] commend sended failed, no target {}'.format(targetIP)) 108 | else: 109 | await self.channel_layer.send(targetChannel, {'type': 'deployMessage', 110 | 'message': cmd_msg}) 111 | 112 | async def deployMessage(self, event): 113 | message = event['message'] 114 | await self.send(text_data=json.dumps(message)) 115 | -------------------------------------------------------------------------------- /AirConditioningManagementSystem/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for AirConditioningManagementSystem project. 3 | Generated by 'django-admin startproject' using Django 3.0.6. 4 | 5 | For more information on this file, see 6 | https://docs.djangoproject.com/en/3.0/topics/settings/ 7 | 8 | For the full list of settings and their values, see 9 | https://docs.djangoproject.com/en/3.0/ref/settings/ 10 | """ 11 | 12 | import os 13 | 14 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 15 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 16 | 17 | # Quick-start development settings - unsuitable for production 18 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 19 | 20 | # SECURITY WARNING: keep the secret key used in production secret! 21 | SECRET_KEY = '0!e4#zl#vm+$$f6cgdo*s(+4jn9&f8&mm5l)tthu5%ex*ypo0u' 22 | 23 | # SECURITY WARNING: don't run with debug turned on in production! 24 | DEBUG = True 25 | 26 | ALLOWED_HOSTS = [] 27 | 28 | # Application definition 29 | 30 | INSTALLED_APPS = [ 31 | # 'backend', 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | 'backend.apps.BackendConfig', 39 | ] 40 | 41 | MIDDLEWARE = [ 42 | 'django.middleware.security.SecurityMiddleware', 43 | 'django.contrib.sessions.middleware.SessionMiddleware', 44 | 'django.middleware.common.CommonMiddleware', 45 | 'django.middleware.csrf.CsrfViewMiddleware', 46 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 47 | 'django.contrib.messages.middleware.MessageMiddleware', 48 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 49 | ] 50 | 51 | ROOT_URLCONF = 'AirConditioningManagementSystem.urls' 52 | 53 | TEMPLATES = [ 54 | { 55 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 56 | 'DIRS': ['frontend/dist'], 57 | # 'DIRS': [os.path.join(BASE_DIR, 'templates')], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'AirConditioningManagementSystem.wsgi.application' 71 | 72 | # Database 73 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 74 | ''' 75 | DATABASES = { 76 | 'default': { 77 | 'ENGINE': 'django.db.backends.mysql', 78 | 'NAME': 'AirConditioningManagementSystem', 79 | 'USER': 'root', 80 | 'PASSWORD': 'some_pass', 81 | 'HOST': '103.105.49.174', 82 | 'PORT': 3306, 83 | 'OPTIONS': {'charset': 'utf8mb4'}, 84 | 'TEST': { 85 | 'NAME': 'test_AirConditioningManagementSystem', 86 | 'CHARSET': 'utf8mb4', 87 | }, 88 | } 89 | } 90 | ''' 91 | DATABASES = { 92 | 'default': { 93 | 'ENGINE': 'django.db.backends.mysql', 94 | 'NAME': 'django', 95 | 'USER': 'root', 96 | 'PASSWORD': 'ckl123456', 97 | 'HOST': '127.0.0.1', 98 | 'PORT': 3306, 99 | 'OPTIONS': {'charset': 'utf8mb4'}, 100 | 'TEST': { 101 | 'NAME': 'test_AirConditioningManagementSystem', 102 | 'CHARSET': 'utf8mb4', 103 | }, 104 | } 105 | } 106 | ''' 107 | DATABASES = { 108 | 'default': { 109 | 'ENGINE': 'django.db.backends.sqlite3', 110 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 111 | } 112 | }''' 113 | 114 | # Password validation 115 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 116 | 117 | AUTH_PASSWORD_VALIDATORS = [ 118 | { 119 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 120 | }, 121 | { 122 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 123 | }, 124 | { 125 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 126 | }, 127 | { 128 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 129 | }, 130 | ] 131 | 132 | # Internationalization 133 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 134 | 135 | LANGUAGE_CODE = 'zh-hans' 136 | 137 | TIME_ZONE = 'Asia/Shanghai' 138 | 139 | USE_I18N = True 140 | 141 | USE_L10N = True 142 | 143 | USE_TZ = False 144 | 145 | # Static files (CSS, JavaScript, Images) 146 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 147 | 148 | STATIC_URL = '/static/' 149 | 150 | STATICFILES_DIRS = [ 151 | os.path.join(BASE_DIR, "frontend/dist/static/"), 152 | ] 153 | -------------------------------------------------------------------------------- /frontend/src/components/Tenant/Tenant.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 104 | 105 | 111 | -------------------------------------------------------------------------------- /doc/分布式温控系统的需求定义及其领域模型.md: -------------------------------------------------------------------------------- 1 | # 分布式温控系统的需求定义及其领域模型 2 | 3 | ## 业务介绍 4 | 5 | 6 | 7 | ## 业务流程 8 | 9 | 10 | 11 | ## 用户需求定义 12 | 13 | 注:实体加粗,属性斜体。 14 | 15 | - **空调管理员**可依照季节设置**中央空调主机**的*送风模式* 、*开关状态* ,包括制冷模式或制热模式,以及*计费单位时间*(默认为秒)、*单位电费* (默认1元/度)与*各档风速单位时间耗电量* (默认高风0.01666度/秒,中风0.00833度/秒,低风0.00556度/秒)。 16 | - **空调管理员**可实时监控各客房空调使用情况,即查看**请求队列** 和**送风队列**信息。 17 | 18 | - **房客**入住酒店,**酒店前台**分配客房,**客房空调**开始计费。 19 | - **房客**退房,**客房空调**停止计费,**酒店前台**开具**账单**及**详单**。**账单**包含*空调用电总度数* 以及*空调总费用* ,**详单**包含各空调使用时段的*送风模式* 、*风速*、*送风时长*、*空调用电度数*、*空调费用*。 20 | 21 | - **房客**可通过**控制面板**设定**室内机**的*开关状态*、*目标温度*、*送风模式*、*风速* ,查看**温度传感器**传来的*室温* 以及**服务器端**传来的*空调总费用* 。*送风模式* 为制冷模式时,*目标温度* 为18-25℃;为制热模式时,*目标温度* 为25-30℃。*目标温度* 缺省值可被设定,如25℃。*目标温度* 每秒最多调节1次,以一秒内最后发送的请求为准。*风速* 可设高风、中风、低风三档。 22 | - **客户端**根据*目标温度*动态向**服务端**发出**送风请求**,包括*房客号* 、*房间号*、*发送时间*、*送风模式*、*风速* 、*目标温度*。*室温* 达到*目标温度*(即制冷模式下*室温* ≤*目标温度* ,制热模式下*室温* ≥*目标温度* ),发送*风速* 为无风的**送风请求**给**服务端**;当*室温* 超过*目标温度* 1℃时(即制冷模式下*室温* ≥*目标温度* +1,制热模式下*室温* ≤*目标温度* -1),发送指定**送风请求**给**服务端**。 23 | - **服务器端**接收*送风模式* 与设定一致的**送风请求**,删除在**请求队列** 或**送风队列** 里老的(*房间号* 相同、*发送时间* 更早的)**送风请求**,若*风速* 不为无风,添加该**送风请求**至**请求队列**,并由此新建**空调使用记录**,包括*房客号* 、*房间号*、*开始时间*、*结束时间*、*送风模式*、*风速* 、*用电度数* 、*电费* 。收到新的**送风请求**或**室内机**关机,**空调使用记录**结束记录。维护**请求队列** 和**送风队列** ,使得*送风队列长度* < *最大送风队列长度* ,**请求队列** 中任意*送风请求风速* 均不高于**送风队列**中所有*送风请求风速* ,且**请求队列** 中任意与**送风队列**中最低*送风请求风速* 相同的**送风请求**的*发送时间* 与当前时间之差不大于*最大等待时间* (默认值为s秒) ,以满足调度需求。**送风请求**从**送风队列**移动到**请求队列**后,其*发送时间* 更新为当前时间。 24 | - **温度传感器**的测试输入数据构造方法为: 25 | - 空调开机,向设定温度变化,中风模式每分钟变化0.5℃,高风模式每分钟变化0.6℃,低风模式每分钟变化0.4℃。 26 | - 空调关机或待机,向初始温度变化,每分钟变化0.5℃。 27 | 28 | - **酒店经理**可查看某时间范围内的格式化统计报表。 29 | 30 | 报表分为**日报表**、**周报表**、**月报表**、**年报表** 31 | 32 | 报表内容:*房客编号、房间号、发送送风请求时间、发送停止送风请求时间、发送请求时初始温度和目标温度、每次送风服务选择的送风模式、发送送风及停止送风请求次数、每次送风服务用电度数、每次送风服务费用、每日/周/月空调费用、每日/周/月空调用电度数* 33 | 34 | 35 | 36 | ## 领域模型 37 | 38 | ### 类图 39 | 40 | ```mermaid 41 | classDiagram 42 | class Server{ 43 | 44 | } 45 | class Client{ 46 | string room_number 47 | } 48 | Server "1" o-- "*" CentralAirConditionerMainFrame: Aggregation 49 | Server "1" o-- "*" RoomAirConditionerUsage: Aggregation 50 | Server "1" o-- "*" RoomRequest: Aggregation 51 | class CentralAirConditionerMainFrame{ 52 | int max_air_supply_number 53 | int max_waiting_time 54 | bool working 55 | string mode 56 | double basic_charge_unit 57 | double electricity_rate 58 | %% 各档功率 59 | double mode_power[] 60 | } 61 | class RoomAirConditionerUsage{ 62 | string guest_number 63 | string room_number 64 | timestamp start_time 65 | timestamp finish_time 66 | string mode 67 | %% 风速 68 | string fan_speed 69 | %% 用电度数 70 | double reading 71 | double electricity_bill 72 | } 73 | class RoomRequest{ 74 | string guest_number 75 | string room_number 76 | timestamp request_time 77 | string mode 78 | string fan_speed 79 | int target_temperature 80 | bool satisfied 81 | } 82 | Client "1" o-- "0..1" RoomRequest: Aggregation 83 | Client "1" o-- "1" RoomTemperatureSensor: Aggregation 84 | Client "1" o-- "1" RoomAirConditioner: Aggregation 85 | 86 | class RoomAirConditioner 87 | class RoomTemperatureSensor 88 | class User{ 89 | string name 90 | string password 91 | timestamp register_time 92 | bool valid 93 | } 94 | class Staff{ 95 | int level 96 | string permissions[] 97 | } 98 | class Guest{ 99 | string guest_number 100 | } 101 | class Reception 102 | class AirConditionerAdministrator 103 | class HotelManager 104 | class ControlPanel{ 105 | double room_temperature 106 | int target_temperature 107 | string mode 108 | string fan_speed 109 | double reading 110 | double electricity_bill 111 | } 112 | 113 | User <|--Guest: Inheritance 114 | User <|--Staff: Inheritance 115 | Staff <|-- Reception: Inheritance 116 | Staff <|-- AirConditionerAdministrator: Inheritance 117 | Staff <|-- HotelManager: Inheritance 118 | Guest --> ControlPanel: Association 119 | ControlPanel --> Client: Association 120 | Reception --> Server: Association 121 | AirConditionerAdministrator --> Server: Association 122 | HotelManager --> Server: Association 123 | 124 | ``` 125 | 126 | 127 | 128 | ### 活动图 129 | 130 | ```mermaid 131 | graph TB 132 | 133 | A0-->B0 134 | B1-->A1 135 | A5-->B2 136 | C0-->A0 137 | A1-->C1 138 | A2-->C1 139 | A3-->C1 140 | A4-->C1 141 | A1-->D0 142 | A2-->D0 143 | A3-->D0 144 | A4-->D0 145 | B3-->FinalState 146 | subgraph 空调管理员 147 | InitialState((Initial))-->C[打开中央空调] 148 | C-->C0[设定中央空调送风模式] 149 | C0-->C1[监控各客房空调使用情况] 150 | end 151 | 152 | subgraph 房客 153 | A0[入住] 154 | A1[开空调]-->A2[设定温度] 155 | A2-->A3[设定风速] 156 | A3-->A4[关空调] 157 | A4-->A5[退房] 158 | FinalState((Final)) 159 | style FinalState stroke-width:4px, stroke-width:8px 160 | end 161 | 162 | subgraph 酒店前台 163 | B0[分配客房]-->B1[开始空调计费] 164 | B2[结束空调计费]-->B3[出具空调账单及详单] 165 | end 166 | 167 | subgraph 酒店经理 168 | D0[查看报表] 169 | end 170 | ``` 171 | 172 | -------------------------------------------------------------------------------- /frontend/src/components/Administrator/RoomDetailForAdmin.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 112 | 113 | 118 | -------------------------------------------------------------------------------- /frontendDevServer/server/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, HttpResponse 2 | from django.http import FileResponse 3 | from django.db import models 4 | from django.utils.http import http_date 5 | from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet 6 | from rest_framework.authentication import SessionAuthentication 7 | from rest_framework.response import Response 8 | from rest_framework import status 9 | from django.views.decorators.csrf import csrf_exempt 10 | from .serializers import * 11 | from .models import * 12 | from django.conf import settings 13 | import os,re,stat,mimetypes,json 14 | import logging 15 | logger = logging.getLogger('') 16 | 17 | def index(request): 18 | print(request.META) 19 | return render(request, 'index.html', {}) 20 | 21 | 22 | # 下载大文件 23 | def bigFile(request, path): 24 | fileName = settings.MEDIA_ROOT + path 25 | if not os.path.exists(fileName): 26 | return HttpResponse('你所访问的页面不存在', status=404) 27 | statobj = os.stat(fileName) 28 | # 获取文件的content_type 29 | content_type, encoding = mimetypes.guess_type(fileName) 30 | content_type = content_type or 'application/octet-stream' 31 | 32 | # 计算读取文件的起始位置 33 | start_bytes = re.search( 34 | r'bytes=(\d+)-', request.META.get('HTTP_RANGE', ''), re.S) 35 | start_bytes = int(start_bytes.group(1)) if start_bytes else 0 36 | 37 | # 打开文件并移动下标到起始位置,客户端点击继续下载时,从上次断开的点继续读取 38 | the_file = open(fileName, 'rb') 39 | the_file.seek(start_bytes, os.SEEK_SET) 40 | 41 | # status=200表示下载开始,status=206表示下载暂停后继续,为了兼容火狐浏览器而区分两种状态 42 | # FileResponse默认block_size = 4096,因此迭代器每次读取4KB数据 43 | response = FileResponse( 44 | the_file, content_type=content_type, status=206 if start_bytes > 0 else 200) 45 | 46 | # 'Last-Modified'表示文件修改时间,与'HTTP_IF_MODIFIED_SINCE'对应使用,参考:https://www.jianshu.com/p/b4ecca41bbff 47 | response['Last-Modified'] = http_date(statobj.st_mtime) 48 | 49 | # 这里'Content-Length'表示剩余待传输的文件字节长度 50 | if stat.S_ISREG(statobj.st_mode): 51 | response['Content-Length'] = statobj.st_size - start_bytes 52 | if encoding: 53 | response['Content-Encoding'] = encoding 54 | 55 | # 'Content-Range'的'/'之前描述响应覆盖的文件字节范围,起始下标为0,'/'之后描述整个文件长度,与'HTTP_RANGE'对应使用 56 | response['Content-Range'] = 'bytes %s-%s/%s' % ( 57 | start_bytes, statobj.st_size - 1, statobj.st_size) 58 | 59 | # 'Cache-Control'控制浏览器缓存行为,此处禁止浏览器缓存 60 | response['Cache-Control'] = 'no-cache, no-store, must-revalidate' 61 | return response 62 | 63 | 64 | class CsrfExemptSessionAuthentication(SessionAuthentication): 65 | """ 66 | 去除csrf检测 67 | """ 68 | 69 | def enforce_csrf(self, request): 70 | return 71 | 72 | 73 | # 访问房间列表(直接关联model),只允许Get操作 74 | class RoomViewSet(ModelViewSet): 75 | queryset = Room.objects.all() 76 | serializer_class = RoomSerializer 77 | authentication_classes = (CsrfExemptSessionAuthentication,) 78 | http_method_names = ['get'] 79 | 80 | def list(self, request, *args, **kwargs): 81 | serializer = RoomListSerializer(self.get_queryset(), many=True) 82 | try: 83 | orderlist = sorted( 84 | serializer.data, key=lambda x: int(x['name'], 16)) 85 | return Response(data=orderlist, headers={'Access-Control-Allow-Origin': '*'}) 86 | except ValueError: 87 | return Response(data=serializer.data, headers={'Access-Control-Allow-Origin': '*'}) 88 | 89 | def retrieve(self, request, *args, **kwargs): 90 | instance = self.get_object() 91 | serializer = self.get_serializer(instance) 92 | return Response(serializer.data, headers={'Access-Control-Allow-Origin': '*'}) 93 | 94 | def create(self, request, *args, **kwargs): 95 | serializer = self.serializer_class(data=request.data) 96 | if serializer.is_valid(): 97 | logger.info('create new room') 98 | serializer.save() 99 | return Response(data=serializer.data, status=status.HTTP_201_CREATED) 100 | logger.error('create new room error: {}'.format(serializer.errors)) 101 | return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) 102 | 103 | def update(self, request, *args, **kwargs): 104 | instance = self.get_object() 105 | serializer = self.serializer_class(instance, data=request.data) 106 | if serializer.is_valid(): 107 | logger.info('update new room') 108 | serializer.save() 109 | return Response(data=serializer.data, status=status.HTTP_202_ACCEPTED) 110 | logger.error('update new room error: {}'.format(serializer.errors)) 111 | return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) 112 | 113 | 114 | class SoftwareViewSet(ModelViewSet): 115 | queryset = Software.objects.all() 116 | serializer_class = SoftwareSerializer 117 | authentication_classes = (CsrfExemptSessionAuthentication,) 118 | 119 | 120 | # 访问设备状态列表,只允许Get操作,不允许其他 121 | class RoomStatusViewSet(ModelViewSet): 122 | queryset = RoomStatus.objects.all() 123 | serializer_class = RoomStatusSerializer 124 | http_method_names = ['get'] 125 | authentication_classes = (CsrfExemptSessionAuthentication,) 126 | 127 | def list(self, request, *args, **kwargs): 128 | queryset = self.filter_queryset(self.get_queryset()) 129 | 130 | page = self.paginate_queryset(queryset) 131 | if page is not None: 132 | serializer = self.get_serializer(page, many=True) 133 | return self.get_paginated_response(serializer.data) 134 | 135 | serializer = self.get_serializer(queryset, many=True) 136 | return Response(serializer.data, headers={'Access-Control-Allow-Origin': '*'}) 137 | 138 | def retrieve(self, request, *args, **kwargs): 139 | instance = self.get_object() 140 | serializer = self.get_serializer(instance) 141 | return Response(serializer.data, headers={'Access-Control-Allow-Origin': '*'}) 142 | 143 | 144 | @csrf_exempt 145 | def shutdown(request): 146 | logger.warn('client:{} request shutdown server, shutdown at 30s later'.format(request.META['REMOTE_ADDR'])) 147 | os.system("shutdown -s -t 30") 148 | return HttpResponse(status=200) 149 | -------------------------------------------------------------------------------- /frontendDevServer/roomMonitor/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for roomMonitor project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'p)k9_d6+)d-l)-jt(y9(4d6aeeo4b9dox%)c%zihnxon^kop-8' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = False 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'channels', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'rest_framework', 42 | 'server' 43 | ] 44 | 45 | ASGI_APPLICATION = 'roomMonitor.routing.application' 46 | 47 | CHANNEL_LAYERS={ 48 | 'default':{ 49 | 'BACKEND':'channels_redis.core.RedisChannelLayer', 50 | 'CONFIG':{ 51 | 'hosts':[('127.0.0.1',6379)], 52 | } 53 | } 54 | } 55 | 56 | 57 | MIDDLEWARE = [ 58 | 'django.middleware.security.SecurityMiddleware', 59 | 'django.contrib.sessions.middleware.SessionMiddleware', 60 | 'django.middleware.common.CommonMiddleware', 61 | 'django.middleware.csrf.CsrfViewMiddleware', 62 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 63 | 'django.contrib.messages.middleware.MessageMiddleware', 64 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 65 | ] 66 | 67 | ROOT_URLCONF = 'roomMonitor.urls' 68 | 69 | TEMPLATES = [ 70 | { 71 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 72 | 'DIRS': ['client'], 73 | 'APP_DIRS': True, 74 | 'OPTIONS': { 75 | 'context_processors': [ 76 | 'django.template.context_processors.debug', 77 | 'django.template.context_processors.request', 78 | 'django.contrib.auth.context_processors.auth', 79 | 'django.contrib.messages.context_processors.messages', 80 | ], 81 | }, 82 | }, 83 | ] 84 | 85 | WSGI_APPLICATION = 'roomMonitor.wsgi.application' 86 | 87 | 88 | # Database 89 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 90 | 91 | DATABASES = { 92 | 'default': { 93 | 'ENGINE': 'django.db.backends.mysql', 94 | 'NAME': 'roomMonitor', 95 | 'USER':'test', 96 | 'PASSWORD': 'test1234', 97 | 'HOST':'127.0.0.1', 98 | 'PORT':'3306', 99 | } 100 | } 101 | 102 | 103 | # Password validation 104 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 105 | 106 | AUTH_PASSWORD_VALIDATORS = [ 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 109 | }, 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 112 | }, 113 | { 114 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 115 | }, 116 | { 117 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 118 | }, 119 | ] 120 | 121 | 122 | # Internationalization 123 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 124 | 125 | LANGUAGE_CODE = 'en-us' 126 | 127 | TIME_ZONE = 'Asia/Shanghai' 128 | 129 | USE_I18N = True 130 | 131 | USE_L10N = True 132 | 133 | USE_TZ = False 134 | 135 | 136 | # Static files (CSS, JavaScript, Images) 137 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 138 | 139 | STATIC_URL = '/static/' 140 | STATIC_ROOT = os.path.join(BASE_DIR, 'static/') 141 | 142 | MEDIA_URL = '/upload/' 143 | MEDIA_ROOT = os.path.join(BASE_DIR, 'upload/') 144 | 145 | STATICFILES_DIRS = [ 146 | os.path.join(BASE_DIR,'client/static/') 147 | ] 148 | 149 | DATA_UPLOAD_MAX_MEMORY_SIZE = 900*1024*1024 150 | 151 | #log 152 | BASE_LOG_DIR = os.path.join(BASE_DIR, "logs") 153 | 154 | 155 | LOGGING = { 156 | 'version': 1, # 保留的参数,默认是1 157 | 'disable_existing_loggers': False, # 是否禁用已经存在的logger实例 158 | # 日志输出格式的定义 159 | 'formatters': { 160 | 'standard': { # 标准的日志格式化 161 | 'format': '[%(levelname)s]-%(asctime)s <%(module)s>: %(message)s' 162 | }, 163 | 'error': { # 错误日志输出格式 164 | 'format': '[%(levelname)s]-%(asctime)s "%(pathname)s" <%(module)s>: %(message)s' 165 | }, 166 | 'simple': { 167 | 'format': '[%(levelname)s]-%(asctime)s : %(message)s' 168 | }, 169 | 'collect': { 170 | 'format': '%(message)s' 171 | } 172 | }, 173 | # 处理器:需要处理什么级别的日志及如何处理 174 | 'handlers': { 175 | # 将日志打印到终端 176 | 'console': { 177 | 'level': 'DEBUG', # 日志级别 178 | 'class': 'logging.StreamHandler', # 使用什么类去处理日志流 179 | 'formatter': 'simple' # 指定上面定义过的一种日志输出格式 180 | }, 181 | # 默认日志处理器 182 | 'default': { 183 | 'level': 'INFO', 184 | 'class': 'logging.handlers.TimedRotatingFileHandler', # 保存到文件,自动切 185 | 'filename': os.path.join(BASE_LOG_DIR, "room-default.log"), # 日志文件路径 186 | 'backupCount': 5, # 日志文件备份的数量 187 | 'when': 'M', # 日志间隔 188 | 'formatter': 'standard', # 日志输出格式 189 | 'encoding': 'utf-8', 190 | }, 191 | # 日志处理级别warn 192 | 'warn': { 193 | 'level': 'WARN', 194 | 'class': 'logging.handlers.TimedRotatingFileHandler', # 保存到文件,自动切 195 | 'filename': os.path.join(BASE_LOG_DIR, "room-warn.log"), # 日志文件路径 196 | 'backupCount': 5, # 日志文件备份的数量 197 | 'when': 'M', # 日志间隔 198 | 'formatter': 'standard', # 日志格式 199 | 'encoding': 'utf-8', 200 | }, 201 | # 日志级别error 202 | 'error': { 203 | 'level': 'ERROR', 204 | 'class': 'logging.handlers.TimedRotatingFileHandler', # 保存到文件,自动切 205 | 'filename': os.path.join(BASE_LOG_DIR, "room-error.log"), # 日志文件路径 206 | 'backupCount': 5, 207 | 'when': 'M', # 日志间隔 208 | 'formatter': 'error', # 日志格式 209 | 'encoding': 'utf-8', 210 | }, 211 | }, 212 | 213 | 'loggers': { 214 | # 默认的logger应用如下配置 215 | '': { 216 | 'handlers': ['console','default', 'warn', 'error'], 217 | 'level': 'DEBUG', 218 | 'propagate': True, # 如果有父级的logger示例,表示不要向上传递日志流 219 | }, 220 | 'collect': { 221 | 'handlers': ['console', 'default', 'warn', 'error'], 222 | 'level': 'DEBUG', 223 | }, 224 | }, 225 | } 226 | 227 | -------------------------------------------------------------------------------- /frontend/api/roomAPI.js: -------------------------------------------------------------------------------- 1 | /* Http Restful Api 接口,访问服务器获取设备列表和状态 */ 2 | 3 | import axios from "axios"; 4 | 5 | const METHOD = { 6 | http: 0, 7 | websocket: 1 8 | }; 9 | 10 | const MESSAGE = { 11 | ADD_GROUP: 101, 12 | LEAVE_GROUP: 102, 13 | ADD_WHITELIST: 103, 14 | QUERY_DEVICE_STATUS: 104, 15 | 16 | COMMAND: 200, 17 | CMD_CLOSE_SERVER: 210, 18 | 19 | MESSAGE: 300, 20 | MESSAGE_BROAD_CAST: 301, 21 | MSG_ROOM_STATUS: 302, 22 | MSG_ROOM_STATUS_ALL: 303, 23 | MSG_HEART_BEATS: 304, 24 | 25 | OK: 403, 26 | ERROR: 404 27 | } 28 | 29 | const serverUrl = '192.168.0.183:8000'; 30 | 31 | let roomList = []; 32 | let roomMap = new Map(); 33 | let updateMethod = METHOD.http; 34 | let websocket = null; 35 | let interval = null; 36 | 37 | 38 | //设置更新模式 39 | function setUpdateMode(mode) { 40 | if (mode in METHOD) { 41 | updateMethod = METHOD[mode]; 42 | } 43 | } 44 | 45 | //初始化设备列表,主要是初始化 逻辑ID和名称之间的对应关系 46 | function initialList() { 47 | clear(); 48 | axios.get("http://" + serverUrl + "/api/rooms/") 49 | .then(response => { 50 | for (let index = 0; index < response.data.length; index++) { 51 | let room = { 52 | roomNumber: response.data[index].roomNumber, 53 | isActive: response.data[index].isActive, 54 | currentTemperature: 20, 55 | currentSpeed: 0, 56 | AC: { 57 | isACActive: false, 58 | targetTemperature: 20, 59 | targetSpeed: 0, 60 | requestStatus: 'ready' 61 | }, 62 | updateTime: null, //该设备更新的时间戳 63 | }; 64 | 65 | roomMap.set(room.roomNumber, room); 66 | roomList.push(room) 67 | } 68 | }) 69 | .catch(error => { 70 | alert("从服务器获取房间列表失败"); 71 | window.console.log("error: " + error); 72 | }); 73 | } 74 | 75 | //获取整个房间列表 76 | function getRoomList() { 77 | return roomList; 78 | } 79 | 80 | //根据房间号来获取对应的设备信息 81 | function getRoomByNumber(targetRoomNumber) { 82 | if (roomList.length === 0) { 83 | return null; 84 | } 85 | for (let currentRoom in roomList) { 86 | if (currentRoom.roomNumber === targetRoomNumber) { 87 | return currentRoom; 88 | } 89 | } 90 | return null; 91 | } 92 | 93 | 94 | //Http 请求更新房间列表状态信息 95 | function updateRoomList(){ 96 | if (updateMethod === METHOD.websocket) { 97 | updateRoomListByWS(); 98 | } else { 99 | updateRoomListByHttp(); 100 | } 101 | } 102 | 103 | //通过http轮询的方式监听和更新房间状态 104 | function updateRoomListByHttp() { 105 | interval = window.setInterval(() => { 106 | window.console.log("update by http"); 107 | axios.get("http://" + serverUrl + "/api/room_status/") 108 | .then(response => { 109 | for (let index = 0; index < response.data.length; index++) { 110 | let roomStatus = response.data[index]; 111 | if (roomMap.get(roomStatus.roomNumber)) { 112 | let room = roomMap.get(roomStatus.roomNumber); 113 | room.isActive = roomStatus.isActive; 114 | room.currentTemperature = roomStatus.currentTemperature; 115 | room.currentSpeed = roomStatus.currentSpeed; 116 | room.AC.isACActive = roomStatus.AC.isACActive; 117 | room.AC.targetTemperature = roomStatus.AC.targetTemperature; 118 | room.AC.targetSpeed = roomStatus.AC.targetSpeed; 119 | room.AC.requestStatus = roomStatus.AC.requestStatus; 120 | room.updateTime = new Date().getTime(); 121 | } 122 | } 123 | }) 124 | .catch(error => { 125 | window.console.log("error: " + error); 126 | }); 127 | }, 2000); 128 | } 129 | 130 | //通过websocket的方式监听和更新设备状态 131 | function updateRoomListByWS() { 132 | if (websocket != null) { 133 | websocket.close(); 134 | } 135 | websocket = new WebSocket("ws://" + serverUrl + "/ws/monitor/"); 136 | websocket.onmessage = onMessageReceived; 137 | websocket.onopen = event => { 138 | 139 | webSocketPulse.reset().start(); 140 | let addGroup = { 141 | msgType: 101, 142 | message: "Clients" 143 | }; 144 | 145 | if (websocket.readyState === websocket.OPEN) { 146 | //若是ws开启状态 147 | websocket.send(JSON.stringify(addGroup)); 148 | } else if (websocket.readyState === websocket.CONNECTING) { 149 | // 若是正在开启状态,则等待1s后重新调用 150 | window.setTimeout(() => { 151 | websocket.send(JSON.stringify(addGroup)); 152 | }, 1000); 153 | } 154 | }; 155 | websocket.onclose = event => { 156 | alert("连接关闭"); 157 | window.console.log(event); 158 | }; 159 | websocket.onerror = error => { 160 | alert("获取房间信息失败,连接出错"); 161 | window.console.log(error); 162 | websocket.reconnect(); 163 | }; 164 | 165 | //处理重连 166 | websocket.reconnect = function () { 167 | // Set request interval time to avoid congest 168 | let reconnectTimeout = 5000; 169 | let reconnectTimer = setTimeout(function () { 170 | // Avoid duplicated connection 171 | if (websocket.readyState === 1) { 172 | clearTimeout(reconnectTimer); 173 | } 174 | // Recreate WebSocket Object and reestablish link 175 | else if (websocket.readyState === 2 || websocket.readyState === 3) { 176 | websocket = new WebSocket("ws://" + serverUrl + "/ws/monitor/"); 177 | } 178 | }, reconnectTimeout) 179 | }; 180 | 181 | //每隔一段时间检查房间的连接情况,太久没有更新状态的,视为离线 182 | interval = window.setInterval(() => { 183 | let currentTime = new Date().getTime(); 184 | for (let i = 0; i < roomList.length; i++) { 185 | if (currentTime - roomList[i].updateTime > 3000) { 186 | roomList[i].isActive = false; 187 | } 188 | } 189 | }, 4000); 190 | } 191 | 192 | //处理接收到的数据 193 | function onMessageReceived(event) { 194 | webSocketPulse.reset().start(); 195 | let data = JSON.parse(event.data); 196 | let dataType = data.msgType; 197 | if (dataType === MESSAGE.MSG_ROOM_STATUS) { 198 | let roomStatus = data.message; 199 | if (roomMap.get(roomStatus.roomNumber)) { 200 | let room = roomMap.get(roomStatus.roomNumber); 201 | room.isActive = true; 202 | room.currentTemperature = roomStatus.currentTemperature; 203 | room.currentSpeed = roomStatus.currentSpeed; 204 | room.AC.isACActive = roomStatus.AC.isACActive; 205 | room.AC.targetTemperature = roomStatus.AC.targetTemperature; 206 | room.AC.targetSpeed = roomStatus.AC.targetSpeed; 207 | room.AC.requestStatus = roomStatus.AC.requestStatus; 208 | room.updateTime = new Date().getTime(); 209 | } 210 | } 211 | else { 212 | window.console.log(data.message); 213 | } 214 | } 215 | 216 | 217 | // Websocket Pulse instance 218 | let webSocketPulse = { 219 | pulseTimeout: 30000, 220 | pulseTimer: null, 221 | serverPulseTimer: null, 222 | reset: function () { 223 | clearTimeout(this.pulseTimer); 224 | clearTimeout(this.serverPulseTimer); 225 | return this; 226 | }, 227 | start: function () { 228 | this.pulseTimer = setTimeout(function () { 229 | // Send a pulse to server, and wait for service reply a pulse 230 | wsSendPulse(); 231 | // No response from server, then reconnect to server 232 | //this.serverPulseTimer = setTimeout(function () { 233 | // websocket.close(); 234 | //}, self.pulseTimeout) 235 | }, this.pulseTimeout) 236 | } 237 | }; 238 | 239 | // 心跳包发送函数 240 | function wsSendPulse() { 241 | let req = { 242 | msgType: 304, 243 | message: "echo", 244 | }; 245 | let json = JSON.stringify(req); 246 | websocket.send(json) 247 | } 248 | 249 | //清空列表 250 | function clear() { 251 | roomList = []; 252 | roomMap = new Map(); 253 | } 254 | 255 | //Api释放 256 | function releaseAPI() { 257 | clear(); 258 | if (websocket != null) 259 | websocket.close(); 260 | window.clearInterval(interval); 261 | } 262 | 263 | export default { 264 | setUpdateMode, 265 | initialList, 266 | getRoomList, 267 | getRoomByNumber, 268 | updateRoomList, 269 | releaseAPI 270 | } 271 | -------------------------------------------------------------------------------- /frontend/api/roomAPI.js.backup: -------------------------------------------------------------------------------- 1 | /* Http Restful Api 接口,访问服务器获取设备列表和状态 */ 2 | 3 | import axios from "axios"; 4 | 5 | const METHOD = { 6 | http: 0, 7 | websocket: 1 8 | }; 9 | 10 | const MESSAGE = { 11 | ADD_GROUP: 101, 12 | LEAVE_GROUP: 102, 13 | ADD_WHITELIST: 103, 14 | QUERY_DEVICE_STATUS: 104, 15 | 16 | COMMAND: 200, 17 | CMD_CLOSE_SERVER: 210, 18 | 19 | MESSAGE: 300, 20 | MESSAGE_BROAD_CAST: 301, 21 | MSG_ROOM_STATUS: 302, 22 | MSG_ROOM_STATUS_ALL: 303, 23 | MSG_HEART_BEATS: 304, 24 | 25 | OK: 403, 26 | ERROR: 404 27 | } 28 | 29 | const serverUrl = '192.168.0.183:8000'; 30 | 31 | let roomList = []; 32 | let roomMap = new Map(); 33 | let updateMethod = METHOD.http; 34 | let websocket = null; 35 | let interval = null; 36 | 37 | 38 | //设置更新模式 39 | function setUpdateMode(mode) { 40 | if (mode in METHOD) { 41 | updateMethod = METHOD[mode]; 42 | } 43 | } 44 | 45 | //初始化设备列表,主要是初始化 逻辑ID和名称之间的对应关系 46 | function initialList() { 47 | clear(); 48 | axios.get("http://" + serverUrl + "/api/rooms/") 49 | .then(response => { 50 | for (let index = 0; index < response.data.length; index++) { 51 | let room = { 52 | roomNumber: response.data[index].roomNumber, 53 | isActive: response.data[index].isActive, 54 | currentTemperature: 20, 55 | currentSpeed: 0, 56 | AC: { 57 | isACActive: false, 58 | targetTemperature: 20, 59 | targetSpeed: 0, 60 | requestStatus: 'ready' 61 | }, 62 | updateTime: null, //该设备更新的时间戳 63 | }; 64 | 65 | roomMap.set(room.roomNumber, room); 66 | roomList.push(room) 67 | } 68 | }) 69 | .catch(error => { 70 | alert("从服务器获取房间列表失败"); 71 | window.console.log("error: " + error); 72 | }); 73 | } 74 | 75 | //获取整个房间列表 76 | function getRoomList() { 77 | return roomList; 78 | } 79 | 80 | //根据房间号来获取对应的设备信息 81 | function getRoomByNumber(targetRoomNumber) { 82 | if (roomList.length === 0) { 83 | return null; 84 | } 85 | for (let currentRoom in roomList) { 86 | if (currentRoom.roomNumber === targetRoomNumber) { 87 | return currentRoom; 88 | } 89 | } 90 | return null; 91 | } 92 | 93 | 94 | //Http 请求更新房间列表状态信息 95 | function updateRoomList(){ 96 | if (updateMethod === METHOD.websocket) { 97 | updateRoomListByWS(); 98 | } else { 99 | updateRoomListByHttp(); 100 | } 101 | } 102 | 103 | //通过http轮询的方式监听和更新房间状态 104 | function updateRoomListByHttp() { 105 | interval = window.setInterval(() => { 106 | window.console.log("update by http"); 107 | axios.get("http://" + serverUrl + "/api/room_status/") 108 | .then(response => { 109 | for (let index = 0; index < response.data.length; index++) { 110 | let roomStatus = response.data[index]; 111 | if (roomMap.get(roomStatus.roomNumber)) { 112 | let room = roomMap.get(roomStatus.roomNumber); 113 | room.isActive = roomStatus.isActive; 114 | room.currentTemperature = roomStatus.currentTemperature; 115 | room.currentSpeed = roomStatus.currentSpeed; 116 | room.AC.isACActive = roomStatus.AC.isACActive; 117 | room.AC.targetTemperature = roomStatus.AC.targetTemperature; 118 | room.AC.targetSpeed = roomStatus.AC.targetSpeed; 119 | room.AC.requestStatus = roomStatus.AC.requestStatus; 120 | room.updateTime = new Date().getTime(); 121 | } 122 | } 123 | }) 124 | .catch(error => { 125 | window.console.log("error: " + error); 126 | }); 127 | }, 2000); 128 | } 129 | 130 | //通过websocket的方式监听和更新设备状态 131 | function updateRoomListByWS() { 132 | if (websocket != null) { 133 | websocket.close(); 134 | } 135 | websocket = new WebSocket("ws://" + serverUrl + "/ws/monitor/"); 136 | websocket.onmessage = onMessageReceived; 137 | websocket.onopen = event => { 138 | 139 | webSocketPulse.reset().start(); 140 | let addGroup = { 141 | msgType: 101, 142 | message: "Clients" 143 | }; 144 | 145 | if (websocket.readyState === websocket.OPEN) { 146 | //若是ws开启状态 147 | websocket.send(JSON.stringify(addGroup)); 148 | } else if (websocket.readyState === websocket.CONNECTING) { 149 | // 若是正在开启状态,则等待1s后重新调用 150 | window.setTimeout(() => { 151 | websocket.send(JSON.stringify(addGroup)); 152 | }, 1000); 153 | } 154 | }; 155 | websocket.onclose = event => { 156 | alert("连接关闭"); 157 | window.console.log(event); 158 | }; 159 | websocket.onerror = error => { 160 | alert("获取房间信息失败,连接出错"); 161 | window.console.log(error); 162 | websocket.reconnect(); 163 | }; 164 | 165 | //处理重连 166 | websocket.reconnect = function () { 167 | // Set request interval time to avoid congest 168 | let reconnectTimeout = 5000; 169 | let reconnectTimer = setTimeout(function () { 170 | // Avoid duplicated connection 171 | if (websocket.readyState === 1) { 172 | clearTimeout(reconnectTimer); 173 | } 174 | // Recreate WebSocket Object and reestablish link 175 | else if (websocket.readyState === 2 || websocket.readyState === 3) { 176 | websocket = new WebSocket("ws://" + serverUrl + "/ws/monitor/"); 177 | } 178 | }, reconnectTimeout) 179 | }; 180 | 181 | //每隔一段时间检查房间的连接情况,太久没有更新状态的,视为离线 182 | interval = window.setInterval(() => { 183 | let currentTime = new Date().getTime(); 184 | for (let i = 0; i < roomList.length; i++) { 185 | if (currentTime - roomList[i].updateTime > 3000) { 186 | roomList[i].isActive = false; 187 | } 188 | } 189 | }, 4000); 190 | } 191 | 192 | //处理接收到的数据 193 | function onMessageReceived(event) { 194 | webSocketPulse.reset().start(); 195 | let data = JSON.parse(event.data); 196 | let dataType = data.msgType; 197 | if (dataType === MESSAGE.MSG_ROOM_STATUS) { 198 | let roomStatus = data.message; 199 | if (roomMap.get(roomStatus.roomNumber)) { 200 | let room = roomMap.get(roomStatus.roomNumber); 201 | room.isActive = true; 202 | room.currentTemperature = roomStatus.currentTemperature; 203 | room.currentSpeed = roomStatus.currentSpeed; 204 | room.AC.isACActive = roomStatus.AC.isACActive; 205 | room.AC.targetTemperature = roomStatus.AC.targetTemperature; 206 | room.AC.targetSpeed = roomStatus.AC.targetSpeed; 207 | room.AC.requestStatus = roomStatus.AC.requestStatus; 208 | room.updateTime = new Date().getTime(); 209 | } 210 | } 211 | else { 212 | window.console.log(data.message); 213 | } 214 | } 215 | 216 | 217 | // Websocket Pulse instance 218 | let webSocketPulse = { 219 | pulseTimeout: 30000, 220 | pulseTimer: null, 221 | serverPulseTimer: null, 222 | reset: function () { 223 | clearTimeout(this.pulseTimer); 224 | clearTimeout(this.serverPulseTimer); 225 | return this; 226 | }, 227 | start: function () { 228 | this.pulseTimer = setTimeout(function () { 229 | // Send a pulse to server, and wait for service reply a pulse 230 | wsSendPulse(); 231 | // No response from server, then reconnect to server 232 | //this.serverPulseTimer = setTimeout(function () { 233 | // websocket.close(); 234 | //}, self.pulseTimeout) 235 | }, this.pulseTimeout) 236 | } 237 | }; 238 | 239 | // 心跳包发送函数 240 | function wsSendPulse() { 241 | let req = { 242 | msgType: 304, 243 | message: "echo", 244 | }; 245 | let json = JSON.stringify(req); 246 | websocket.send(json) 247 | } 248 | 249 | //清空列表 250 | function clear() { 251 | roomList = []; 252 | roomMap = new Map(); 253 | } 254 | 255 | //Api释放 256 | function releaseAPI() { 257 | clear(); 258 | if (websocket != null) 259 | websocket.close(); 260 | window.clearInterval(interval); 261 | } 262 | 263 | export default { 264 | setUpdateMode, 265 | initialList, 266 | getRoomList, 267 | getRoomByNumber, 268 | updateRoomList, 269 | releaseAPI 270 | } 271 | -------------------------------------------------------------------------------- /backend/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-06-28 12:29 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ACAdministrator', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=128, unique=True)), 21 | ('password', models.CharField(default='', max_length=256)), 22 | ('c_time', models.DateTimeField(auto_now_add=True)), 23 | ], 24 | options={ 25 | 'verbose_name': '空调管理员', 26 | 'verbose_name_plural': '空调管理员', 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='CentralAirConditioner', 31 | fields=[ 32 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('ac_state', models.CharField(choices=[('close', '关闭'), ('set_Model', '设置模式'), ('ready', '就绪'), ('none', '备用')], default='close', max_length=64, verbose_name='中央空调状态')), 34 | ('temp_mode', models.SmallIntegerField(choices=[(1, '制冷模式'), (-1, '制热模式')], default=1, verbose_name='制冷制热模式')), 35 | ('cool_temp_highlimit', models.IntegerField(default=25, verbose_name='制冷温控范围最高温')), 36 | ('cool_temp_lowlimit', models.IntegerField(default=18, verbose_name='制冷温控范围最低温')), 37 | ('heat_temp_highlimit', models.IntegerField(default=30, verbose_name='制热温控范围最高温')), 38 | ('heat_temp_lowlimit', models.IntegerField(default=25, verbose_name='制热温控范围最低温')), 39 | ('default_temp', models.IntegerField(default=25, verbose_name='缺省温度')), 40 | ('feerate_H', models.FloatField(default=0.016666666666666666, verbose_name='高风费率')), 41 | ('feerate_M', models.FloatField(default=0.008333333333333333, verbose_name='中风费率')), 42 | ('feerate_L', models.FloatField(default=0.005555555555555556, verbose_name='低风费率')), 43 | ('max_load', models.IntegerField(default=3, verbose_name='最大带机量')), 44 | ('waiting_duration', models.IntegerField(default=120)), 45 | ], 46 | ), 47 | migrations.CreateModel( 48 | name='Manager', 49 | fields=[ 50 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 51 | ('name', models.CharField(max_length=128, unique=True)), 52 | ('password', models.CharField(default='', max_length=256)), 53 | ('c_time', models.DateTimeField(auto_now_add=True)), 54 | ], 55 | options={ 56 | 'verbose_name': '酒店经理', 57 | 'verbose_name_plural': '酒店经理', 58 | }, 59 | ), 60 | migrations.CreateModel( 61 | name='requestQueue', 62 | fields=[ 63 | ('room_id', models.CharField(max_length=64, primary_key=True, serialize=False, verbose_name='房间号')), 64 | ('room_state', models.SmallIntegerField(choices=[(0, '关闭'), (1, '运行'), (2, '挂起')], default=2, verbose_name='房间送风状态')), 65 | ('temp_mode', models.SmallIntegerField(choices=[(1, '制冷模式'), (-1, '制热模式')], default=1, verbose_name='制冷制热模式')), 66 | ('blow_mode', models.SmallIntegerField(choices=[(0, '低风'), (1, '中风'), (2, '高风')], default=1, verbose_name='送风模式')), 67 | ('request_timestamp', models.DateTimeField(auto_now_add=True, verbose_name='请求送风时间戳')), 68 | ('air_timestamp', models.DateTimeField(null=True, verbose_name='开始送风时间戳')), 69 | ('service_duration', models.IntegerField(default=0, verbose_name='当前服务时长(秒)')), 70 | ], 71 | ), 72 | migrations.CreateModel( 73 | name='RequestRecord', 74 | fields=[ 75 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 76 | ('room_id', models.CharField(max_length=64, verbose_name='房间号')), 77 | ('room_state', models.SmallIntegerField(choices=[(0, '关闭'), (1, '运行'), (2, '挂起')], default=2, verbose_name='房间送风状态')), 78 | ('temp_mode', models.SmallIntegerField(choices=[(1, '制冷模式'), (-1, '制热模式')], default=2, verbose_name='送风模式')), 79 | ('start_temp', models.IntegerField(verbose_name='初始温度')), 80 | ('target_temp', models.IntegerField(verbose_name='目标温度')), 81 | ('request_time', models.DateTimeField()), 82 | ('finished', models.IntegerField(default=0, verbose_name='结束')), 83 | ], 84 | ), 85 | migrations.CreateModel( 86 | name='Room', 87 | fields=[ 88 | ('room_id', models.CharField(max_length=64, primary_key=True, serialize=False, unique=True, verbose_name='房间号')), 89 | ('room_state', models.SmallIntegerField(choices=[(0, '关闭'), (1, '运行'), (2, '挂起')], default=0, verbose_name='房间从控机状态')), 90 | ('temp_mode', models.SmallIntegerField(choices=[(1, '制冷模式'), (-1, '制热模式')], default=1, verbose_name='制冷制热模式')), 91 | ('blow_mode', models.SmallIntegerField(choices=[(0, '低风'), (1, '中风'), (2, '高风')], default=1, verbose_name='送风模式')), 92 | ('current_temp', models.IntegerField(verbose_name='当前温度')), 93 | ('target_temp', models.IntegerField(default=25, verbose_name='目标温度')), 94 | ('fee', models.FloatField(verbose_name='总费用')), 95 | ('duration', models.IntegerField(verbose_name='服务时长(秒)')), 96 | ], 97 | ), 98 | migrations.CreateModel( 99 | name='RoomDailyReport', 100 | fields=[ 101 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 102 | ('room_id', models.CharField(max_length=64, verbose_name='房间号')), 103 | ('date', models.DateField(auto_now=True, verbose_name='日期')), 104 | ('switch_count', models.IntegerField(default=0, verbose_name='开关次数')), 105 | ('schedule_count', models.IntegerField(default=0, verbose_name='被调度次数')), 106 | ('change_temp_count', models.IntegerField(default=0, verbose_name='调温次数')), 107 | ('change_speed_count', models.IntegerField(default=0, verbose_name='调风次数')), 108 | ], 109 | ), 110 | migrations.CreateModel( 111 | name='TemperatureSensor', 112 | fields=[ 113 | ('room_id', models.CharField(max_length=64, primary_key=True, serialize=False, unique=True, verbose_name='房间号')), 114 | ('init_temp', models.FloatField(verbose_name='初始温度')), 115 | ('current_temp', models.FloatField(verbose_name='当前温度')), 116 | ('last_update', models.DateTimeField(default=datetime.datetime(2020, 6, 28, 12, 29, 57, 12718), verbose_name='上次更新时间')), 117 | ], 118 | ), 119 | migrations.CreateModel( 120 | name='Tenant', 121 | fields=[ 122 | ('name', models.CharField(max_length=128, unique=True)), 123 | ('password', models.CharField(default='', max_length=256)), 124 | ('c_time', models.DateTimeField(auto_now_add=True)), 125 | ('room_id', models.CharField(max_length=64, primary_key=True, serialize=False, verbose_name='房间号')), 126 | ('date_in', models.DateField(null=True, verbose_name='入住日期')), 127 | ('date_out', models.DateField(null=True, verbose_name='登出日期')), 128 | ], 129 | options={ 130 | 'verbose_name': '房客', 131 | 'verbose_name_plural': '房客', 132 | 'ordering': ['-c_time'], 133 | }, 134 | ), 135 | migrations.CreateModel( 136 | name='Waiter', 137 | fields=[ 138 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 139 | ('name', models.CharField(max_length=128, unique=True)), 140 | ('password', models.CharField(default='', max_length=256)), 141 | ('c_time', models.DateTimeField(auto_now_add=True)), 142 | ], 143 | options={ 144 | 'verbose_name': '前台服务员', 145 | 'verbose_name_plural': '前台服务员', 146 | }, 147 | ), 148 | migrations.CreateModel( 149 | name='ServiceRecord', 150 | fields=[ 151 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 152 | ('blow_mode', models.SmallIntegerField(choices=[(0, '低风'), (1, '中风'), (2, '高风')], default=2, verbose_name='送风模式')), 153 | ('start_time', models.DateTimeField()), 154 | ('end_time', models.DateTimeField()), 155 | ('service_time', models.DurationField()), 156 | ('power_comsumption', models.FloatField(verbose_name='用电度数')), 157 | ('now_temp', models.FloatField(verbose_name='当前温度')), 158 | ('fee_rate', models.FloatField(choices=[(0.01666, '高风单位耗电量(度/秒)'), (0.00833, '中风单位耗电量(度/秒)'), (0.00556, '低风单位耗电量(度/秒)')], default=2, verbose_name='费率')), 159 | ('fee', models.FloatField()), 160 | ('RR', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.RequestRecord')), 161 | ], 162 | ), 163 | ] 164 | -------------------------------------------------------------------------------- /doc/用例模型-房客.md: -------------------------------------------------------------------------------- 1 | # 用例图 2 | 3 | 4 | 5 | 或者使用`markdown`扩展`plantuml-markdown`查看,具体安装操作请参阅`README.md` 6 | 7 | ```python 8 | ::uml:: format="png" classes="uml useCaseDiagram" alt="Tenant Use Case Diagram" title="Tenant Use Case Diagram" width="1000px" height="400px" 9 | left to right direction 10 | 11 | :房客 Tenant: as Tenant 12 | 13 | (房间空调管理 Room AC Management) as (Management) 14 | (开启空调 Enable AC) as (Enable) 15 | (关闭空调 Disable AC) as (Disable) 16 | (调整温度 AC Temperature Adjustment) as (Temperature) 17 | (调整风速 AC Speed Adjustment) as (Speed) 18 | 19 | Tenant -- (Management) 20 | 21 | (Management) ..> (Enable) : <> 22 | (Management) ..> (Disable) : <> 23 | (Management) ..> (Temperature) : <> 24 | (Management) ..> (Speed) : <> 25 | ::end-uml:: 26 | ``` 27 | 28 | --- 29 | 30 | # 用例说明 31 | 32 | | 用例编号 | 房客1 | 33 | | -------------------- | --------------------------------------------- | 34 | | 用例名称 | 开启空调 | 35 | | 范围 | 客户端 | 36 | | 级别 | 用例 | 37 | | 参与者 | 房客 | 38 | | 项目相关人员及其兴趣 | 房客需要开启空调 | 39 | | 前置条件 | 中央空调工作,房客空调不在工作 | 40 | | 后置条件 | 记录工作状态 | 41 | | 主要成功场景 | N/A | 42 | | 1 | 房客选择“开启空调” | 43 | | 2 | 房客启动空调 | 44 | | 3 | 温控系统启动空调 | 45 | | 4 | 房客关闭“开启空调” | 46 | | 扩展(或替代流程) | N/A | 47 | | 2a | 房客房间空调工作异常,提示开启空调失败,返回2 | 48 | | 2b | 房客房间空调已开启,隐藏开启空调操作,返回2 | 49 | | 特殊需求 | 暂无 | 50 | | 技术与数据的变化列表 | 暂无 | 51 | | 发生频率 | 当房客开启空调时发生 | 52 | | 待解决的问题 | 暂无 | 53 | 54 | | 用例编号 | 房客2 | 55 | | -------------------- | --------------------------------------------- | 56 | | 用例名称 | 关闭空调 | 57 | | 范围 | 客户端 | 58 | | 级别 | 用例 | 59 | | 参与者 | 房客 | 60 | | 项目相关人员及其兴趣 | 房客需要关闭空调 | 61 | | 前置条件 | 中央空调工作,房客空调正在工作 | 62 | | 后置条件 | 记录工作状态 | 63 | | 主要成功场景 | N/A | 64 | | 1 | 房客选择“关闭空调” | 65 | | 2 | 房客关闭空调 | 66 | | 3 | 温控系统关闭空调 | 67 | | 4 | 房客关闭“关闭空调” | 68 | | 扩展(或替代流程) | N/A | 69 | | 2a | 房客房间空调工作异常,提示关闭空调失败,返回2 | 70 | | 2b | 房客房间空调已关闭,隐藏关闭空调操作,返回2 | 71 | | 特殊需求 | 暂无 | 72 | | 技术与数据的变化列表 | 暂无 | 73 | | 发生频率 | 当房客开启空调时发生 | 74 | | 待解决的问题 | 暂无 | 75 | 76 | | 用例编号 | 房客3 | 77 | | -------------------- | ------------------------------------------------- | 78 | | 用例名称 | 调整空调温度 | 79 | | 范围 | 客户端 | 80 | | 级别 | 用例 | 81 | | 参与者 | 房客 | 82 | | 项目相关人员及其兴趣 | 房客需要调整房间当前温度 | 83 | | 前置条件 | 中央空调工作,房客空调正在工作 | 84 | | 后置条件 | 记录工作调整 | 85 | | 主要成功场景 | N/A | 86 | | 1 | 房客选择“调整空调温度” | 87 | | 2 | 房客调整温度 | 88 | | 3 | 温控系统设定空调工作温度为指定温度 | 89 | | 4 | 房客关闭“调整空调温度” | 90 | | 扩展(或替代流程) | N/A | 91 | | 2a | 房客设定温度过于频繁,采用throttle限制操作,返回2 | 92 | | 2b | 房客设定温度超出空调工作能力,提示温度超限,返回2 | 93 | | 特殊需求 | 暂无 | 94 | | 技术与数据的变化列表 | 暂无 | 95 | | 发生频率 | 当房客调整温度时发生 | 96 | | 待解决的问题 | throttle节流限制设置时间未定 | 97 | 98 | | 用例编号 | 房客4 | 99 | | -------------------- | ------------------------------------------------- | 100 | | 用例名称 | 调整空调风速 | 101 | | 范围 | 客户端 | 102 | | 级别 | 用例 | 103 | | 参与者 | 房客 | 104 | | 项目相关人员及其兴趣 | 房客需要调整房间当前风速 | 105 | | 前置条件 | 中央空调工作,房客空调正在工作 | 106 | | 后置条件 | 记录工作调整 | 107 | | 主要成功场景 | N/A | 108 | | 1 | 房客选择“调整空调风速” | 109 | | 2 | 房客调整温度 | 110 | | 3 | 温控系统设定空调工作风速为指定风速 | 111 | | 4 | 房客关闭“调整空调风速” | 112 | | 扩展(或替代流程) | N/A | 113 | | 2a | 房客设定风速过于频繁,采用throttle限制操作,返回2 | 114 | | 2b | 房客设定风速超出空调工作能力,提示风速超限,返回2 | 115 | | 特殊需求 | 暂无 | 116 | | 技术与数据的变化列表 | 暂无 | 117 | | 发生频率 | 当房客调整风速时发生 | 118 | | 待解决的问题 | throttle节流限制设置时间未定 | 119 | 120 | --- 121 | 122 | # 系统顺序图 123 | 124 | 125 | 126 | 或者使用`markdown`扩展`plantuml-markdown`查看 127 | 128 | ```python 129 | ::uml:: format="png" classes="uml tenantSequenceDiagram" alt="Tenant Sequence Diagram" title="Tenant Sequence Diagram" width="1000px" height="1400px" 130 | actor "房客 Tenant" as Client 131 | entity "服务器 Server" as Server 132 | database "数据库 Database" as DB 133 | 134 | ref over Client, Server, DB: 登录 Login 135 | DB -> Server: 房客入住 Tenant Arrived 136 | Server -> Client: 登陆成功 Login Succeed 137 | 138 | alt 房间空调管理 Room AC Management 139 | 140 | == 开启空调 Enable AC == 141 | 142 | Client->Server: 开启空调 Enable AC 143 | Server->DB: 记录操作 Record Process 144 | DB-->Server: 记录成功 Record Succeed 145 | Server-->Client: 空调已开启 Enabled AC 146 | 147 | == 房间空调参数调整 Room AC Variable Adjustment == 148 | 149 | loop 150 | 151 | Client->Server: 调整温度 AC Temperature Adjustment 152 | Server->DB: 记录操作 Record Process 153 | DB-->Server: 记录成功 Record Succeed 154 | Server-->Client: 温度已调整 AC Temperature Adjustment Success 155 | 156 | Client->Server: 调整风速 AC Speed Adjustment 157 | Server->DB: 记录操作 Record Process 158 | DB-->Server: 记录成功 Record Succeed 159 | Server-->Client: 风速已调整 AC Speed Adjustment Success 160 | 161 | end 162 | 163 | == 关闭空调 Disable AC == 164 | 165 | Client->Server: 关闭空调 Disable AC 166 | Server->DB: 记录操作 Record Process 167 | DB-->Server: 记录成功 Record Succeed 168 | Server-->Client: 空调已关闭 Disabled AC 169 | 170 | end 171 | ::end-uml:: 172 | ``` 173 | 174 | --- 175 | 176 | # 操作契约 177 | 178 | | 系统事件 | 开启空调 enable_ac(void) | 179 | | -------- | ------------------------------ | 180 | | 交叉引用 | 空调管理员业务处理 | 181 | | 前置条件 | 中央空调工作,房客空调不在工作 | 182 | | 后置条件 | 房客空调工作,记录此次开启操作 | 183 | 184 | | 系统事件 | 关闭空调 disable_ac(void) | 185 | | -------- | ------------------------------ | 186 | | 交叉引用 | 空调管理员业务处理 | 187 | | 前置条件 | 中央空调工作,房客空调正在工作 | 188 | | 后置条件 | 房客空调工作,记录此次关闭操作 | 189 | 190 | | 系统事件 | 调整空调温度 ac_temperature_adjustment(target_temperature) | 191 | | -------- | :--------------------------------------------------------- | 192 | | 交叉引用 | 空调管理员业务处理 | 193 | | 前置条件 | 中央空调工作,房客空调正在工作 | 194 | | 后置条件 | 房客空调工作,记录此次温度调整操作 | 195 | 196 | | 系统事件 | 调整空调风速 ac_speed_adjustment(target_speed) | 197 | | -------- | ---------------------------------------------- | 198 | | 交叉引用 | 空调管理员业务处理 | 199 | | 前置条件 | 中央空调工作,房客空调正在工作 | 200 | | 后置条件 | 房客空调工作,记录此次风速调整操作 | -------------------------------------------------------------------------------- /backend/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.utils import timezone 3 | from .models import Room, TemperatureSensor, Tenant, CentralAirConditioner, ACAdministrator, \ 4 | RequestRecord, Waiter, ServiceRecord, Manager, RoomDailyReport 5 | import datetime 6 | import time 7 | 8 | 9 | # Create your tests here. 10 | class ManagerTest(TestCase): 11 | def test_weekly_report(self): 12 | current_time = timezone.now() 13 | date_in = current_time - datetime.timedelta(days=10) 14 | date_out = date_in + datetime.timedelta(days=5) 15 | request_record = RequestRecord( 16 | room_id='房间一', room_state=1, temp_mode=1, 17 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=1) 18 | ) 19 | request_record.save() 20 | 21 | request_record = RequestRecord( 22 | room_id='房间一', room_state=2, temp_mode=1, 23 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=2) 24 | ) 25 | request_record.save() 26 | 27 | request_record = RequestRecord( 28 | room_id='房间一', room_state=1, temp_mode=1, 29 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=3) 30 | ) 31 | request_record.save() 32 | 33 | request_record = RequestRecord( 34 | room_id='房间一', room_state=0, temp_mode=1, 35 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=4) 36 | ) 37 | request_record.save() 38 | 39 | request_record = RequestRecord( 40 | room_id='房间二', room_state=1, temp_mode=1, 41 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=1) 42 | ) 43 | request_record.save() 44 | 45 | request_record = RequestRecord( 46 | room_id='房间二', room_state=0, temp_mode=1, 47 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=2) 48 | ) 49 | request_record.save() 50 | 51 | service_record = ServiceRecord( 52 | RR_id=1, blow_mode=1, start_time=date_in + datetime.timedelta(minutes=1), 53 | end_time=date_in + datetime.timedelta(minutes=2), service_time=datetime.timedelta(minutes=1), 54 | power_comsumption=0.00833 * 60, now_temp=32 - 0.5, fee_rate=1.0, fee=0.00833 * 60 55 | ) 56 | service_record.save() 57 | 58 | service_record = ServiceRecord( 59 | RR_id=5, blow_mode=1, start_time=date_in + datetime.timedelta(minutes=1), 60 | end_time=date_in + datetime.timedelta(minutes=2), service_time=datetime.timedelta(minutes=1), 61 | power_comsumption=0.00833 * 60, now_temp=32 - 0.5, fee_rate=1.0, fee=0.00833 * 60 62 | ) 63 | service_record.save() 64 | 65 | service_record = ServiceRecord( 66 | RR_id=3, blow_mode=1, start_time=date_in + datetime.timedelta(minutes=3), 67 | end_time=date_in + datetime.timedelta(minutes=4), service_time=datetime.timedelta(minutes=1), 68 | power_comsumption=0.00833 * 60, now_temp=32 - 0.5 + 0.5 - 0.5, fee_rate=1.0, fee=0.00833 * 60 69 | ) 70 | service_record.save() 71 | 72 | room_daily_report = RoomDailyReport( 73 | room_id='房间一', date=date_in, switch_count=1, schedule_count=1, 74 | change_temp_count=0, change_speed_count=0 75 | ) 76 | room_daily_report.save() 77 | 78 | room_daily_report = RoomDailyReport( 79 | room_id='房间一', date=date_in + datetime.timedelta(days=1), switch_count=0, schedule_count=0, 80 | change_temp_count=0, change_speed_count=0 81 | ) 82 | room_daily_report.save() 83 | 84 | room_daily_report = RoomDailyReport( 85 | room_id='房间二', date=date_in + datetime.timedelta(days=1), switch_count=0, schedule_count=0, 86 | change_temp_count=0, change_speed_count=0 87 | ) 88 | room_daily_report.save() 89 | 90 | room_daily_report = RoomDailyReport( 91 | room_id='房间二', date=date_in, switch_count=1, schedule_count=0, 92 | change_temp_count=0, change_speed_count=0 93 | ) 94 | room_daily_report.save() 95 | 96 | m = Manager(name='manager1', password='none', c_time=current_time) 97 | report = m.dailyReport(date_in, date_in + datetime.timedelta(days=1)) 98 | 99 | expected_reports = [ 100 | {'room_id': '房间一', 'date': datetime.date(2020, 6, 18), 'switch_count': 1, 'schedule_count': 1, 101 | 'change_temp_count': 0, 'change_speed_count': 0, 'service_time__sum': datetime.timedelta(seconds=120), 102 | 'fee__sum': 0.9996, 'detail_record_count': 2}, 103 | {'room_id': '房间一', 'date': datetime.date(2020, 6, 19), 'switch_count': 0, 'schedule_count': 0, 104 | 'change_temp_count': 0, 'change_speed_count': 0}, 105 | {'room_id': '房间二', 'date': datetime.date(2020, 6, 19), 'switch_count': 0, 'schedule_count': 0, 106 | 'change_temp_count': 0, 'change_speed_count': 0}, 107 | {'room_id': '房间二', 'date': datetime.date(2020, 6, 18), 'switch_count': 1, 'schedule_count': 0, 108 | 'change_temp_count': 0, 'change_speed_count': 0, 'service_time__sum': datetime.timedelta(seconds=60), 109 | 'fee__sum': 0.4998, 'detail_record_count': 1} 110 | ] 111 | 112 | self.assertDictEqual(report[0], expected_reports[0]) 113 | self.assertDictEqual(report[1], expected_reports[1]) 114 | self.assertDictEqual(report[2], expected_reports[2]) 115 | self.assertDictEqual(report[3], expected_reports[3]) 116 | 117 | 118 | ''' 119 | class WaiterTest(TestCase): 120 | def test_RDR_invoice(self): 121 | current_time = timezone.now() 122 | date_in = current_time - datetime.timedelta(days=10) 123 | date_out = date_in + datetime.timedelta(days=5) 124 | request_record = RequestRecord( 125 | room_id='房间一', room_state=1, temp_mode=1, 126 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=1) 127 | ) 128 | request_record.save() 129 | 130 | request_record = RequestRecord( 131 | room_id='房间一', room_state=2, temp_mode=1, 132 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=2) 133 | ) 134 | request_record.save() 135 | 136 | request_record = RequestRecord( 137 | room_id='房间一', room_state=1, temp_mode=1, 138 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=3) 139 | ) 140 | request_record.save() 141 | 142 | request_record = RequestRecord( 143 | room_id='房间一', room_state=0, temp_mode=1, 144 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=4) 145 | ) 146 | request_record.save() 147 | 148 | request_record = RequestRecord( 149 | room_id='房间二', room_state=1, temp_mode=1, 150 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=1) 151 | ) 152 | request_record.save() 153 | 154 | request_record = RequestRecord( 155 | room_id='房间二', room_state=2, temp_mode=1, 156 | start_temp=32, target_temp=27, request_time=date_in + datetime.timedelta(minutes=2) 157 | ) 158 | request_record.save() 159 | 160 | service_record = ServiceRecord( 161 | RR_id=1, blow_mode=1, start_time=date_in + datetime.timedelta(minutes=1), 162 | end_time=date_in + datetime.timedelta(minutes=2), service_time=datetime.timedelta(minutes=1), 163 | power_comsumption=0.00833 * 60, now_temp=32 - 0.5, fee_rate=1.0, fee=0.00833 * 60 164 | ) 165 | service_record.save() 166 | 167 | service_record = ServiceRecord( 168 | RR_id=5, blow_mode=1, start_time=date_in + datetime.timedelta(minutes=1), 169 | end_time=date_in + datetime.timedelta(minutes=2), service_time=datetime.timedelta(minutes=1), 170 | power_comsumption=0.00833 * 60, now_temp=32-0.5, fee_rate=1.0, fee=0.00833 * 60 171 | ) 172 | service_record.save() 173 | 174 | service_record = ServiceRecord( 175 | RR_id=3, blow_mode=1, start_time=date_in + datetime.timedelta(minutes=3), 176 | end_time=date_in + datetime.timedelta(minutes=4), service_time=datetime.timedelta(minutes=1), 177 | power_comsumption=0.00833 * 60, now_temp=32 - 0.5 + 0.5-0.5, fee_rate=1.0, fee=0.00833 * 60 178 | ) 179 | service_record.save() 180 | 181 | w = Waiter(name='waiter1', password='none', c_time=current_time) 182 | detailRecords = w.printRDR('房间一', date_in, date_out) 183 | invoice = w.printInvoice('房间一', date_in, date_out) 184 | print(detailRecords) 185 | expected_detailRecords = [ 186 | dict( 187 | RR_id=1, blow_mode=1, start_time=date_in + datetime.timedelta(minutes=1), 188 | end_time=date_in + datetime.timedelta(minutes=2), id=1, service_time=datetime.timedelta(minutes=1), 189 | power_comsumption=0.00833 * 60, now_temp=31.5, fee_rate=1.0, fee=0.00833 * 60 190 | ), 191 | dict( 192 | RR_id=3, blow_mode=1, start_time=date_in + datetime.timedelta(minutes=3), 193 | end_time=date_in + datetime.timedelta(minutes=4), id=3, service_time=datetime.timedelta(minutes=1), 194 | power_comsumption=0.00833 * 60, now_temp=32 - 0.5 + 0.5 - 0.5, fee_rate=1.0, fee=0.00833 * 60 195 | ) 196 | ] 197 | self.assertDictEqual(detailRecords[0], expected_detailRecords[0]) 198 | self.assertDictEqual(detailRecords[1], expected_detailRecords[1]) 199 | self.assertDictEqual(invoice, 200 | { 201 | "room_id": '房间一', 202 | "total_fee": 0.00833 * 60 * 2, 203 | "date_in": date_in, 204 | "date_out": date_out 205 | }) 206 | 207 | 208 | 209 | class ScheduleTests(TestCase): 210 | def test_schedule_priority(self): 211 | admin = ACAdministrator() 212 | admin.startup() 213 | admin.poweron() 214 | # admin.setpara() 215 | 216 | central = CentralAirConditioner.objects.all()[0] 217 | self.assertEqual(central.temp_mode, 1) 218 | 219 | for i in range(4): 220 | current_time = timezone.now() 221 | room = Room( 222 | room_id=str(i), room_state=0, temp_mode=1, blow_mode=1 if i != 3 else 2, 223 | current_temp=32, target_temp=27, fee=0, duration=0 224 | ) 225 | room.save() 226 | temp_sensor = TemperatureSensor( 227 | room_id=str(i), init_temp=32.0, current_temp=32.0, 228 | last_update=current_time 229 | ) 230 | temp_sensor.save() 231 | guest = Tenant(name=str(i), room_id=str(i), date_in=datetime.date.today(), date_out=datetime.date.today()) 232 | guest.save() 233 | 234 | guest.requestOn() 235 | 236 | self.assertEqual(requestQueue.objects.all().filter(room_state=2).count(), 4) 237 | central.schedule() 238 | rooms = Room.objects.all() 239 | self.assertEqual(requestQueue.objects.all().filter(room_state=1).count(), 3) 240 | self.assertEqual(rooms.filter(room_id=3)[0].blow_mode, 2) 241 | self.assertEqual(rooms.filter(blow_mode=1).count(), 3) 242 | self.assertEqual(rooms.filter(room_id=3)[0].room_state, 1) 243 | 244 | 245 | 246 | class TemperatureSensorTests(TestCase): 247 | def test_update_current_temp(self): 248 | admin = ACAdministrator() 249 | admin.startup() 250 | admin.poweron() 251 | # admin.setpara() 252 | 253 | central = CentralAirConditioner.objects.all()[0] 254 | self.assertEqual(central.temp_mode, 1) 255 | 256 | current_time = timezone.now() 257 | room = Room( 258 | room_id='1', room_state=0, temp_mode=1, blow_mode=1, 259 | current_temp=32, target_temp=27, fee=0, duration=0 260 | ) 261 | room.save() 262 | temp_sensor = TemperatureSensor( 263 | room_id='1', init_temp = 32.0, current_temp = 32.0, 264 | last_update = current_time 265 | ) 266 | temp_sensor.save() 267 | guest = Tenant(room_id='1', date_in=datetime.date.today(), date_out=datetime.date.today()) 268 | guest.save() 269 | 270 | guest.requestOn() 271 | room = Room.objects.get(pk='1') 272 | self.assertIs(room.room_state == 2, True) 273 | room.room_state = 1 274 | room.save() 275 | 276 | for i in range(61): 277 | central.schedule() 278 | room = Room.objects.get(pk='1') 279 | temp_sensor.update_current_temp(current_time + datetime.timedelta(seconds=i)) 280 | 281 | room = Room.objects.get(pk='1') 282 | self.assertAlmostEqual(temp_sensor.current_temp, 32 - 0.5) 283 | 284 | guest.requestOff() 285 | room = Room.objects.get(pk='1') 286 | self.assertIs(room.room_state == 0, True) 287 | 288 | 289 | guest.changeFanSpeed(2) 290 | two_minute_later = current_time + datetime.timedelta(minutes=2) 291 | temp_sensor.update_current_temp(current_time=two_minute_later) 292 | room = Room.objects.get(pk='1') 293 | self.assertIs(-1e-6 <= temp_sensor.current_temp - (32 - 0.5 - 0.6) <= 1e-6, True) 294 | 295 | guest.requestOff() 296 | room = Room.objects.get(pk='1') 297 | self.assertIs(room.room_state == 0, True) 298 | three_minute_later = current_time + datetime.timedelta(minutes=3) 299 | temp_sensor.update_current_temp(current_time=three_minute_later) 300 | room = Room.objects.get(pk='1') 301 | self.assertIs(-1e-6 <= temp_sensor.current_temp - (32 - 0.6) <= 1e-6, True) 302 | ''' 303 | -------------------------------------------------------------------------------- /backend/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import Sum, Count 3 | from django.utils import timezone 4 | import time 5 | import datetime 6 | from django.db.models import F 7 | 8 | # Create your models here. 9 | # 制冷制热模式, 默认制冷 10 | temp_mode_choice = ( 11 | (1, '制冷模式'), 12 | (-1, '制热模式'), 13 | ) 14 | # 送风模式,风速 15 | blow_mode_choice = ( 16 | (0, '低风'), 17 | (1, '中风'), 18 | (2, '高风'), 19 | ) 20 | # 房间状态 21 | room_state_choice = ( 22 | (0, '关闭'), 23 | (1, '运行'), 24 | (2, '挂起'), 25 | ) 26 | 27 | # 浮点误差 28 | eps = 1e-6 29 | # 费率:1元/度 30 | fee_rate = 1 31 | # 空调关机或待机,向初始温度变化,每分钟变化0.5℃,每秒变化除以60 32 | default_temp_changing = 0.5 / 60 33 | # 温度每秒变化速率 34 | temp_change_speed_array = ( 35 | (0.4 / 60, '低风温度变化'), 36 | (0.5 / 60, '中风温度变化'), 37 | (0.6 / 60, '高风温度变化'), 38 | ) 39 | # 缺省目标温度 40 | default_temp = 25 41 | # 各档风速耗电量 42 | feerate_choice = ( 43 | (0.01666, '高风单位耗电量(度/秒)'), 44 | (0.00833, '中风单位耗电量(度/秒)'), # 缺省风速 45 | (0.00556, '低风单位耗电量(度/秒)'), 46 | ) 47 | # room_list 48 | room_list = [1, 2, 3, 4, 5] 49 | 50 | 51 | class Personel(models.Model): 52 | name = models.CharField(max_length=128, unique=True) 53 | password = models.CharField(max_length=256, default='') 54 | c_time = models.DateTimeField(auto_now_add=True) # 角色创建时间,用于排序管理 55 | 56 | class Meta: 57 | ordering = ["-c_time"] # 按创建时间降序排序, 优先显示新创建的 58 | abstract = True 59 | 60 | 61 | class requestQueue(models.Model): 62 | room_id = models.CharField('房间号', max_length=64, primary_key=True) 63 | room_state = models.SmallIntegerField(choices=room_state_choice, default=2, verbose_name="房间送风状态") 64 | temp_mode = models.SmallIntegerField('制冷制热模式', choices=temp_mode_choice, default=1) 65 | blow_mode = models.SmallIntegerField('送风模式', choices=blow_mode_choice, default=1) 66 | # target_temp = models.IntegerField('目标温度') 67 | request_timestamp = models.DateTimeField('请求送风时间戳', auto_now_add=True) 68 | air_timestamp = models.DateTimeField('开始送风时间戳', null=True) 69 | service_duration = models.IntegerField('当前服务时长(秒)', default=0) 70 | 71 | 72 | class ACAdministrator(Personel): 73 | def __str__(self): 74 | return self.name 75 | 76 | def poweron(self): 77 | central_air_conditioner = CentralAirConditioner.objects.all()[0] 78 | central_air_conditioner.ac_state = 'set_mode' 79 | central_air_conditioner.save() 80 | 81 | def setpara(self, temp_mode=None, cool_temp_highlimit=None, cool_temp_lowlimit=None, heat_temp_highlimit=None, 82 | heat_temp_lowlimit=None, \ 83 | default_targettemp=None, feerate_h=None, feerate_m=None, feerate_l=None, max_load=None, 84 | waiting_duration=None): 85 | central_air_conditioner = CentralAirConditioner.objects.all()[0] 86 | if central_air_conditioner.ac_state == 'set_mode': 87 | if temp_mode is not None: 88 | central_air_conditioner.temp_mode = temp_mode 89 | if cool_temp_highlimit is not None: 90 | central_air_conditioner.cool_temp_highlimit = cool_temp_highlimit 91 | if cool_temp_lowlimit is not None: 92 | central_air_conditioner.cool_temp_lowlimit = cool_temp_lowlimit 93 | if heat_temp_highlimit is not None: 94 | central_air_conditioner.heat_temp_highlimit = heat_temp_highlimit 95 | if heat_temp_lowlimit is not None: 96 | central_air_conditioner.heat_temp_lowlimit = heat_temp_lowlimit 97 | if default_temp is not None: 98 | central_air_conditioner.default_temp = default_targettemp 99 | if feerate_h is not None: 100 | central_air_conditioner.feerate_H = feerate_h 101 | if feerate_m is not None: 102 | central_air_conditioner.feerate_M = feerate_m 103 | if feerate_l is not None: 104 | central_air_conditioner.feerate_L = feerate_l 105 | if max_load is not None: 106 | central_air_conditioner.max_load = max_load 107 | if waiting_duration is not None: 108 | central_air_conditioner.waiting_duration = waiting_duration 109 | central_air_conditioner.save() 110 | 111 | def startup(self): 112 | central_air_conditioner = CentralAirConditioner() 113 | central_air_conditioner.ac_state = 'ready' 114 | central_air_conditioner.save() 115 | 116 | def checkroomstate(self): 117 | central_air_conditioner = CentralAirConditioner.objects.all()[0] 118 | while central_air_conditioner.ac_state == 'ready': 119 | info = Room.objects.all() 120 | time.sleep(60) 121 | return info 122 | # 前端每分钟刷新显示房间的[models.Room.room_state, models.Room.current_temp, models.Room.target_temp, 123 | # (续)models.Room.blow_mode, models.Room.fee_rate, models.Room.fee, models.Room.duration] 124 | 125 | class Meta: 126 | verbose_name = "空调管理员" # 可读性佳的名字 127 | verbose_name_plural = "空调管理员" # 复数形式 128 | 129 | 130 | class Waiter(Personel): 131 | def __str__(self): 132 | return self.name 133 | 134 | class Meta: 135 | verbose_name = "前台服务员" # 可读性佳的名字 136 | verbose_name_plural = "前台服务员" # 复数形式 137 | 138 | def printRDR(self, room_id, date_in, date_out): 139 | records = ServiceRecord.objects.filter(RR__room_id=room_id, start_time__range=(date_in, date_out)).values() 140 | return records 141 | 142 | def printInvoice(self, room_id, date_in, date_out): 143 | total_fee = ServiceRecord.objects.filter(RR__room_id=room_id, start_time__range=(date_in, date_out)).aggregate( 144 | Sum("fee")).get('fee__sum') 145 | invoice = { 146 | "room_id": room_id, 147 | "total_fee": total_fee, 148 | "date_in": date_in, 149 | "date_out": date_out 150 | } 151 | return invoice 152 | 153 | 154 | class Manager(Personel): 155 | def __str__(self): 156 | return self.name 157 | 158 | class Meta: 159 | verbose_name = "酒店经理" # 可读性佳的名字 160 | verbose_name_plural = "酒店经理" # 复数形式 161 | 162 | def dailyReport(self, date_start, date_end): 163 | basic_room_info = RoomDailyReport.objects.filter(date__range=(date_start.date(), date_end.date())).\ 164 | values('room_id', 'date', 'switch_count', 'schedule_count', 'change_temp_count', 'change_speed_count') 165 | 166 | for i in range((date_end - date_start).days+1): 167 | date = date_start+datetime.timedelta(days=i) 168 | day_service_items = ServiceRecord.objects.filter(start_time__date=date).values('RR__room_id').\ 169 | annotate(Sum('service_time')).values('RR__room_id', 'service_time__sum') 170 | 171 | for k in day_service_items: 172 | for j in basic_room_info: 173 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 174 | j['service_time__sum'] = k['service_time__sum'] 175 | 176 | day_room_fee = ServiceRecord.objects.filter(start_time__date=date).values('RR__room_id').\ 177 | annotate(Sum('fee')).values('RR__room_id', 'fee__sum') 178 | 179 | for k in day_room_fee: 180 | for j in basic_room_info: 181 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 182 | j['fee__sum'] = k['fee__sum'] 183 | 184 | day_detail_record = ServiceRecord.objects.filter(start_time__date=date).values('RR__room_id'). \ 185 | annotate(detail_record_count=Count('RR__room_id')).values('RR__room_id', 'detail_record_count') 186 | 187 | for k in day_detail_record: 188 | for j in basic_room_info: 189 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 190 | j['detail_record_count'] = k['detail_record_count'] 191 | 192 | return basic_room_info 193 | 194 | def weeklyReport(self, date_start, date_end): 195 | basic_room_info = RoomDailyReport.objects.filter(date__week__range=(date_start.week(), date_end.week())).\ 196 | values('room_id', 'date', 'switch_count', 'schedule_count', 'change_temp_count', 'change_speed_count') 197 | 198 | for i in range((date_end - date_start).weeks + 1): 199 | date = date_start + datetime.timedelta(weeks=i) 200 | day_service_items = ServiceRecord.objects.filter(start_time__week=date.week()).values('RR__room_id'). \ 201 | annotate(Sum('service_time')).values('RR__room_id', 'service_time__sum') 202 | 203 | for k in day_service_items: 204 | for j in basic_room_info: 205 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 206 | j['service_time__sum'] = k['service_time__sum'] 207 | 208 | day_room_fee = ServiceRecord.objects.filter(start_time__week=date.week()).values('RR__room_id'). \ 209 | annotate(Sum('fee')).values('RR__room_id', 'fee__sum') 210 | 211 | for k in day_room_fee: 212 | for j in basic_room_info: 213 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 214 | j['fee__sum'] = k['fee__sum'] 215 | 216 | day_detail_record = ServiceRecord.objects.filter(start_time__week=date.week()).values('RR__room_id'). \ 217 | annotate(detail_record_count=Count('RR__room_id')).values('RR__room_id', 'detail_record_count') 218 | 219 | for k in day_detail_record: 220 | for j in basic_room_info: 221 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 222 | j['detail_record_count'] = k['detail_record_count'] 223 | 224 | return basic_room_info 225 | 226 | def monthlyReport(self, date_start, date_end): 227 | basic_room_info = RoomDailyReport.objects.filter(date__month__range=(date_start.month(), date_end.month())). \ 228 | values('room_id', 'date', 'switch_count', 'schedule_count', 'change_temp_count', 'change_speed_count') 229 | 230 | for i in range((date_end - date_start).weeks + 1): 231 | date = date_start + datetime.timedelta(months=i) 232 | day_service_items = ServiceRecord.objects.filter(start_time__month=date.month()).values('RR__room_id'). \ 233 | annotate(Sum('service_time')).values('RR__room_id', 'service_time__sum') 234 | 235 | for k in day_service_items: 236 | for j in basic_room_info: 237 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 238 | j['service_time__sum'] = k['service_time__sum'] 239 | 240 | day_room_fee = ServiceRecord.objects.filter(start_time__month=date.month()).values('RR__room_id'). \ 241 | annotate(Sum('fee')).values('RR__room_id', 'fee__sum') 242 | 243 | for k in day_room_fee: 244 | for j in basic_room_info: 245 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 246 | j['fee__sum'] = k['fee__sum'] 247 | 248 | day_detail_record = ServiceRecord.objects.filter(start_time__month=date.month()).values('RR__room_id'). \ 249 | annotate(detail_record_count=Count('RR__room_id')).values('RR__room_id', 'detail_record_count') 250 | 251 | for k in day_detail_record: 252 | for j in basic_room_info: 253 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 254 | j['detail_record_count'] = k['detail_record_count'] 255 | 256 | return basic_room_info 257 | 258 | def yearlyReport(self): 259 | basic_room_info = RoomDailyReport.objects.filter(date__year__range=(date_start.year(), date_end.year())). \ 260 | values('room_id', 'date', 'switch_count', 'schedule_count', 'change_temp_count', 'change_speed_count') 261 | 262 | for i in range((date_end - date_start).weeks + 1): 263 | date = date_start + datetime.timedelta(years=i) 264 | day_service_items = ServiceRecord.objects.filter(start_time__year=date.year()).values('RR__room_id'). \ 265 | annotate(Sum('service_time')).values('RR__room_id', 'service_time__sum') 266 | 267 | for k in day_service_items: 268 | for j in basic_room_info: 269 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 270 | j['service_time__sum'] = k['service_time__sum'] 271 | 272 | day_room_fee = ServiceRecord.objects.filter(start_time__year=date.year()).values('RR__room_id'). \ 273 | annotate(Sum('fee')).values('RR__room_id', 'fee__sum') 274 | 275 | for k in day_room_fee: 276 | for j in basic_room_info: 277 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 278 | j['fee__sum'] = k['fee__sum'] 279 | 280 | day_detail_record = ServiceRecord.objects.filter(start_time__year=date.year()).values('RR__room_id'). \ 281 | annotate(detail_record_count=Count('RR__room_id')).values('RR__room_id', 'detail_record_count') 282 | 283 | for k in day_detail_record: 284 | for j in basic_room_info: 285 | if j['date'] == date.date() and j['room_id'] == k['RR__room_id']: 286 | j['detail_record_count'] = k['detail_record_count'] 287 | 288 | return basic_room_info 289 | 290 | 291 | class Tenant(models.Model): 292 | name = models.CharField(max_length=128, unique=True) 293 | password = models.CharField(max_length=256, default='') 294 | c_time = models.DateTimeField(auto_now_add=True) # 角色创建时间,用于排序管理 295 | room_id = models.CharField('房间号', max_length=64, primary_key=True) 296 | date_in = models.DateField('入住日期', null=True) 297 | date_out = models.DateField('登出日期', null=True) 298 | 299 | class Meta: 300 | ordering = ["-c_time"] # 按创建时间降序排序, 优先显示新创建的 301 | verbose_name = "房客" # 可读性佳的名字 302 | verbose_name_plural = "房客" # 复数形式 303 | 304 | def initiateTenant(self): 305 | pass 306 | 307 | def requestOn(self): 308 | room = Room.objects.get(pk=self.room_id) 309 | room.room_state = 2 # 开机后,房间空调处于待机状态,若中央空调能送风才处于工作状态 310 | room.switch_count += 1 311 | room.save() 312 | room_daily_report = RoomDailyReport.objects.filter(room_id=self.room_id, date=datetime.date.today()) 313 | if room_daily_report.count() == 0: 314 | room_daily_report = RoomDailyReport(room_id=self.room_id) 315 | else: 316 | room_daily_report = room_daily_report[0] 317 | room_daily_report.switch_count += 1 318 | room_daily_report.save() 319 | room.requestAir() 320 | 321 | def changeTargetTemp(self, target_temp): 322 | room = Room.objects.get(pk=self.room_id) 323 | room.target_temp = target_temp 324 | # room.change_temp_count += 1 325 | room.save() 326 | 327 | room_daily_report = RoomDailyReport.objects.filter(room_id=self.room_id, date=datetime.date.today()) 328 | if room_daily_report.count() == 0: 329 | room_daily_report = RoomDailyReport(room_id=self.room_id) 330 | else: 331 | room_daily_report = room_daily_report[0] 332 | room_daily_report.change_temp_count += 1 333 | room_daily_report.save() 334 | 335 | room.cancelAir() 336 | room.requestAir() 337 | 338 | def changeFanSpeed(self, fan_speed): 339 | room = Room.objects.get(pk=self.room_id) 340 | room.blow_mode = fan_speed 341 | # room.change_speed_count += 1 342 | room.save() 343 | room_daily_report = RoomDailyReport.objects.filter(room_id=self.room_id, date=datetime.date.today()) 344 | if room_daily_report.count() == 0: 345 | room_daily_report = RoomDailyReport(room_id=self.room_id) 346 | else: 347 | room_daily_report = room_daily_report[0] 348 | room_daily_report.change_speed_count += 1 349 | room_daily_report.save() 350 | 351 | room.cancelAir() 352 | room.requestAir() 353 | 354 | def requestOff(self): 355 | room = Room.objects.get(pk=self.room_id) 356 | room.room_state = 0 357 | room.save() 358 | room.cancelAir() 359 | 360 | 361 | class TemperatureSensor(models.Model): 362 | room_id = models.CharField( 363 | '房间号', max_length=64, unique=True, primary_key=True) 364 | init_temp = models.FloatField('初始温度') 365 | current_temp = models.FloatField('当前温度') 366 | last_update = models.DateTimeField('上次更新时间', default=timezone.now()) 367 | 368 | def update_current_temp(self, current_time=None): 369 | if current_time is None: 370 | current_time = timezone.now() 371 | room = Room.objects.get(pk=self.room_id) 372 | # current_time = 373 | delta_update_time = current_time - self.last_update 374 | temp_change_direction = room.temp_mode 375 | if room.get_room_state_display() != '运行': 376 | delta_temp_change = default_temp_changing * delta_update_time.total_seconds() 377 | self.current_temp += temp_change_direction * delta_temp_change 378 | # 房间回温算法变化到室温为止 379 | if (temp_change_direction == 1 and self.current_temp > self.init_temp) or \ 380 | (temp_change_direction == -1 and self.current_temp < self.init_temp): 381 | self.current_temp = self.init_temp 382 | else: 383 | delta_temp_change = temp_change_speed_array[room.blow_mode][0] * \ 384 | delta_update_time.total_seconds() 385 | self.current_temp -= temp_change_direction * delta_temp_change 386 | self.last_update = current_time 387 | 388 | 389 | class RoomDailyReport(models.Model): 390 | room_id = models.CharField('房间号', max_length=64) 391 | date = models.DateField('日期') 392 | switch_count = models.IntegerField('开关次数', default=0) 393 | schedule_count = models.IntegerField('被调度次数', default=0) 394 | change_temp_count = models.IntegerField('调温次数', default=0) 395 | change_speed_count = models.IntegerField('调风次数', default=0) 396 | 397 | 398 | class Room(models.Model): 399 | room_id = models.CharField( 400 | '房间号', max_length=64, unique=True, primary_key=True) 401 | room_state = models.SmallIntegerField( 402 | choices=room_state_choice, default=0, verbose_name="房间从控机状态") 403 | temp_mode = models.SmallIntegerField( 404 | '制冷制热模式', choices=temp_mode_choice, default=1) 405 | blow_mode = models.SmallIntegerField( 406 | '送风模式', choices=blow_mode_choice, default=1) 407 | current_temp = models.IntegerField('当前温度') 408 | target_temp = models.IntegerField('目标温度', default=default_temp) 409 | # fee_rate = models.FloatField('费率') 410 | fee = models.FloatField('总费用') 411 | duration = models.IntegerField('服务时长(秒)') 412 | 413 | 414 | def requestAir(self): 415 | new_air_request = requestQueue(self.room_id, self.room_state, self.temp_mode, self.blow_mode) 416 | new_air_request.save() 417 | new_request_record = RequestRecord( # 创建请求记录 418 | room_id=self.room_id, room_state=self.room_state, blow_mode=self.blow_mode, 419 | start_temp=self.current_temp, target_temp=self.target_temp, request_time=timezone.now() 420 | ) 421 | new_request_record.save() 422 | 423 | def cancelAir(self): 424 | old_air_request = requestQueue.objects.get(pk=self.room_id) 425 | old_air_request.delete() 426 | 427 | old_request_record = RequestRecord.objects.get(room_id=self.room_id, finished=0) 428 | old_request_record.finished=1 # 结束请求记录 429 | old_request_record.save() 430 | ServiceRecord.objects.filter(RR=old_request_record.RR, room_id=self.room_id)\ 431 | .update(end_time=timezone.now, power_comsumption=old_request_record.service_duration * old_request_record.blow_mode / 180.0, 432 | fee=old_request_record.service_duration * old_request_record.blow_mode / 180.0) 433 | 434 | 435 | def auto_ajust_temp(self): 436 | if self.room_state == 1: 437 | # 达到目标温度,取消送风 438 | if (self.temp_mode == 1 and self.current_temp <= self.target_temp + eps) or \ 439 | (self.temp_mode == -1 and self.current_temp >= self.target_temp - eps): 440 | self.cancelAir() 441 | elif self.room_state == 2: 442 | # 偏离目标温度一度,请求送风 443 | if (self.temp_mode == 1 and self.current_temp >= self.target_temp + 1 - eps) or \ 444 | (self.temp_mode == -1 and self.current_temp <= self.target_temp - 1 + eps): 445 | self.requestAir() 446 | 447 | 448 | class CentralAirConditioner(models.Model): 449 | ac_state_choice = ( 450 | ('close', '关闭'), 451 | ('set_Model', '设置模式'), 452 | ('ready', '就绪'), 453 | ('none', '备用'), 454 | ) 455 | ac_state = models.CharField( 456 | choices=ac_state_choice, max_length=64, default='close', verbose_name="中央空调状态") 457 | temp_mode = models.SmallIntegerField( 458 | '制冷制热模式', choices=temp_mode_choice, default=1) 459 | cool_temp_highlimit = models.IntegerField('制冷温控范围最高温', default=25) 460 | cool_temp_lowlimit = models.IntegerField('制冷温控范围最低温', default=18) 461 | heat_temp_highlimit = models.IntegerField('制热温控范围最高温', default=30) 462 | heat_temp_lowlimit = models.IntegerField('制热温控范围最低温', default=25) 463 | default_temp = models.IntegerField('缺省温度', default=25) 464 | feerate_H = models.FloatField('高风费率', default=1.0 / 60) 465 | feerate_M = models.FloatField('中风费率', default=0.5 / 60) 466 | feerate_L = models.FloatField('低风费率', default=1.0 / 180) 467 | max_load = models.IntegerField('最大带机量', default=3) 468 | waiting_duration = models.IntegerField(default=120) 469 | 470 | # 需要每秒运行 471 | def billing(self): 472 | pass 473 | 474 | # 需要每秒钟运行 475 | def schedule(self): 476 | 477 | def send_air(satisfy_request): 478 | satisfy_room = Room.objects.get(pk=satisfy_request.room_id) # 取得房间 479 | satisfy_room.room_state = 1 480 | satisfy_room.save() 481 | 482 | satisfy_request.room_state = 1 # 开始送风 483 | satisfy_request.air_timestamp = timezone.now() # 送风开始时间 484 | satisfy_request.save() 485 | 486 | request_record = RequestRecord.objects.get(room_id=satisfy_request.room_id, finished=0) 487 | new_service_record = ServiceRecord( 488 | RR=request_record.RR, room_id=satisfy_request.room_id, blow_mode=request_record.blow_mode, 489 | start_time=timezone.now(), ntemp=satisfy_room.current_temp 490 | ) 491 | new_service_record.save() 492 | 493 | def cancel_air(weaker_request): 494 | weaker_room = Room.objects.get(pk=weaker_request.room_id) 495 | weaker_room.room_state = 2 496 | # weaker_room.schedule_count += 1 497 | weaker_room.save() 498 | room_daily_report = RoomDailyReport.objects.filter(room_id=weaker_request.room_id, date=datetime.date.today()) 499 | if room_daily_report.count() == 0: 500 | room_daily_report = RoomDailyReport(room_id=self.room_id) 501 | else: 502 | room_daily_report = room_daily_report[0] 503 | room_daily_report.schedule_count += 1 504 | room_daily_report.save() 505 | 506 | weaker_request.room_state = 2 # 停止送风 507 | weaker_request.request_timestamp = timezone.now() # 重新计时 508 | weaker_request.save() 509 | 510 | old_request_record = RequestRecord.objects.get(room_id=weaker_request.room_id, finished=0) 511 | ServiceRecord.objects.filter(RR=old_request_record.RR, room_id=weaker_request.room_id)\ 512 | .update(end_time=timezone.now, power_comsumption=weaker_request.service_duration * weaker_request.blow_mode / 180.0, 513 | fee=weaker_request.service_duration * weaker_request.blow_mode / 180.0) 514 | 515 | all_rooms = Room.objects.all() 516 | all_requests = requestQueue.objects.all() 517 | current_request_cnt = len(all_requests) 518 | waiting_request = all_requests.filter(room_state=2) 519 | serving_request = all_requests.filter(room_state=1) 520 | current_load = len(serving_request) 521 | 522 | # 更新服务时间 523 | serving_request.update(service_duration=F('service_duration') + 1) 524 | 525 | # 全部送风请求都已满足,不需要调度 526 | if len(waiting_request) == 0: 527 | return 528 | 529 | for satisfy_request in waiting_request: 530 | # 尚未超过负载能力,满足送风请求 531 | if requestQueue.objects.all().filter(room_state=1).count() < self.max_load: 532 | # 与设定制冷制热模式一致 533 | if satisfy_request.temp_mode == self.temp_mode: 534 | send_air(satisfy_request) 535 | 536 | 537 | else: # 超过负载能力,需要进行调度 538 | current_time = timezone.now() 539 | serving_request = requestQueue.objects.all().filter(room_state=1) 540 | weaker_request = serving_request.filter(blow_mode__lt=satisfy_request.blow_mode) 541 | if len(weaker_request) > 0: # 优先级调度,先满足高风速请求 542 | weaker_request = weaker_request[0] # 挑选出一个低风速请求 543 | cancel_air(weaker_request) 544 | send_air(satisfy_request) 545 | else: 546 | equal_request = serving_request.filter(blow_mode=satisfy_request.blow_mode).order_by( 547 | '-service_duration') 548 | if len( 549 | equal_request) > 0 and satisfy_request.request_timestamp <= current_time - datetime.timedelta( 550 | seconds=self.waiting_duration): # 时间片调度 551 | equal_request = equal_request[0] # 关掉服务时间最长的 552 | cancel_air(weaker_request) 553 | send_air(satisfy_request) 554 | 555 | 556 | class RequestRecord(models.Model): 557 | room_id = models.CharField('房间号', max_length=64) 558 | room_state = models.SmallIntegerField("房间送风状态", choices=room_state_choice, default=2) 559 | temp_mode = models.SmallIntegerField('送风模式', choices=temp_mode_choice, default=2) 560 | start_temp = models.IntegerField('初始温度') 561 | target_temp = models.IntegerField('目标温度') 562 | request_time = models.DateTimeField() 563 | finished = models.IntegerField('结束', default=0) 564 | 565 | 566 | class ServiceRecord(models.Model): 567 | RR = models.ForeignKey("RequestRecord", on_delete=models.CASCADE) 568 | blow_mode = models.SmallIntegerField('送风模式', choices=blow_mode_choice, default=2) 569 | start_time = models.DateTimeField() 570 | end_time = models.DateTimeField() 571 | service_time = models.DurationField() 572 | power_comsumption = models.FloatField('用电度数') 573 | now_temp = models.FloatField('当前温度') 574 | fee_rate = models.FloatField('费率', choices=feerate_choice, default=2) 575 | fee = models.FloatField() 576 | --------------------------------------------------------------------------------