init
This commit is contained in:
commit
aa0f98e742
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
11
LICENSE
Normal file
11
LICENSE
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Copyright (c) 2023 Zakary Timson
|
||||||
|
|
||||||
|
All Rights Reserved.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
64
README.md
Normal file
64
README.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<!-- Header -->
|
||||||
|
<div id="top" align="center">
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<!-- Logo -->
|
||||||
|
<img src="https://git.zakscode.com/repo-avatars/02c3c82a2c192095fa04a6ea4d92d9614d37ab508fc948938b37a9cfd3196734" alt="Logo" width="200" height="200">
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
### GrowBot
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
Plant Manager
|
||||||
|
|
||||||
|
<!-- Repo badges -->
|
||||||
|
[![Version](https://img.shields.io/badge/dynamic/json.svg?label=Version&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/GrowBot/tags&query=$[0].name)](https://git.zakscode.com/ztimson/GrowBot/tags)
|
||||||
|
[![Pull Requests](https://img.shields.io/badge/dynamic/json.svg?label=Pull%20Requests&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/GrowBot&query=open_pr_counter)](https://git.zakscode.com/ztimson/GrowBot/pulls)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [GrowBot](#top)
|
||||||
|
- [About](#about)
|
||||||
|
- [Built With](#built-with)
|
||||||
|
- [Setup](#setup)
|
||||||
|
- [Development](#development)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
|
||||||
|
### Built With
|
||||||
|
[![Angular](https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular)](https://angular.io/)
|
||||||
|
[![Bootstrap](https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white)](https://getbootstrap.com)
|
||||||
|
[![Node](https://img.shields.io/badge/Node.js-000000?style=for-the-badge&logo=nodedotjs)](https://nodejs.org/)
|
||||||
|
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<h3 id="development" style="display: inline">
|
||||||
|
Development
|
||||||
|
</h3>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
- [CMake](https://cmake.org/download/)
|
||||||
|
- [Node.js](https://nodejs.org/en/download)
|
||||||
|
|
||||||
|
#### Instructions
|
||||||
|
1. Install the build tools: `npm install -g node-gyp windows-build-tools`
|
||||||
|
2. Install dependencies:
|
||||||
|
1. Client: `cd client && npm install`
|
||||||
|
2. Server: `cd ../server && npm install`
|
||||||
|
3. Start the Node server: `npm run start`
|
||||||
|
4. Start the Angular client: `cd client && npm run start`
|
||||||
|
5. Open http://localhost:4200
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## License
|
||||||
|
Copyright © 2023 Zakary Timson | All Rights Reserved | Available under MIT Licensing
|
||||||
|
|
||||||
|
See the [license](./LICENSE) for more information.
|
18
client/.browserslistrc
Normal file
18
client/.browserslistrc
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# For the full list of supported browsers by the Angular framework, please see:
|
||||||
|
# https://angular.io/guide/browser-support
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
last 1 Chrome version
|
||||||
|
last 1 Firefox version
|
||||||
|
last 2 Edge major versions
|
||||||
|
last 2 Safari major versions
|
||||||
|
last 2 iOS major versions
|
||||||
|
Firefox ESR
|
||||||
|
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
|
||||||
|
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
16
client/.editorconfig
Normal file
16
client/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
46
client/.gitignore
vendored
Normal file
46
client/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
# Only exists if Bazel was run
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# profiling files
|
||||||
|
chrome-profiler-events*.json
|
||||||
|
speed-measure-plugin*.json
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
112
client/angular.json
Normal file
112
client/angular.json
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"GrowBot": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/GrowBot",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.json",
|
||||||
|
"aot": true,
|
||||||
|
"assets": [
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"extractCss": true,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "GrowBot:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "GrowBot:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "GrowBot:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": [
|
||||||
|
"tsconfig.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
|
"options": {
|
||||||
|
"protractorConfig": "e2e/protractor.conf.js",
|
||||||
|
"devServerTarget": "GrowBot:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "GrowBot:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "GrowBot",
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
|
}
|
||||||
|
}
|
13574
client/package-lock.json
generated
Normal file
13574
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
client/package.json
Normal file
53
client/package.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "grow-bot",
|
||||||
|
"version": "0.3.10",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"test": "ng test",
|
||||||
|
"lint": "ng lint",
|
||||||
|
"e2e": "ng e2e"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "~10.0.5",
|
||||||
|
"@angular/cdk": "^10.1.0",
|
||||||
|
"@angular/common": "~10.0.5",
|
||||||
|
"@angular/compiler": "~10.0.5",
|
||||||
|
"@angular/core": "~10.0.5",
|
||||||
|
"@angular/forms": "~10.0.5",
|
||||||
|
"@angular/material": "^10.1.0",
|
||||||
|
"@angular/platform-browser": "~10.0.5",
|
||||||
|
"@angular/platform-browser-dynamic": "~10.0.5",
|
||||||
|
"@angular/router": "~10.0.5",
|
||||||
|
"bootstrap-scss": "^4.5.0",
|
||||||
|
"chart.js": "^2.9.3",
|
||||||
|
"chartjs-plugin-annotation": "^0.5.7",
|
||||||
|
"ng2-charts": "^2.3.3",
|
||||||
|
"rxjs": "~6.5.5",
|
||||||
|
"socket.io-client": "^2.3.0",
|
||||||
|
"tslib": "^2.0.0",
|
||||||
|
"zone.js": "~0.10.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "~0.1000.4",
|
||||||
|
"@angular/cli": "~10.0.4",
|
||||||
|
"@angular/compiler-cli": "~10.0.5",
|
||||||
|
"@types/node": "^12.11.1",
|
||||||
|
"@types/jasmine": "~3.5.0",
|
||||||
|
"@types/jasminewd2": "~2.0.3",
|
||||||
|
"codelyzer": "^6.0.0",
|
||||||
|
"jasmine-core": "~3.5.0",
|
||||||
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
|
"karma": "~5.0.0",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
|
"karma-jasmine": "~3.3.0",
|
||||||
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
|
"protractor": "~7.0.0",
|
||||||
|
"ts-node": "~8.3.0",
|
||||||
|
"tslint": "~6.1.0",
|
||||||
|
"typescript": "~3.9.5"
|
||||||
|
}
|
||||||
|
}
|
50
client/src/app/animations.ts
Normal file
50
client/src/app/animations.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
trigger,
|
||||||
|
animate,
|
||||||
|
transition,
|
||||||
|
style,
|
||||||
|
query, group
|
||||||
|
} from '@angular/animations';
|
||||||
|
|
||||||
|
export const collapseUp = trigger('collapseUp', [
|
||||||
|
transition('* => void', [
|
||||||
|
style({opacity: 1, transform: 'translateY(0%)'}),
|
||||||
|
animate('0.5s', style({opacity: 0, transform: 'translateY(-100%)'}))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const expandDown = trigger('expandDown', [
|
||||||
|
transition('void => *', [
|
||||||
|
style({opacity: 0, transform: 'translateY(-100%)'}),
|
||||||
|
animate('0.5s', style({opacity: 1, transform: 'translateY(0%)'}))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fadeIn = trigger('fadeIn', [
|
||||||
|
transition('void => *', [
|
||||||
|
style({opacity: 0}),
|
||||||
|
animate('0.5s ease-in-out', style({opacity: 1}))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const fadeOut = trigger('fadeOut', [
|
||||||
|
transition('* => void', [
|
||||||
|
style({opacity: 1}),
|
||||||
|
animate('0.5s ease-in-out', style({opacity: 0}))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const routerTransition = trigger('routerTransition', [
|
||||||
|
transition('* <=> *', [
|
||||||
|
query(':enter, :leave', style({position: 'fixed', width: '100%'}), {optional: true}),
|
||||||
|
group([
|
||||||
|
query(':enter', [
|
||||||
|
style({transform: 'translateX(100%)'}),
|
||||||
|
animate('0.5s ease-in-out', style({transform: 'translateX(0%)'}))
|
||||||
|
], {optional: true}), query(':leave', [
|
||||||
|
style({transform: 'translateX(0%)'}),
|
||||||
|
animate('0.5s ease-in-out', style({transform: 'translateX(-100%)'}))
|
||||||
|
], {optional: true}),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
27
client/src/app/app-routing.module.ts
Normal file
27
client/src/app/app-routing.module.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {Routes, RouterModule} from '@angular/router';
|
||||||
|
import {CameraComponent} from "./views/camera/camera.component";
|
||||||
|
import {ClimateComponent} from "./views/climate/climate.component";
|
||||||
|
import {DashboardComponent} from "./views/dashboard/dashboard.component";
|
||||||
|
import {WaterComponent} from "./views/water/water.component";
|
||||||
|
import {GrowOpsComponent} from "./views/growOps/growOps.component";
|
||||||
|
import {ScheduleComponent} from "./views/schedule/schedule.component";
|
||||||
|
import {NotesComponent} from "./views/notes/notes.component";
|
||||||
|
import {SettingsComponent} from "./views/settings/settings.component";
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{path: 'camera', component: CameraComponent},
|
||||||
|
{path: 'climate', component: ClimateComponent},
|
||||||
|
{path: 'growops', component: GrowOpsComponent},
|
||||||
|
{path: 'notes', component: NotesComponent},
|
||||||
|
{path: 'schedule', component: ScheduleComponent},
|
||||||
|
{path: 'settings', component: SettingsComponent},
|
||||||
|
{path: 'water', component: WaterComponent},
|
||||||
|
{path: '**', component: DashboardComponent, data: {noAnimation: true}},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
67
client/src/app/app.module.ts
Normal file
67
client/src/app/app.module.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
import {AppRoutingModule} from './app-routing.module';
|
||||||
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
import {MaterialModule} from "./material.module";
|
||||||
|
import {LogoComponent} from "./components/logo/logo.component";
|
||||||
|
import {MenuComponent} from "./components/menu/menu.component";
|
||||||
|
import {StreamComponent} from "./components/stream/stream.component";
|
||||||
|
import {CameraComponent} from "./views/camera/camera.component";
|
||||||
|
import {FullscreenCameraComponent} from "./views/fullscreenCamera/fullscreenCamera.component";
|
||||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||||
|
import {MatNativeDateModule} from "@angular/material/core";
|
||||||
|
import {HttpClientModule} from "@angular/common/http";
|
||||||
|
import {ClimateComponent} from "./views/climate/climate.component";
|
||||||
|
import {ChartsModule} from "ng2-charts";
|
||||||
|
import {ClimateGraphComponent} from "./components/climateGraph/climateGraph.component";
|
||||||
|
import {DashboardComponent} from "./views/dashboard/dashboard.component";
|
||||||
|
import {AppComponent} from "./views/app.component";
|
||||||
|
import {TemperatureComponent} from "./components/temperature/temperature.component";
|
||||||
|
import {FanAutoComponent} from "./components/fanAuto/fanAuto.component";
|
||||||
|
import {LightAutoComponent} from "./components/lightAuto/lightAuto.component";
|
||||||
|
import {FanToggleComponent} from "./components/fanToggle/fanToggle.component";
|
||||||
|
import {LightToggleComponent} from "./components/lightToggle/lightToggle.component";
|
||||||
|
import {WaterComponent} from "./views/water/water.component";
|
||||||
|
import {GrowOpsComponent} from "./views/growOps/growOps.component";
|
||||||
|
import {ScheduleComponent} from "./views/schedule/schedule.component";
|
||||||
|
import {NotesComponent} from "./views/notes/notes.component";
|
||||||
|
import {SettingsComponent} from "./views/settings/settings.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
CameraComponent,
|
||||||
|
ClimateComponent,
|
||||||
|
ClimateGraphComponent,
|
||||||
|
DashboardComponent,
|
||||||
|
FanAutoComponent,
|
||||||
|
FanToggleComponent,
|
||||||
|
FullscreenCameraComponent,
|
||||||
|
GrowOpsComponent,
|
||||||
|
LightAutoComponent,
|
||||||
|
LightToggleComponent,
|
||||||
|
LogoComponent,
|
||||||
|
MenuComponent,
|
||||||
|
NotesComponent,
|
||||||
|
ScheduleComponent,
|
||||||
|
SettingsComponent,
|
||||||
|
StreamComponent,
|
||||||
|
TemperatureComponent,
|
||||||
|
WaterComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AppRoutingModule,
|
||||||
|
BrowserModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
ChartsModule,
|
||||||
|
FormsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
MaterialModule,
|
||||||
|
MatNativeDateModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
5
client/src/app/bootstrap.scss
vendored
Normal file
5
client/src/app/bootstrap.scss
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@import 'node_modules/bootstrap-scss/bootstrap';
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: #689f38 !important
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<div class="w-100 h-100">
|
||||||
|
<div>
|
||||||
|
<div style="width: 250px">
|
||||||
|
<mat-form-field appearance="fill" color="accent">
|
||||||
|
<mat-label>Time Frame</mat-label>
|
||||||
|
<mat-select [(ngModel)]="timeFrame">
|
||||||
|
<mat-option value="12">12 hours</mat-option>
|
||||||
|
<mat-option value="24">24 hours</mat-option>
|
||||||
|
<mat-option value="72">3 Days</mat-option>
|
||||||
|
<mat-option value="168">7 Days</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-checkbox class="mr-5 red" [(ngModel)]="toggleTemp" (change)="refresh()">Temperature</mat-checkbox>
|
||||||
|
<mat-checkbox class="mr-5 green" [(ngModel)]="toggleHumidity" (change)="refresh()">Humidity</mat-checkbox>
|
||||||
|
<mat-checkbox class="mr-5 yellow" [(ngModel)]="toggleLight" (change)="refresh()">Lights</mat-checkbox>
|
||||||
|
<mat-checkbox class="mr-5 blue" [(ngModel)]="toggleFans" (change)="refresh()">Fans</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<canvas baseChart class="mt-4"
|
||||||
|
[datasets]="data | async"
|
||||||
|
[labels]="labels | async"
|
||||||
|
[colors]="colors"
|
||||||
|
[legend]="false"
|
||||||
|
chartType="line"
|
||||||
|
[plugins]="plugins"
|
||||||
|
[options]="options | async">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
@ -0,0 +1,79 @@
|
|||||||
|
.red {
|
||||||
|
::ng-deep.mat-checkbox-label {
|
||||||
|
color: rgb(150, 80, 80) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.mat-checkbox-frame {
|
||||||
|
border-color: rgb(150,80,80);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mat-checkbox-checked {
|
||||||
|
::ng-deep.mat-checkbox-background {
|
||||||
|
background-color: rgb(150,80,80) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.mat-ripple-element {
|
||||||
|
background: rgb(150,80,80) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.green {
|
||||||
|
::ng-deep.mat-checkbox-label {
|
||||||
|
color: rgb(80,150,80) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.mat-checkbox-frame {
|
||||||
|
border-color: rgb(80,150,80);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mat-checkbox-checked {
|
||||||
|
::ng-deep.mat-checkbox-background {
|
||||||
|
background-color: rgb(80,150,80) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.mat-ripple-element {
|
||||||
|
background: rgb(80,150,80) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue {
|
||||||
|
::ng-deep.mat-checkbox-label {
|
||||||
|
color: rgb(80,80,230) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.mat-checkbox-frame {
|
||||||
|
border-color: rgb(80,80,230);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mat-checkbox-checked {
|
||||||
|
::ng-deep.mat-checkbox-background {
|
||||||
|
background-color: rgb(80,80,230) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.mat-ripple-element {
|
||||||
|
background: rgb(80,80,230) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow {
|
||||||
|
::ng-deep.mat-checkbox-label {
|
||||||
|
color: rgb(150,150,80) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.mat-checkbox-frame {
|
||||||
|
border-color: rgb(150,150,80);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mat-checkbox-checked {
|
||||||
|
::ng-deep.mat-checkbox-background {
|
||||||
|
background-color: rgb(150,150,80) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.mat-ripple-element {
|
||||||
|
background: rgb(150,150,80) !important;
|
||||||
|
}
|
||||||
|
}
|
135
client/src/app/components/climateGraph/climateGraph.component.ts
Normal file
135
client/src/app/components/climateGraph/climateGraph.component.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
import {ChartDataSets} from "chart.js";
|
||||||
|
import * as pluginAnnotations from 'chartjs-plugin-annotation';
|
||||||
|
import {BehaviorSubject} from "rxjs";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'climate-graph',
|
||||||
|
templateUrl: './climateGraph.component.html',
|
||||||
|
styleUrls: ['./climateGraph.component.scss']
|
||||||
|
})
|
||||||
|
export class ClimateGraphComponent {
|
||||||
|
timeFrame = '12';
|
||||||
|
|
||||||
|
xLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
|
||||||
|
|
||||||
|
tempData: number[] = [28, 48, 40, 19, 86, 27, 90];
|
||||||
|
toggleTemp = true;
|
||||||
|
humidityData: number[] = [38, 28, 50, 9, 76, 17, 80];
|
||||||
|
toggleHumidity = true;
|
||||||
|
|
||||||
|
lightData: number[][] = [[1, 3], [4, 6]];
|
||||||
|
toggleLight = false;
|
||||||
|
fanData: number[][] = [[2, 5]];
|
||||||
|
toggleFans = false;
|
||||||
|
|
||||||
|
colors = [{
|
||||||
|
backgroundColor: 'rgba(150,0,0,0.5)',
|
||||||
|
borderColor: 'rgba(150,0,0,0.5)',
|
||||||
|
pointBackgroundColor: 'rgba(0,0,0,0)',
|
||||||
|
pointBorderColor: 'rgba(0,0,0,0)',
|
||||||
|
pointHoverBackgroundColor: 'rgba(0,0,0,0)',
|
||||||
|
pointHoverBorderColor: 'rgba(0,0,0,0)',
|
||||||
|
}, {
|
||||||
|
backgroundColor: 'rgba(0,150,0,0.5)',
|
||||||
|
borderColor: 'rgba(0,150,0,0.5)',
|
||||||
|
pointBackgroundColor: 'rgba(0,0,0,0)',
|
||||||
|
pointBorderColor: 'rgba(0,0,0,0)',
|
||||||
|
pointHoverBackgroundColor: 'rgba(0,0,0,0)',
|
||||||
|
pointHoverBorderColor: 'rgba(0,0,0,0)',
|
||||||
|
}];
|
||||||
|
data = new BehaviorSubject(null);
|
||||||
|
labels = new BehaviorSubject(null);
|
||||||
|
options = new BehaviorSubject(null);
|
||||||
|
plugins = [pluginAnnotations];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.data.next([
|
||||||
|
{data: this.tempData, label: 'Temperature', fill: false, showLine: this.toggleTemp},
|
||||||
|
{data: this.humidityData, label: 'Humidity', fill: false, showLine: this.toggleHumidity}
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.labels.next(this.xLabels);
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
xAxes: [{}],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
id: 'y-axis-0',
|
||||||
|
position: 'left',
|
||||||
|
gridLines: {
|
||||||
|
color: 'rgba(0,0,0,0)',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: (val) => `${val} °C`,
|
||||||
|
beginAtZero: true,
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
precision: 0,
|
||||||
|
stepSize: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'y-axis-1',
|
||||||
|
position: 'right',
|
||||||
|
gridLines: {
|
||||||
|
color: 'rgba(0,0,0,0)',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: (val) => `${Math.round(val * 100)} %`,
|
||||||
|
beginAtZero: true,
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
precision: 0,
|
||||||
|
stepSize: 0.1,
|
||||||
|
suggestedMax: 1,
|
||||||
|
suggestedMin: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
annotation: {
|
||||||
|
annotations: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if(this.toggleLight) {
|
||||||
|
this.lightData.forEach((dataPoint: number[]) => {
|
||||||
|
options.annotation.annotations.push({
|
||||||
|
type: 'box',
|
||||||
|
drawTime: 'beforeDatasetsDraw',
|
||||||
|
xScaleID: 'x-axis-0',
|
||||||
|
yScaleID: 'y-axis-0',
|
||||||
|
xMin: dataPoint[0],
|
||||||
|
xMax: dataPoint[1],
|
||||||
|
yMin: 0,
|
||||||
|
yMax: 100,
|
||||||
|
borderWidth: 3,
|
||||||
|
borderColor: 'orange',
|
||||||
|
backgroundColor: 'rgba(255,255,0,0.1)',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(this.toggleFans) {
|
||||||
|
this.fanData.forEach((dataPoint: number[]) => {
|
||||||
|
options.annotation.annotations.push({
|
||||||
|
type: 'box',
|
||||||
|
drawTime: 'beforeDatasetsDraw',
|
||||||
|
xScaleID: 'x-axis-0',
|
||||||
|
yScaleID: 'y-axis-0',
|
||||||
|
xMin: dataPoint[0],
|
||||||
|
xMax: dataPoint[1],
|
||||||
|
yMin: 0,
|
||||||
|
yMax: 100,
|
||||||
|
borderWidth: 3,
|
||||||
|
borderColor: 'blue',
|
||||||
|
backgroundColor: 'rgba(0,0,255,0.1)',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.options.next(options);
|
||||||
|
}
|
||||||
|
}
|
41
client/src/app/components/fanAuto/fanAuto.component.html
Normal file
41
client/src/app/components/fanAuto/fanAuto.component.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-xl-6">
|
||||||
|
<mat-slide-toggle color="accent" [(ngModel)]="climateService.fanConfig.autoFan" (change)="save()">Automatic</mat-slide-toggle>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-xl-6 mt-4 mt-xl-0">
|
||||||
|
<mat-radio-group [disabled]="!climateService.fanConfig.autoFan" [(ngModel)]="climateService.fanConfig.fanMode" (change)="save()">
|
||||||
|
<mat-radio-button value="time" checked>Time</mat-radio-button>
|
||||||
|
<mat-radio-button class="ml-3" value="temp">Temp</mat-radio-button>
|
||||||
|
</mat-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="climateService.fanConfig.fanMode == 'time'" class="mt-4 row">
|
||||||
|
<div class="col">
|
||||||
|
<mat-form-field class="w-100" appearance="fill" color="accent">
|
||||||
|
<mat-label>From</mat-label>
|
||||||
|
<input matInput type="time" [(ngModel)]="climateService.fanConfig.fanOn" (change)="save()" [disabled]="!climateService.fanConfig.autoFan">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<mat-form-field class="w-100" appearance="fill" color="accent">
|
||||||
|
<mat-label>To</mat-label>
|
||||||
|
<input matInput type="time" [(ngModel)]="climateService.fanConfig.fanOff" (change)="save()" [disabled]="!climateService.fanConfig.autoFan">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="climateService.fanConfig.fanMode == 'temp'" class="mt-4 row">
|
||||||
|
<div class="col">
|
||||||
|
<mat-form-field class="w-100" appearance="fill" color="accent">
|
||||||
|
<mat-label>Temperature</mat-label>
|
||||||
|
<input matInput type="number" [(ngModel)]="climateService.fanConfig.fanTemp" (change)="save()" [disabled]="!climateService.fanConfig.autoFan">
|
||||||
|
<span matSuffix>°C</span>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<mat-form-field class="w-100" appearance="fill" color="accent">
|
||||||
|
<mat-label>Humidity</mat-label>
|
||||||
|
<input matInput type="number" [(ngModel)]="climateService.fanConfig.fanHumidity" (change)="save()" [disabled]="!climateService.fanConfig.autoFan">
|
||||||
|
<span matSuffix>%</span>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
23
client/src/app/components/fanAuto/fanAuto.component.ts
Normal file
23
client/src/app/components/fanAuto/fanAuto.component.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
import {ClimateService} from "../../services/climate.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fan-auto',
|
||||||
|
templateUrl: './fanAuto.component.html'
|
||||||
|
})
|
||||||
|
export class FanAutoComponent {
|
||||||
|
private readonly TIMEOUT = 2500;
|
||||||
|
|
||||||
|
private debuf;
|
||||||
|
|
||||||
|
constructor(public climateService: ClimateService) { }
|
||||||
|
|
||||||
|
save() {
|
||||||
|
if(this.debuf == null) {
|
||||||
|
this.debuf = setTimeout(() => {
|
||||||
|
let ignore = this.climateService.saveFan();
|
||||||
|
this.debuf = null;
|
||||||
|
}, this.TIMEOUT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
<button mat-mini-fab [color]="climateService.fanConfig.on ? 'primary' : ''" (click)="climateService.toggleFan()">
|
||||||
|
<mat-icon>power_settings_new</mat-icon>
|
||||||
|
</button>
|
10
client/src/app/components/fanToggle/fanToggle.component.ts
Normal file
10
client/src/app/components/fanToggle/fanToggle.component.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
import {ClimateService} from "../../services/climate.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fan-toggle',
|
||||||
|
templateUrl: './fanToggle.component.html'
|
||||||
|
})
|
||||||
|
export class FanToggleComponent {
|
||||||
|
constructor(public climateService: ClimateService) { }
|
||||||
|
}
|
15
client/src/app/components/lightAuto/lightAuto.component.html
Normal file
15
client/src/app/components/lightAuto/lightAuto.component.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<mat-slide-toggle color="accent" [(ngModel)]="climateService.lightConfig.autoLight" (change)="save()">Automatic</mat-slide-toggle>
|
||||||
|
<div class="mt-4 row">
|
||||||
|
<div class="col">
|
||||||
|
<mat-form-field class="w-100" appearance="fill" color="accent">
|
||||||
|
<mat-label>From</mat-label>
|
||||||
|
<input matInput type="time" [(ngModel)]="climateService.lightConfig.lightOn" (change)="save()" [disabled]="!climateService.lightConfig.autoLight">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<mat-form-field class="w-100" appearance="fill" color="accent">
|
||||||
|
<mat-label>To</mat-label>
|
||||||
|
<input matInput type="time" [(ngModel)]="climateService.lightConfig.lightOff" (change)="save()" [disabled]="!climateService.lightConfig.autoLight">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
23
client/src/app/components/lightAuto/lightAuto.component.ts
Normal file
23
client/src/app/components/lightAuto/lightAuto.component.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
import {ClimateService} from "../../services/climate.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'light-auto',
|
||||||
|
templateUrl: './lightAuto.component.html'
|
||||||
|
})
|
||||||
|
export class LightAutoComponent {
|
||||||
|
private readonly TIMEOUT = 2500;
|
||||||
|
|
||||||
|
private debuf;
|
||||||
|
|
||||||
|
constructor(public climateService: ClimateService) { }
|
||||||
|
|
||||||
|
save() {
|
||||||
|
if(this.debuf == null) {
|
||||||
|
this.debuf = setTimeout(() => {
|
||||||
|
let ignore = this.climateService.saveLight();
|
||||||
|
this.debuf = null;
|
||||||
|
}, this.TIMEOUT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
<button mat-mini-fab [color]="climateService.lightConfig.on ? 'primary' : ''" (click)="climateService.toggleLight()">
|
||||||
|
<mat-icon>power_settings_new</mat-icon>
|
||||||
|
</button>
|
@ -0,0 +1,10 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
import {ClimateService} from "../../services/climate.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'light-toggle',
|
||||||
|
templateUrl: './lightToggle.component.html'
|
||||||
|
})
|
||||||
|
export class LightToggleComponent {
|
||||||
|
constructor(public climateService: ClimateService) { }
|
||||||
|
}
|
1
client/src/app/components/logo/logo.component.html
Normal file
1
client/src/app/components/logo/logo.component.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<img [ngClass]="{'glow': glow}" src="/assets/images/logo.png" alt="GrowBot">
|
14
client/src/app/components/logo/logo.component.scss
Normal file
14
client/src/app/components/logo/logo.component.scss
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.glow {
|
||||||
|
animation: glow;
|
||||||
|
animation-delay: 1s;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-direction: alternate;
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
filter: drop-shadow(0px 0px 0px green);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
from {filter: drop-shadow(0px 0px 0px #00ff00);}
|
||||||
|
to {filter: drop-shadow(0px 0px 5px #00ff00);}
|
||||||
|
}
|
12
client/src/app/components/logo/logo.component.ts
Normal file
12
client/src/app/components/logo/logo.component.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import {Component, Input} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'logo',
|
||||||
|
templateUrl: `./logo.component.html`,
|
||||||
|
styleUrls: ['./logo.component.scss']
|
||||||
|
})
|
||||||
|
export class LogoComponent {
|
||||||
|
@Input() glow = true;
|
||||||
|
@Input() height = 500;
|
||||||
|
@Input() width = 500;
|
||||||
|
}
|
23
client/src/app/components/menu/menu.component.html
Normal file
23
client/src/app/components/menu/menu.component.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<mat-nav-list class="p-0">
|
||||||
|
<ng-container *ngFor="let item of items; let i = $index">
|
||||||
|
<mat-divider *ngIf="i > 0"></mat-divider>
|
||||||
|
<a mat-list-item [routerLink]="item.link" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||||
|
<span class="p-3 pl-0 mr-5">
|
||||||
|
<mat-icon *ngIf="item.icon" class="mr-4" style="vertical-align: sub">{{item.icon}}</mat-icon>
|
||||||
|
<span>{{item.text}}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<ng-container *ngIf="item.sub?.length">
|
||||||
|
<ng-container *ngFor="let subItem of item.sub">
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<a mat-list-item class="pl-4" [routerLink]="subItem.link" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||||
|
<span class="p-3 pl-0 mr-5">
|
||||||
|
<mat-icon *ngIf="subItem.icon" class="mr-4" style="vertical-align: sub">{{subItem.icon}}</mat-icon>
|
||||||
|
<span>{{subItem.text}}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
</ng-container>
|
||||||
|
</mat-nav-list>
|
4
client/src/app/components/menu/menu.component.scss
Normal file
4
client/src/app/components/menu/menu.component.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.active {
|
||||||
|
background-color: #689f38;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
11
client/src/app/components/menu/menu.component.ts
Normal file
11
client/src/app/components/menu/menu.component.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {Component, Input} from "@angular/core";
|
||||||
|
import {MenuItem} from "../../models/menuItem";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'menu',
|
||||||
|
templateUrl: `./menu.component.html`,
|
||||||
|
styleUrls: ['./menu.component.scss']
|
||||||
|
})
|
||||||
|
export class MenuComponent {
|
||||||
|
@Input() items: MenuItem[] = [];
|
||||||
|
}
|
17
client/src/app/components/stream/stream.component.html
Normal file
17
client/src/app/components/stream/stream.component.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<div class="stream h-100 w-100 position-relative">
|
||||||
|
<div class="top-controls text-white">
|
||||||
|
<button mat-icon-button matTooltip="Take Picture" (click)="cameraService.snap()"><mat-icon style=" text-shadow: 0 0 5px #000;">camera_alt</mat-icon></button>
|
||||||
|
<button mat-icon-button *ngIf="fullscreen" matTooltip="Close" [mat-dialog-close]><mat-icon style=" text-shadow: 0 0 5px #000;">fullscreen_exit</mat-icon></button>
|
||||||
|
<button mat-icon-button *ngIf="!fullscreen" matTooltip="Fullscreen" (click)="openFullscreen()"><mat-icon style=" text-shadow: 0 0 5px #000;">fullscreen</mat-icon></button>
|
||||||
|
</div>
|
||||||
|
<mat-progress-spinner *ngIf="loading" class="center" mode="indeterminate"></mat-progress-spinner>
|
||||||
|
<img id="stream" [src]="stream | async" width="100%" height="100%" alt="stream" [hidden]="loading">
|
||||||
|
<div class="bottom-controls text-white">
|
||||||
|
<button mat-icon-button *ngIf="pause" (click)="pause = !pause" matTooltip="Resume">
|
||||||
|
<mat-icon style="font-size: 2rem; text-shadow: 0 0 5px #000;">play_circle_outline</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button *ngIf="!pause" (click)="pause = !pause" matTooltip="Pause">
|
||||||
|
<mat-icon style="font-size: 2rem; text-shadow: 0 0 5px #000;">pause_circle_outline</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
33
client/src/app/components/stream/stream.component.scss
Normal file
33
client/src/app/components/stream/stream.component.scss
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
.stream {
|
||||||
|
min-height: 250px;
|
||||||
|
min-width: 250px;
|
||||||
|
|
||||||
|
.center {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 50px);
|
||||||
|
top: calc(50% - 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.5vh;
|
||||||
|
right: 1.5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1.5vh;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep.fill-it {
|
||||||
|
border-radius: 0;
|
||||||
|
max-width: 100% !important;
|
||||||
|
|
||||||
|
mat-dialog-container {
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
client/src/app/components/stream/stream.component.ts
Normal file
46
client/src/app/components/stream/stream.component.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import {Component, Input, OnDestroy, OnInit} from "@angular/core";
|
||||||
|
import {CameraService} from "../../services/camera.service";
|
||||||
|
import {filter, map, tap} from "rxjs/operators";
|
||||||
|
import {Observable} from "rxjs";
|
||||||
|
import {MatDialog} from "@angular/material/dialog";
|
||||||
|
import {FullscreenCameraComponent} from "../../views/fullscreenCamera/fullscreenCamera.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'stream',
|
||||||
|
templateUrl: './stream.component.html',
|
||||||
|
styleUrls: ['./stream.component.scss']
|
||||||
|
})
|
||||||
|
export class StreamComponent implements OnDestroy, OnInit{
|
||||||
|
@Input() fullscreen = false;
|
||||||
|
@Input() pause: boolean = false;
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
stream: Observable<any>;
|
||||||
|
|
||||||
|
constructor(public cameraService: CameraService, private matDialog: MatDialog) {
|
||||||
|
this.stream = this.cameraService.stream.pipe(
|
||||||
|
filter(() => !this.pause),
|
||||||
|
tap(() => this.loading = false),
|
||||||
|
map(data => `data:image/jpeg;base64,${data}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
openFullscreen() {
|
||||||
|
let orgStatus = this.pause
|
||||||
|
this.pause = true;
|
||||||
|
this.matDialog.open(FullscreenCameraComponent, {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
panelClass: 'fill-it',
|
||||||
|
autoFocus: false
|
||||||
|
}).afterClosed().subscribe(() => this.pause = orgStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.cameraService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.cameraService.stop();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<div>
|
||||||
|
<div class="d-block d-md-none">
|
||||||
|
<div class="row">
|
||||||
|
<h1 class="ml-3 mb-1">Temperature: {{climateService.temp}} °C</h1>
|
||||||
|
</div>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<div class="row">
|
||||||
|
<h1 class="ml-3 mb-0">
|
||||||
|
Humidity: {{climateService.humidity | percent}}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-none d-md-block">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col border-right pr-2">
|
||||||
|
<h1 class="mb-0">
|
||||||
|
Temperature
|
||||||
|
<br>
|
||||||
|
{{climateService.temp}} °C
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col border-left pl-3">
|
||||||
|
<h1 class="mb-0">
|
||||||
|
Humidity
|
||||||
|
<br>
|
||||||
|
{{climateService.humidity | percent}}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,10 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
import {ClimateService} from "../../services/climate.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'temperature',
|
||||||
|
templateUrl: './temperature.component.html'
|
||||||
|
})
|
||||||
|
export class TemperatureComponent {
|
||||||
|
constructor(public climateService: ClimateService) { }
|
||||||
|
}
|
43
client/src/app/material.module.ts
Normal file
43
client/src/app/material.module.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {MatIconModule} from "@angular/material/icon";
|
||||||
|
import {MatToolbarModule} from "@angular/material/toolbar";
|
||||||
|
import {MatDividerModule} from "@angular/material/divider";
|
||||||
|
import {MatSidenavModule} from "@angular/material/sidenav";
|
||||||
|
import {MatListModule} from "@angular/material/list";
|
||||||
|
import {MatButtonModule} from "@angular/material/button";
|
||||||
|
import {MatCardModule} from "@angular/material/card";
|
||||||
|
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||||
|
import {MatDialogModule} from "@angular/material/dialog";
|
||||||
|
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
|
||||||
|
import {MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||||
|
import {MatCheckboxModule} from "@angular/material/checkbox";
|
||||||
|
import {MatInputModule} from "@angular/material/input";
|
||||||
|
import {MatSelectModule} from "@angular/material/select";
|
||||||
|
import {MatRadioModule} from "@angular/material/radio";
|
||||||
|
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||||
|
|
||||||
|
const modules = [
|
||||||
|
MatButtonModule,
|
||||||
|
MatButtonToggleModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatDividerModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatListModule,
|
||||||
|
MatRadioModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatSidenavModule,
|
||||||
|
MatSlideToggleModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: modules,
|
||||||
|
exports: modules
|
||||||
|
})
|
||||||
|
export class MaterialModule {
|
||||||
|
}
|
99
client/src/app/material.scss
Normal file
99
client/src/app/material.scss
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
@import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500|Material+Icons&display=swap);
|
||||||
|
@import 'node_modules/@angular/material/theming';
|
||||||
|
|
||||||
|
@include mat-core();
|
||||||
|
|
||||||
|
$GrowBot-primary: mat-palette($mat-light-green, 700);
|
||||||
|
$GrowBot-accent: mat-palette($mat-light-blue, 700);
|
||||||
|
$GrowBot-warn: mat-palette($mat-red);
|
||||||
|
|
||||||
|
$GrowBot-light-theme: mat-light-theme((
|
||||||
|
color: (
|
||||||
|
primary: $GrowBot-primary,
|
||||||
|
accent: $GrowBot-accent,
|
||||||
|
warn: $GrowBot-warn,
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
$GrowBot-dark-theme: mat-dark-theme((
|
||||||
|
color: (
|
||||||
|
primary: $GrowBot-primary,
|
||||||
|
accent: $GrowBot-accent,
|
||||||
|
warn: $GrowBot-warn,
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
.light-theme {
|
||||||
|
@include angular-material-theme($GrowBot-light-theme);
|
||||||
|
|
||||||
|
.mat-toolbar-row, .mat-toolbar-single-row {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-drawer-container {
|
||||||
|
background-color: rgba(255, 255, 255, 0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-toolbar-row, .mat-toolbar-single-row {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-0 {
|
||||||
|
mat-dialog-container {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-list-item {
|
||||||
|
&.active {
|
||||||
|
color: #FFFFFF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #94ba73 !important;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: #689f38 !important;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme {
|
||||||
|
@include angular-material-theme($GrowBot-dark-theme);
|
||||||
|
|
||||||
|
.mat-toolbar-row, .mat-toolbar-single-row {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-drawer-container {
|
||||||
|
background-color: rgba(255, 255, 255, 0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-toolbar-row, .mat-toolbar-single-row {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-0 {
|
||||||
|
mat-dialog-container {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-list-item {
|
||||||
|
&:hover {
|
||||||
|
background: #94ba73 !important;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: #689f38 !important;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
client/src/app/models/menuItem.ts
Normal file
7
client/src/app/models/menuItem.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface MenuItem {
|
||||||
|
class?: string;
|
||||||
|
icon?: string;
|
||||||
|
link?: string;
|
||||||
|
text: string;
|
||||||
|
sub?: MenuItem[];
|
||||||
|
}
|
71
client/src/app/services/camera.service.ts
Normal file
71
client/src/app/services/camera.service.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
import SocketIO from 'socket.io-client';
|
||||||
|
import {environment} from "../../environments/environment";
|
||||||
|
import {Subject} from "rxjs";
|
||||||
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CameraService {
|
||||||
|
private socket;
|
||||||
|
|
||||||
|
images: string[] = [];
|
||||||
|
stream: Subject<string>;
|
||||||
|
config = {
|
||||||
|
timelapseEnabled: false,
|
||||||
|
timelapseFrequency: '0 0 12 * * *'
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {
|
||||||
|
this.socket = SocketIO(environment.api)
|
||||||
|
this.stream = new Subject();
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(filename) {
|
||||||
|
let resp = <any>(await this.http.delete(`${environment.api}/timelapse/${filename}`).toPromise());
|
||||||
|
this.images = resp.files;
|
||||||
|
this.config = {
|
||||||
|
timelapseEnabled: resp.timelapseEnabled,
|
||||||
|
timelapseFrequency: resp.timelapseFrequency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async list() {
|
||||||
|
let resp = <any>(await this.http.get(`${environment.api}/timelapse`).toPromise());
|
||||||
|
this.images = resp.files;
|
||||||
|
this.config = {
|
||||||
|
timelapseEnabled: resp.timelapseEnabled,
|
||||||
|
timelapseFrequency: resp.timelapseFrequency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(config) {
|
||||||
|
let resp = <any>(await this.http.put(`${environment.api}/timelapse`, config).toPromise());
|
||||||
|
this.images = resp.files;
|
||||||
|
this.config = {
|
||||||
|
timelapseEnabled: resp.timelapseEnabled,
|
||||||
|
timelapseFrequency: resp.timelapseFrequency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async snap() {
|
||||||
|
let resp = <any>(await this.http.post(`${environment.api}/timelapse`, {}).toPromise());
|
||||||
|
this.images = resp.files;
|
||||||
|
this.config = {
|
||||||
|
timelapseEnabled: resp.timelapseEnabled,
|
||||||
|
timelapseFrequency: resp.timelapseFrequency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.socket.on('stream', data => {
|
||||||
|
this.stream.next(data);
|
||||||
|
});
|
||||||
|
return this.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.socket.off('stream');
|
||||||
|
}
|
||||||
|
}
|
59
client/src/app/services/climate.service.ts
Normal file
59
client/src/app/services/climate.service.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
import {environment} from "../../environments/environment";
|
||||||
|
import {timer} from "rxjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ClimateService {
|
||||||
|
private readonly interval = 5 // Seconds
|
||||||
|
private first = true;
|
||||||
|
|
||||||
|
fanConfig = {
|
||||||
|
on: false,
|
||||||
|
autoFan: false,
|
||||||
|
fanMode: 'time',
|
||||||
|
fanOn: null,
|
||||||
|
fanOff: null,
|
||||||
|
fanTemp: null,
|
||||||
|
fanHumidity: null,
|
||||||
|
}
|
||||||
|
lightConfig = {
|
||||||
|
on: false,
|
||||||
|
autoLight: false,
|
||||||
|
lightOn: null,
|
||||||
|
lightOff: null
|
||||||
|
}
|
||||||
|
temp: number = 0;
|
||||||
|
humidity: number = 0;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {
|
||||||
|
timer(0, this.interval * 1000).subscribe(async () => {
|
||||||
|
if(this.first) {
|
||||||
|
this.fanConfig = <any>(await this.http.get(`${environment.api}/fan/`).toPromise());
|
||||||
|
this.lightConfig = <any>(await this.http.get(`${environment.api}/light/`).toPromise());
|
||||||
|
this.first = false;
|
||||||
|
} else {
|
||||||
|
this.fanConfig.on = <any>(await this.http.get(`${environment.api}/fan/`).toPromise())['on'];
|
||||||
|
this.lightConfig.on = <any>(await this.http.get(`${environment.api}/light/`).toPromise())['on'];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleFan() {
|
||||||
|
return this.fanConfig.on = <any>(await this.http.post(`${environment.api}/fan/`, {}).toPromise())['on'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleLight() {
|
||||||
|
return this.lightConfig.on = <any>(await this.http.post(`${environment.api}/light/`, {}).toPromise())['on'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveFan() {
|
||||||
|
this.fanConfig = <any>(await this.http.put(`${environment.api}/fan/`, this.fanConfig).toPromise());
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveLight() {
|
||||||
|
this.lightConfig = <any>(await this.http.put(`${environment.api}/light/`, this.lightConfig).toPromise());
|
||||||
|
}
|
||||||
|
}
|
30
client/src/app/utils/webStorage.ts
Normal file
30
client/src/app/utils/webStorage.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export interface WebStorageOptions {
|
||||||
|
fieldName?: string;
|
||||||
|
encryptionKey?: string;
|
||||||
|
defaultValue?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocalStorage(opts: WebStorageOptions = {}) {
|
||||||
|
return storage(localStorage, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SessionStorage(opts: WebStorageOptions = {}) {
|
||||||
|
return storage(sessionStorage, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function storage(storageType: Storage, opts: WebStorageOptions) {
|
||||||
|
return function(target: object, key: string) {
|
||||||
|
if(!opts.fieldName) opts.fieldName = key;
|
||||||
|
|
||||||
|
Object.defineProperty(target, key, {
|
||||||
|
get: function() {
|
||||||
|
let value = storageType.getItem(<string>opts.fieldName);
|
||||||
|
if(!value && opts.defaultValue != null) return opts.defaultValue;
|
||||||
|
return JSON.parse(<string>value);
|
||||||
|
},
|
||||||
|
set: function(value) {
|
||||||
|
storageType.setItem(<string>opts.fieldName, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
29
client/src/app/views/app.component.html
Normal file
29
client/src/app/views/app.component.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<div [ngClass]="{'light-theme': !darkMode, 'dark-theme': darkMode}">
|
||||||
|
<mat-toolbar *ngIf="!hide" [@collapseUp]="!hide" [@expandDown]="!hide">
|
||||||
|
<button mat-icon-button *ngIf="mobile" class="mr-2">
|
||||||
|
<mat-icon (click)="open = !open">menu</mat-icon>
|
||||||
|
</button>
|
||||||
|
<img src="assets/images/logo.png" class="mr-2" height="24px" width="auto">
|
||||||
|
<span>
|
||||||
|
<span style="font-weight: 500;">GrowBot</span>
|
||||||
|
<small class="text-primary ml-2">v{{version}}</small>
|
||||||
|
</span>
|
||||||
|
<span class="mx-auto"></span>
|
||||||
|
<span>
|
||||||
|
<mat-slide-toggle [(ngModel)]="darkMode"><mat-icon>invert_colors</mat-icon></mat-slide-toggle>
|
||||||
|
</span>
|
||||||
|
</mat-toolbar>
|
||||||
|
<mat-drawer-container class="fill-height" [hasBackdrop]="false">
|
||||||
|
<mat-drawer [mode]="mobile ? 'push' : 'side'" [opened]="open" [disableClose]="!mobile" [autoFocus]="false">
|
||||||
|
<menu class="m-0 p-0" [items]="menuItems"></menu>
|
||||||
|
</mat-drawer>
|
||||||
|
<mat-drawer-content>
|
||||||
|
<main class="h-100" [@routerTransition]="transition(o)">
|
||||||
|
<router-outlet #o="outlet"></router-outlet>
|
||||||
|
</main>
|
||||||
|
</mat-drawer-content>
|
||||||
|
</mat-drawer-container>
|
||||||
|
<div class="background">
|
||||||
|
<logo style="position: fixed; left: 50vw; top: 50vh; transform: translate(-50%, -50%);"></logo>
|
||||||
|
</div>
|
||||||
|
</div>
|
12
client/src/app/views/app.component.scss
Normal file
12
client/src/app/views/app.component.scss
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.background {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-height {
|
||||||
|
height: calc(100vh - 48px);
|
||||||
|
}
|
61
client/src/app/views/app.component.ts
Normal file
61
client/src/app/views/app.component.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import {Component, HostBinding} from '@angular/core';
|
||||||
|
import {version} from '../../../package.json';
|
||||||
|
import {ActivatedRoute, NavigationEnd, Router} from "@angular/router";
|
||||||
|
import {BreakpointObserver, Breakpoints} from "@angular/cdk/layout";
|
||||||
|
import {filter} from "rxjs/operators";
|
||||||
|
import {collapseUp, expandDown, routerTransition} from "../animations";
|
||||||
|
import {MenuItem} from "../models/menuItem";
|
||||||
|
import {LocalStorage} from "../utils/webStorage";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.scss'],
|
||||||
|
animations: [collapseUp, expandDown, routerTransition]
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
// Theme
|
||||||
|
private body = document.getElementsByTagName('body')[0];
|
||||||
|
@LocalStorage({defaultValue: false}) private _darkMode: boolean;
|
||||||
|
get darkMode(): boolean { return this._darkMode; }
|
||||||
|
set darkMode(val: boolean) {
|
||||||
|
this._darkMode = val;
|
||||||
|
this.body.className = val ? 'dark-theme' : 'light-theme';
|
||||||
|
}
|
||||||
|
|
||||||
|
hide = false; // Hide nav
|
||||||
|
mobile = true; // Mobile or desktop size
|
||||||
|
noTransition = false; // Stop router transitions
|
||||||
|
open = false; // Side nav open
|
||||||
|
version = version; // Version number from package.json
|
||||||
|
|
||||||
|
menuItems: MenuItem[] = [
|
||||||
|
{text: 'Dashboard', icon: 'dashboard', link: '/'},
|
||||||
|
{text: 'Climate', icon: 'eco', link: '/climate'},
|
||||||
|
{text: 'Water', icon: 'waves', link: '/water'},
|
||||||
|
{text: 'Camera', icon: 'videocam', link: '/camera'},
|
||||||
|
{text: 'GrowOps', icon: 'spa', link: '/growops', sub: [
|
||||||
|
{text: 'Schedule', icon: 'event', link: '/schedule'},
|
||||||
|
{text: 'Notes', icon: 'notes', link: '/notes'},
|
||||||
|
]},
|
||||||
|
{text: 'Settings', icon: 'settings', link: '/settings'},
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor(private router: Router, route: ActivatedRoute, breakpointObserver: BreakpointObserver) {
|
||||||
|
this.darkMode = this._darkMode;
|
||||||
|
router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
|
||||||
|
this.hide = route.root.firstChild != null ? !!route.root.firstChild.snapshot.data.hide : false;
|
||||||
|
this.open = !this.hide && !this.mobile;
|
||||||
|
});
|
||||||
|
|
||||||
|
breakpointObserver.observe([Breakpoints.Handset, Breakpoints.Tablet]).subscribe(result => {
|
||||||
|
this.mobile = result.matches;
|
||||||
|
this.open = !this.mobile;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
transition(outlet) {
|
||||||
|
if(!outlet.isActivated || !!outlet.activatedRouteData.noAnimation || this.noTransition) return '';
|
||||||
|
return outlet.activatedRoute;
|
||||||
|
}
|
||||||
|
}
|
57
client/src/app/views/camera/camera.component.html
Normal file
57
client/src/app/views/camera/camera.component.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row pt-3">
|
||||||
|
<div class="col-12 col-lg-8">
|
||||||
|
<mat-card style="padding: 3px">
|
||||||
|
<mat-card-content>
|
||||||
|
<stream></stream>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-4 mt-3 mt-lg-0">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-content>
|
||||||
|
<div>
|
||||||
|
<h2>Timelapse</h2>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<div>
|
||||||
|
<mat-slide-toggle class="mt-3" [(ngModel)]="cameraService.config.timelapseEnabled" color="accent" (change)="save()">Enable</mat-slide-toggle>
|
||||||
|
<mat-form-field class="w-100 mt-3" appearance="fill">
|
||||||
|
<mat-label>Photo Frequency</mat-label>
|
||||||
|
<mat-select [(ngModel)]="cameraService.config.timelapseFrequency" [disabled]="!cameraService.config.timelapseEnabled" (selectionChange)="save()">
|
||||||
|
<mat-option value="0 * * * * *">Minute</mat-option>
|
||||||
|
<mat-option value="0 0 * * * *">Hourly</mat-option>
|
||||||
|
<mat-option value="0 0 12 * * *">Daily</mat-option>
|
||||||
|
<mat-option value="0 0 12 */2 * *">Alternating Days</mat-option>
|
||||||
|
<mat-option value="0 0 12 * * 0">Weekly</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h2>Images</h2>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<div>
|
||||||
|
<div *ngIf="cameraService.images.length == 0" class="pt-4 text-primary">No Images</div>
|
||||||
|
<mat-list>
|
||||||
|
<mat-list-item *ngFor="let image of cameraService.images" class="hover">
|
||||||
|
<div class="w-100 d-flex align-items-center hover">
|
||||||
|
<div class="pr-2 pt-1">
|
||||||
|
<mat-icon>image</mat-icon>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<a target="_blank" [href]="environment.api + '/images/' + image">{{image}}</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button mat-icon-button class="text-danger" (click)="cameraService.del(image)"><mat-icon>delete</mat-icon></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
21
client/src/app/views/camera/camera.component.ts
Normal file
21
client/src/app/views/camera/camera.component.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
import {CameraService} from "../../services/camera.service";
|
||||||
|
import {environment} from "../../../environments/environment";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'camera',
|
||||||
|
templateUrl: `./camera.component.html`
|
||||||
|
})
|
||||||
|
export class CameraComponent {
|
||||||
|
environment = environment;
|
||||||
|
config;
|
||||||
|
|
||||||
|
constructor(public cameraService: CameraService) {
|
||||||
|
let ignore = this.cameraService.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
await this.cameraService.save(this.cameraService.config);
|
||||||
|
this.config = this.cameraService.config;
|
||||||
|
}
|
||||||
|
}
|
57
client/src/app/views/climate/climate.component.html
Normal file
57
client/src/app/views/climate/climate.component.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row" style="align-items: flex-end;">
|
||||||
|
<div class="d-none d-lg-block col-lg-4">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-content>
|
||||||
|
<temperature></temperature>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-4 mt-4">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-content>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div style="writing-mode: vertical-lr;">
|
||||||
|
<light-toggle></light-toggle>
|
||||||
|
<h1 class=" pt-3 d-inline">Light</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ml-3 border-left p-3">
|
||||||
|
<light-auto></light-auto>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-4 mt-4">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-content>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div style="writing-mode: vertical-lr;">
|
||||||
|
<fan-toggle></fan-toggle>
|
||||||
|
<h1 class=" pt-3 d-inline">Fan</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ml-3 border-left p-3">
|
||||||
|
<fan-auto></fan-auto>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="d-block d-lg-none col-12 mt-4">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-content>
|
||||||
|
<temperature></temperature>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row my-4">
|
||||||
|
<div class="col">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-content>
|
||||||
|
<climate-graph></climate-graph>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
8
client/src/app/views/climate/climate.component.ts
Normal file
8
client/src/app/views/climate/climate.component.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
import {ClimateService} from "../../services/climate.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'climate',
|
||||||
|
templateUrl: 'climate.component.html'
|
||||||
|
})
|
||||||
|
export class ClimateComponent { }
|
22
client/src/app/views/dashboard/dashboard.component.html
Normal file
22
client/src/app/views/dashboard/dashboard.component.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-4 mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-content>
|
||||||
|
<temperature></temperature>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<mat-card style="padding: 3px">
|
||||||
|
<mat-card-content>
|
||||||
|
<stream></stream>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
7
client/src/app/views/dashboard/dashboard.component.ts
Normal file
7
client/src/app/views/dashboard/dashboard.component.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'dashboard',
|
||||||
|
templateUrl: './dashboard.component.html'
|
||||||
|
})
|
||||||
|
export class DashboardComponent{ }
|
@ -0,0 +1 @@
|
|||||||
|
<stream [fullscreen]="true"></stream>
|
@ -0,0 +1,7 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'fullscreen-camera',
|
||||||
|
templateUrl: `./fullscreenCamera.component.html`,
|
||||||
|
})
|
||||||
|
export class FullscreenCameraComponent { }
|
0
client/src/app/views/growOps/growOps.component.html
Normal file
0
client/src/app/views/growOps/growOps.component.html
Normal file
9
client/src/app/views/growOps/growOps.component.ts
Normal file
9
client/src/app/views/growOps/growOps.component.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'grow-ops',
|
||||||
|
templateUrl: 'growOps.component.html'
|
||||||
|
})
|
||||||
|
export class GrowOpsComponent {
|
||||||
|
constructor() { }
|
||||||
|
}
|
0
client/src/app/views/notes/notes.component.html
Normal file
0
client/src/app/views/notes/notes.component.html
Normal file
7
client/src/app/views/notes/notes.component.ts
Normal file
7
client/src/app/views/notes/notes.component.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'notes',
|
||||||
|
templateUrl: './notes.component.html'
|
||||||
|
})
|
||||||
|
export class NotesComponent { }
|
7
client/src/app/views/schedule/schedule.component.ts
Normal file
7
client/src/app/views/schedule/schedule.component.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'schedule',
|
||||||
|
templateUrl: 'schedule.component.html'
|
||||||
|
})
|
||||||
|
export class ScheduleComponent { }
|
7
client/src/app/views/settings/settings.component.ts
Normal file
7
client/src/app/views/settings/settings.component.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'settings',
|
||||||
|
templateUrl: 'settings.component.html'
|
||||||
|
})
|
||||||
|
export class SettingsComponent { }
|
0
client/src/app/views/water/water.component.html
Normal file
0
client/src/app/views/water/water.component.html
Normal file
9
client/src/app/views/water/water.component.ts
Normal file
9
client/src/app/views/water/water.component.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'water',
|
||||||
|
templateUrl: 'water.component.html'
|
||||||
|
})
|
||||||
|
export class WaterComponent {
|
||||||
|
constructor() { }
|
||||||
|
}
|
0
client/src/assets/.gitkeep
Normal file
0
client/src/assets/.gitkeep
Normal file
BIN
client/src/assets/images/logo.png
Normal file
BIN
client/src/assets/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
3
client/src/environments/environment.prod.ts
Normal file
3
client/src/environments/environment.prod.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: true
|
||||||
|
};
|
4
client/src/environments/environment.ts
Normal file
4
client/src/environments/environment.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const environment = {
|
||||||
|
api: 'http://localhost:5000',
|
||||||
|
production: false
|
||||||
|
};
|
24
client/src/index.html
Normal file
24
client/src/index.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!doctype html>
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<base href="/">
|
||||||
|
<title>GrowBot</title>
|
||||||
|
<link rel="icon" type="image/png" href="/assets/images/logo.png">
|
||||||
|
|
||||||
|
<!-- TODO: meta, manifest & PWA
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<meta name="theme-color" content="#032f1f">
|
||||||
|
-->
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="background-color: #053825">
|
||||||
|
<app-root>
|
||||||
|
<img style="position: fixed; left: 50vw; top: 50vh; transform: translate(-50%, -50%);" src="/assets/images/logo.png" src="GrowBot">
|
||||||
|
</app-root>
|
||||||
|
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
client/src/main.ts
Normal file
12
client/src/main.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { enableProdMode } from '@angular/core';
|
||||||
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
63
client/src/polyfills.ts
Normal file
63
client/src/polyfills.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||||
|
* You can add your own extra polyfills to this file.
|
||||||
|
*
|
||||||
|
* This file is divided into 2 sections:
|
||||||
|
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||||
|
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||||
|
* file.
|
||||||
|
*
|
||||||
|
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||||
|
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||||
|
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||||
|
*
|
||||||
|
* Learn more in https://angular.io/guide/browser-support
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* BROWSER POLYFILLS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||||
|
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web Animations `@angular/platform-browser/animations`
|
||||||
|
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||||
|
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||||
|
*/
|
||||||
|
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||||
|
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||||
|
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||||
|
* will put import in the top of bundle, so user need to create a separate file
|
||||||
|
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||||
|
* into that file, and then add the following code before importing zone.js.
|
||||||
|
* import './zone-flags';
|
||||||
|
*
|
||||||
|
* The flags allowed in zone-flags.ts are listed here.
|
||||||
|
*
|
||||||
|
* The following flags will work for all browsers.
|
||||||
|
*
|
||||||
|
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||||
|
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||||
|
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||||
|
*
|
||||||
|
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||||
|
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||||
|
*
|
||||||
|
* (window as any).__Zone_enable_cross_context_check = true;
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* Zone JS is required by default for Angular itself.
|
||||||
|
*/
|
||||||
|
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* APPLICATION IMPORTS
|
||||||
|
*/
|
27
client/src/styles.scss
Normal file
27
client/src/styles.scss
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@import 'src/app/bootstrap';
|
||||||
|
@import 'src/app/material';
|
||||||
|
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrollbar
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
|
||||||
|
background-color: #FFFFFF15;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
background-color: #FFFFFF15;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #689f38;
|
||||||
|
}
|
33
client/tsconfig.json
Normal file
33
client/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "es2015",
|
||||||
|
"module": "es2020",
|
||||||
|
"types": [],
|
||||||
|
"lib": [
|
||||||
|
"es2018",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts",
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
server/.gitignore
vendored
Normal file
3
server/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.DS_Store
|
3881
server/package-lock.json
generated
Normal file
3881
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
server/package.json
Normal file
30
server/package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "grow-bot",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"serve": "npm run watch | nodemon dist/main.js",
|
||||||
|
"watch": "npm run build && tsc -w"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^14.0.26",
|
||||||
|
"configstore": "^5.0.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"cron": "^1.8.2",
|
||||||
|
"express": "^4.16.4",
|
||||||
|
"opencv4nodejs": "^5.6.0",
|
||||||
|
"pg": "^8.3.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"socket.io": "^2.3.0",
|
||||||
|
"typeorm": "^0.2.25"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.16.1",
|
||||||
|
"nodemon": "^1.19.0",
|
||||||
|
"typescript": "^3.4.5"
|
||||||
|
}
|
||||||
|
}
|
61
server/src/controllers/fanController.ts
Normal file
61
server/src/controllers/fanController.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import {ClimateService} from "../services/climateService";
|
||||||
|
import ConfigStore from 'configstore';
|
||||||
|
import {Express} from "express";
|
||||||
|
|
||||||
|
export function FanController(app: Express, config: ConfigStore, climate: ClimateService) {
|
||||||
|
const ENDPOINT = '/fan'
|
||||||
|
|
||||||
|
app.get(ENDPOINT, get)
|
||||||
|
app.post(ENDPOINT, post);
|
||||||
|
app.put(ENDPOINT, put);
|
||||||
|
|
||||||
|
let onCron, offCron;
|
||||||
|
|
||||||
|
function cron() {
|
||||||
|
if(config.get('climate.autoFan')) {
|
||||||
|
if (onCron != null) {
|
||||||
|
onCron.stop();
|
||||||
|
onCron = null;
|
||||||
|
}
|
||||||
|
} else if (onCron != null || offCron != null) {
|
||||||
|
onCron.stop();
|
||||||
|
onCron = null;
|
||||||
|
offCron.stop();
|
||||||
|
offCron = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(req, res) {
|
||||||
|
let resp = {
|
||||||
|
on: climate.fanState(),
|
||||||
|
autoFan: config.get('climate.autoFan'),
|
||||||
|
fanMode: config.get('climate.fanMode'),
|
||||||
|
fanOn: config.get('climate.fanOn'),
|
||||||
|
fanOff: config.get('climate.fanOff'),
|
||||||
|
fanTemp: config.get('climate.fanTemp'),
|
||||||
|
fanHumidity: config.get('climate.fanHumidity'),
|
||||||
|
};
|
||||||
|
res.json(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(req, res) {
|
||||||
|
console.log('Toggling fan');
|
||||||
|
climate.toggleFan();
|
||||||
|
get(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function put(req, res) {
|
||||||
|
console.log('Updating fan config');
|
||||||
|
console.log(req.body);
|
||||||
|
if(req.body['autoFan'] != null) config.set('climate.autoFan', req.body['autoFan']);
|
||||||
|
if(req.body['fanMode'] != null) config.set('climate.fanMode', req.body['fanMode']);
|
||||||
|
if(req.body['fanOn'] != null) config.set('climate.fanOn', req.body['fanOn']);
|
||||||
|
if(req.body['fanOff'] != null) config.set('climate.fanOff', req.body['fanOff']);
|
||||||
|
if(req.body['fanTemp'] != null) config.set('climate.fanTemp', req.body['fanTemp']);
|
||||||
|
if(req.body['fanHumidity'] != null) config.set('climate.fanHumidity', req.body['fanHumidity']);
|
||||||
|
cron();
|
||||||
|
get(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
cron();
|
||||||
|
}
|
36
server/src/controllers/lightController.ts
Normal file
36
server/src/controllers/lightController.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import {ClimateService} from "../services/climateService";
|
||||||
|
import ConfigStore from 'configstore';
|
||||||
|
import {Express} from "express";
|
||||||
|
|
||||||
|
export function LightController(app: Express, config: ConfigStore, climate: ClimateService) {
|
||||||
|
const ENDPOINT = '/light'
|
||||||
|
|
||||||
|
app.get(ENDPOINT, get)
|
||||||
|
app.post(ENDPOINT, post);
|
||||||
|
app.put(ENDPOINT, put);
|
||||||
|
|
||||||
|
function get(req, res) {
|
||||||
|
let resp = {
|
||||||
|
on: climate.lightState(),
|
||||||
|
autoLight: config.get('climate.autoLight'),
|
||||||
|
lightOn: config.get('climate.lightOn'),
|
||||||
|
lightOff: config.get('climate.lightOff')
|
||||||
|
};
|
||||||
|
res.json(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(req, res) {
|
||||||
|
console.log('Toggling light');
|
||||||
|
climate.toggleLight();
|
||||||
|
get(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function put(req, res) {
|
||||||
|
console.log('Updating light config');
|
||||||
|
console.log(req.body);
|
||||||
|
if(req.body['autoLight'] != null) config.set('climate.autoLight', req.body['autoLight']);
|
||||||
|
if(req.body['lightOn'] != null) config.set('climate.lightOn', req.body['lightOn']);
|
||||||
|
if(req.body['lightOff'] != null) config.set('climate.lightOff', req.body['lightOff']);
|
||||||
|
get(req, res)
|
||||||
|
}
|
||||||
|
}
|
77
server/src/controllers/timelapseController.ts
Normal file
77
server/src/controllers/timelapseController.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import express, {Express} from 'express';
|
||||||
|
import ConfigStore from 'configstore';
|
||||||
|
import fs from 'fs';
|
||||||
|
import {environment} from "../environments/environment";
|
||||||
|
import {CameraService} from "../services/cameraService";
|
||||||
|
import {CronJob} from 'cron';
|
||||||
|
|
||||||
|
export function TimelapseController(app: Express, config: ConfigStore, camera: CameraService) {
|
||||||
|
const SAVE_DIR = environment.imageDir;
|
||||||
|
const ENDPOINT = '/timelapse'
|
||||||
|
|
||||||
|
app.use('/images', express.static(SAVE_DIR))
|
||||||
|
app.delete(ENDPOINT + '/:filename', del);
|
||||||
|
app.get(ENDPOINT, get)
|
||||||
|
app.post(ENDPOINT, post);
|
||||||
|
app.put(ENDPOINT, put);
|
||||||
|
|
||||||
|
let timelapseCron: CronJob;
|
||||||
|
|
||||||
|
function cron() {
|
||||||
|
if(config.get('camera.timelapseEnabled') == true) {
|
||||||
|
if(timelapseCron != null) {
|
||||||
|
timelapseCron.stop();
|
||||||
|
timelapseCron = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
timelapseCron = new CronJob(config.get('camera.timelapseFrequency'), () => {
|
||||||
|
console.log('Snapping timelapse picture')
|
||||||
|
let date = new Date();
|
||||||
|
let image = camera.snap();
|
||||||
|
let filename = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}.jpg`;
|
||||||
|
fs.writeFileSync(`${SAVE_DIR}/${filename}`, image, 'base64');
|
||||||
|
});
|
||||||
|
timelapseCron.start();
|
||||||
|
} else if(timelapseCron != null) {
|
||||||
|
timelapseCron.stop();
|
||||||
|
timelapseCron = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(req, res) {
|
||||||
|
let filename = req.params.filename;
|
||||||
|
console.log(`Deleting ${filename}`);
|
||||||
|
fs.unlinkSync(`${SAVE_DIR}/${filename}`);
|
||||||
|
get(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(req, res) {
|
||||||
|
let resp = {
|
||||||
|
timelapseEnabled: config.get('camera.timelapseEnabled'),
|
||||||
|
timelapseFrequency: config.get('camera.timelapseFrequency'),
|
||||||
|
files: fs.readdirSync(SAVE_DIR)
|
||||||
|
};
|
||||||
|
res.json(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(req, res) {
|
||||||
|
console.log('Snapping picture')
|
||||||
|
let date = new Date();
|
||||||
|
let image = camera.snap();
|
||||||
|
let filename = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}.jpg`;
|
||||||
|
fs.writeFileSync(`${SAVE_DIR}/${filename}`, image, 'base64');
|
||||||
|
get(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function put(req, res) {
|
||||||
|
console.log('Updating timelapse');
|
||||||
|
console.log(req.body);
|
||||||
|
if(req.body['timelapseEnabled'] != null) config.set('camera.timelapseEnabled', req.body['timelapseEnabled']);
|
||||||
|
if(req.body['timelapseFrequency'] != null) config.set('camera.timelapseFrequency', req.body['timelapseFrequency']);
|
||||||
|
cron();
|
||||||
|
get(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!fs.existsSync(SAVE_DIR)) fs.mkdirSync(SAVE_DIR);
|
||||||
|
cron();
|
||||||
|
}
|
23
server/src/environments/environment.ts
Normal file
23
server/src/environments/environment.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export const environment = {
|
||||||
|
cors: 'http://localhost:4200',
|
||||||
|
imageDir: __dirname + '/../images',
|
||||||
|
port: 5000,
|
||||||
|
defaultConfig: {
|
||||||
|
camera: {
|
||||||
|
timelapseEnabled: true,
|
||||||
|
timelapseFrequency: '0 0 12 * * *' // '0 0 12 * * *'
|
||||||
|
},
|
||||||
|
climate: {
|
||||||
|
autoFan: false,
|
||||||
|
fanMode: 'time',
|
||||||
|
fanOn: null,
|
||||||
|
fanOff: null,
|
||||||
|
fanTemp: null,
|
||||||
|
fanHumidity: null,
|
||||||
|
autoLight: false,
|
||||||
|
lightOn: null,
|
||||||
|
lightOff: null,
|
||||||
|
logRate: '1m',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
server/src/main.ts
Normal file
34
server/src/main.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import ConfigStore from 'configstore';
|
||||||
|
import express from 'express';
|
||||||
|
import {environment} from './environments/environment';
|
||||||
|
import {CameraService} from "./services/cameraService";
|
||||||
|
import SocketIO from 'socket.io';
|
||||||
|
import * as http from 'http';
|
||||||
|
import CORS from 'cors';
|
||||||
|
import {CameraConnectionService} from "./services/cameraConnectionService";
|
||||||
|
import {TimelapseController} from "./controllers/timelapseController";
|
||||||
|
import {ClimateService} from "./services/climateService";
|
||||||
|
import {FanController} from "./controllers/fanController";
|
||||||
|
import {LightController} from "./controllers/lightController";
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const app = express()
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const socket = SocketIO(server);
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(CORS({origin: environment.cors, credentials: true}));
|
||||||
|
const config = new ConfigStore('grow-bot', environment.defaultConfig);
|
||||||
|
config.set(environment.defaultConfig);
|
||||||
|
|
||||||
|
// Services
|
||||||
|
const camera = new CameraService();
|
||||||
|
const cameraConnectionService = new CameraConnectionService(socket, camera);
|
||||||
|
const climateService = new ClimateService();
|
||||||
|
|
||||||
|
// Controllers
|
||||||
|
FanController(app, config, climateService);
|
||||||
|
LightController(app, config, climateService);
|
||||||
|
TimelapseController(app, config, camera);
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
server.listen(environment.port, () => console.log(`Starting Server: http://localhost:${environment.port}`));
|
17
server/src/models/image.ts
Normal file
17
server/src/models/image.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export class Image {
|
||||||
|
constructor(private image) {}
|
||||||
|
|
||||||
|
save(path: string) {
|
||||||
|
return fs.writeFileSync(path, this.image)
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.image;
|
||||||
|
}
|
||||||
|
|
||||||
|
valueOf() {
|
||||||
|
return this.image;
|
||||||
|
}
|
||||||
|
}
|
22
server/src/services/cameraConnectionService.ts
Normal file
22
server/src/services/cameraConnectionService.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {CameraService} from "./cameraService";
|
||||||
|
|
||||||
|
export class CameraConnectionService {
|
||||||
|
private readonly FPS = 4;
|
||||||
|
|
||||||
|
private broadcast;
|
||||||
|
|
||||||
|
constructor(private socket, private camera: CameraService) {
|
||||||
|
this.socket.on('connection', (s) => {
|
||||||
|
let address = s.request.connection.remoteAddress;
|
||||||
|
console.log(`Client Connecting: ${address}`)
|
||||||
|
})
|
||||||
|
this.beginBroadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
beginBroadcast() {
|
||||||
|
this.broadcast = setInterval(() => {
|
||||||
|
let frame = this.camera.snap();
|
||||||
|
this.socket.emit('stream', frame);
|
||||||
|
}, 1000 / this.FPS);
|
||||||
|
}
|
||||||
|
}
|
17
server/src/services/cameraService.ts
Normal file
17
server/src/services/cameraService.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import cv from 'opencv4nodejs';
|
||||||
|
import {Image} from "../models/image";
|
||||||
|
|
||||||
|
export class CameraService {
|
||||||
|
private capture;
|
||||||
|
|
||||||
|
constructor(private resolution: [number, number] = [1920, 1080]) {
|
||||||
|
this.capture = new cv.VideoCapture(0);
|
||||||
|
this.capture.set(cv.CAP_PROP_FRAME_HEIGHT, this.resolution[0]/1); // hack to turn int into double
|
||||||
|
this.capture.set(cv.CAP_PROP_FRAME_WIDTH, this.resolution[1]/1);
|
||||||
|
}
|
||||||
|
|
||||||
|
snap() {
|
||||||
|
let frame = this.capture.read();
|
||||||
|
return cv.imencode('.jpg', frame).toString('base64');
|
||||||
|
}
|
||||||
|
}
|
44
server/src/services/climateService.ts
Normal file
44
server/src/services/climateService.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
export class ClimateService {
|
||||||
|
private readonly interval = 3 // Seconds
|
||||||
|
|
||||||
|
private fanStatus = false;
|
||||||
|
private lightStatus = false;
|
||||||
|
private temp = [25.0];
|
||||||
|
private humidity = [0.65];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
setInterval(() => {
|
||||||
|
let up = Math.random() > 0.5,
|
||||||
|
lastTemp = this.temp[this.temp.length - 1],
|
||||||
|
lastHumid = this.humidity[this.humidity.length - 1];
|
||||||
|
this.temp.push(lastTemp + (up ? 1 : -1) * Math.random())
|
||||||
|
this.humidity.push(lastHumid + (up ? 1 : -1) * Math.random())
|
||||||
|
}, this.interval * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fanState() {
|
||||||
|
return this.fanStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public lightState() {
|
||||||
|
return this.lightStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHumidity() {
|
||||||
|
return this.humidity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTemp() {
|
||||||
|
return this.temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleFan() {
|
||||||
|
this.fanStatus = !this.fanStatus;
|
||||||
|
return this.fanState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleLight() {
|
||||||
|
this.lightStatus = !this.lightStatus;
|
||||||
|
return this.lightState();
|
||||||
|
}
|
||||||
|
}
|
20
server/tsconfig.json
Normal file
20
server/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"target": "es6",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"*": ["node_modules/*"]
|
||||||
|
},
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types"
|
||||||
|
],
|
||||||
|
"lib": ["es2015"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user