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