├── .gitignore
├── README.md
├── client
├── .editorconfig
├── .gitignore
├── README.md
├── angular.json
├── e2e
│ ├── protractor.conf.js
│ ├── src
│ │ ├── app.e2e-spec.ts
│ │ └── app.po.ts
│ └── tsconfig.e2e.json
├── package-lock.json
├── package.json
├── src
│ ├── app
│ │ ├── api.service.spec.ts
│ │ ├── api.service.ts
│ │ ├── app.component.css
│ │ ├── app.component.html
│ │ ├── app.component.spec.ts
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── touchscroll.module.ts
│ │ └── weatherdash
│ │ │ ├── ecobee.types.ts
│ │ │ ├── owm.types.ts
│ │ │ ├── weatherdash.component.css
│ │ │ ├── weatherdash.component.html
│ │ │ ├── weatherdash.component.spec.ts
│ │ │ ├── weatherdash.component.ts
│ │ │ └── weatherdash.config.ts
│ ├── assets
│ │ ├── .gitkeep
│ │ ├── fonts
│ │ │ ├── README.md
│ │ │ ├── digital-7 (italic).ttf
│ │ │ ├── digital-7 (mono italic).ttf
│ │ │ ├── digital-7 (mono).ttf
│ │ │ ├── digital-7.ttf
│ │ │ ├── owfont-regular.eot
│ │ │ ├── owfont-regular.otf
│ │ │ ├── owfont-regular.svg
│ │ │ ├── owfont-regular.ttf
│ │ │ ├── owfont-regular.woff
│ │ │ └── readme.txt
│ │ └── styles
│ │ │ └── owfont-regular.min.css
│ ├── browserslist
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── karma.conf.js
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.css
│ ├── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ └── tslint.json
├── tsconfig.json
└── tslint.json
├── images
├── WeatherDashUI.PNG
└── test
├── server
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── apis.js
├── betterlog.js
├── config.js
├── index.js
├── keys
│ └── .gitignore
├── package-lock.json
└── package.json
└── startup.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeatherDash
2 |
3 | 
4 |
5 | **Smart weather dashboard.**
6 |
7 | **Integrates with OpenWeatherMaps and Ecobee.**
8 |
9 | **Designed to run on RaspberryPi.**
10 |
11 | **Written with Node and Angular.**
12 |
13 |
14 | ## Server
15 | The WeatherDash server is a Node application.
16 |
17 | This server is designed to:
18 |
19 | - Access the configured APIs (currently OpenWeatherMaps and Ecobee)
20 | - Process and cache API reponses
21 | - Host WeatherDash API endpoints to provide this data to the client application.
22 | ### Setting Up the Server
23 | cd server
24 | #### Install Dependencies
25 | npm install
26 | #### Confgure
27 | ##### OpenWeatherMaps
28 | - Go to: https://openweathermap.org/appid
29 | - Login or Create an account
30 | - Generate an API key
31 | - Under the **keys** directory, populate your API key in a file called **owm.key**
32 | - In **config.js**, set the values of **DEFAULT_UNIT** and **DEFAULT_ZIP_CODE** to your preference.
33 | ( *Note*: these settings can be overridden via the WeatherDash API by the client application config and UI)
34 | ##### Ecobee
35 | - Go to: https://www.ecobee.com/developers/
36 | - Login or Create Account
37 | - Enable developer dashboard
38 | - Generate an API key (you'll only need read permissions)
39 | - Under the **keys** directory, populate your API key in a file called **ecobee.key**
40 | ### Starting the Server
41 | npm start
42 | **You'll need to run the server manually the first time.**
43 | The first time you run the server, you'll be presented with instructions for pairing the application with your Ecobee account, using a pin code.
44 | After that succeeds, no further manual interaction with the server should be necessary.
45 | From that point, the server will access the OpenWeatherMaps and Ecobee APIs, negotiate token refreshes, and host an API endpoint for the client application.
46 | The WeatherDash API endpoint will be hosted at localhost:8000
47 |
48 | ## Client
49 | The WeatherDash client is an Angular application.
50 |
51 | The client is designed to:
52 |
53 | - Access API data from server application
54 | - Display time and date info
55 | - Display Ecobee indoor climate info
56 | - Display OpenWeatherMaps city climate info
57 | - Display OpenWeatherMaps city forecast info
58 | ### Setting Up the Client
59 | cd client
60 | #### Install Dependencies
61 | npm install
62 | #### Confgure
63 | In **src/app/weatherdash/weatherdash.config.ts**, set the values of **defaultZip** and **defaultUnit** to your preference.
64 | ( *Note*: these settings can be overridden by the UI and will persist via localstorage)
65 | ### Starting the Client
66 | ng serve
67 | **The WeatherDash UI will be hosted at localhost:4200**
68 |
69 |
70 | ## Using the App
71 | In your browser, navigate to: **localhost:4200**
72 |
73 | There's just a few interactions possible with the app:
74 |
75 | - If multiple thermostats are connected to your Ecobee account, you will be prompted to select the one you wish to display. If you change your mind, simply refresh the page and change your selection.
76 | - Clicking the City Name will allow you to change the zip code. Your choice will be saved across sessions via localStorage.
77 | - Clicking the Unit Symbol (in the upperleft corner of either Indoor or Local weather) will allow you switch between Imperial and Metric units. Your choice will be saved across sessions via localStorage.
78 | - The Forecast can be dragged horizontally to view up to 5 days of weather outlook.
79 |
80 |
81 | ## Setting Up On Raspberry Pi
82 | You will need a Raspberry Pi board with some form of display output. This could be HDMI to a monitor, a MIPI display, a VNC/RDP session, etc. You'll also need a network connection, via onboard WiFi, WiFi dongle, or ethernet cable. And of course, some method for input will be necessary, be it a USB keyboard or ssh.
83 |
84 | Personally, I'll be using a Raspberry Pi 2 Model B, with the Official Raspberry Pi Touch Display. Both will be housed in a SmartiPi Touch case. Since the Pi 2B doesn't have onboard WiFi, I'll be using a cheap (RTL8188CUS) WiFi dongle. And lastly, a USB keyboard during initial configuration.
85 |
86 | Setup your Pi with Raspbian according to the official documentation. Follow through the steps to get a network connection and configure your display if necessary.
87 |
88 | ### Disable Screensaver (optional)
89 | You'll likely want to disable the screensaver so that the WeatherDash app is always on display:
90 |
91 | sudo apt-get install xscreensaver
92 | xscreensaver
93 |
94 | Click **Settings**, and set **Mode** to **Disable Screen Saver**
95 |
96 | ### Hide Cursor on Idle (optional)
97 | Install unclutter:
98 |
99 | sudo apt-get install unclutter
100 |
101 | To hide cursor after being idle for 3 seconds, add the following line to **/home/pi/.config/lxsession/LXDE-pi/autostart**:
102 |
103 | @unclutter -idle 3
104 |
105 | ### Dim Backlight (optional)
106 | If you are using the offical Raspberry Pi 7" Touch Display, there's a virtual filesystem which can be used to interact with the display:
107 |
108 | sudo sh -c 'echo "128" > /sys/class/backlight/rpi_backlight/brightness'
109 |
110 | The number you echo must be between 0 and 255. Find a number that works for you. You want to set it such that the display is bright enough to cut through most glare in a well-lit room while also dim enough to not be harsh in a darkened room. Lower brightness means lower power consumption. I eventually settled on 20.
111 |
112 | ### Overclock (optional)
113 | Downloading NPM packages and building Angular applications can be a bit slow on the Raspberry Pi. As an optional step, you may wish to overclock your Pi. This can be done via the Overclock menu in:
114 |
115 | sudo raspi-config
116 |
117 | (*Note*: You'll want to reboot if you changed any Overclock settings)
118 |
119 | ### Install Node and NPM
120 | Then, you'll want to install a modern version of Node and NPM:
121 |
122 | curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
123 | sudo apt-get install -y nodejs
124 |
125 |
126 | With NPM, install the Angular CLI and HTTP Server:
127 |
128 | npm install -g @angular/cli
129 | npm install -g angular-http-server
130 |
131 | ### Install WeatherDash
132 | If you haven't done so already, clone this repo:
133 |
134 | git clone https://github.com/Mrjohns42/WeatherDash.git /home/pi/WeatherDash
135 |
136 | Follow the sections above to setup and start the Server and Client, and then open the WeatherDash app in a browser to verify your setup was done correctly.
137 |
138 | To automatically run the Client and Server and open the WeatherDash app in fullscreen mode, there is a helpful bash script in the root of the repo.
139 |
140 | startup.sh
141 |
142 | (*Note*: to exit Chromium's Fullscreen Mode, use F11)
143 |
144 | To run this script automatically at Login, add the following line to **/home/pi/.config/lxsession/LXDE-pi/autostart**:
145 |
146 | @lxterminal -e /home/pi/WeatherDash/startup.sh
147 |
148 | Then reboot, and WeatherDash should automatically build and launch.
149 |
150 | #### ENJOY!
151 |
152 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/client/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | yarn-error.log
34 | testem.log
35 | /typings
36 |
37 | # System Files
38 | .DS_Store
39 | Thumbs.db
40 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # WeatherClient
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.1.1.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
24 |
25 | ## Further help
26 |
27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
28 |
--------------------------------------------------------------------------------
/client/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "WeatherClient": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "prefix": "app",
11 | "schematics": {},
12 | "architect": {
13 | "build": {
14 | "builder": "@angular-devkit/build-angular:browser",
15 | "options": {
16 | "outputPath": "dist/WeatherClient",
17 | "index": "src/index.html",
18 | "main": "src/main.ts",
19 | "polyfills": "src/polyfills.ts",
20 | "tsConfig": "src/tsconfig.app.json",
21 | "assets": [
22 | "src/favicon.ico",
23 | "src/assets"
24 | ],
25 | "styles": [
26 | "src/styles.css"
27 | ],
28 | "scripts": []
29 | },
30 | "configurations": {
31 | "production": {
32 | "fileReplacements": [
33 | {
34 | "replace": "src/environments/environment.ts",
35 | "with": "src/environments/environment.prod.ts"
36 | }
37 | ],
38 | "optimization": true,
39 | "outputHashing": "all",
40 | "sourceMap": false,
41 | "extractCss": true,
42 | "namedChunks": false,
43 | "aot": true,
44 | "extractLicenses": true,
45 | "vendorChunk": false,
46 | "buildOptimizer": true
47 | }
48 | }
49 | },
50 | "serve": {
51 | "builder": "@angular-devkit/build-angular:dev-server",
52 | "options": {
53 | "browserTarget": "WeatherClient:build"
54 | },
55 | "configurations": {
56 | "production": {
57 | "browserTarget": "WeatherClient:build:production"
58 | }
59 | }
60 | },
61 | "extract-i18n": {
62 | "builder": "@angular-devkit/build-angular:extract-i18n",
63 | "options": {
64 | "browserTarget": "WeatherClient:build"
65 | }
66 | },
67 | "test": {
68 | "builder": "@angular-devkit/build-angular:karma",
69 | "options": {
70 | "main": "src/test.ts",
71 | "polyfills": "src/polyfills.ts",
72 | "tsConfig": "src/tsconfig.spec.json",
73 | "karmaConfig": "src/karma.conf.js",
74 | "styles": [
75 | "src/styles.css"
76 | ],
77 | "scripts": [],
78 | "assets": [
79 | "src/favicon.ico",
80 | "src/assets"
81 | ]
82 | }
83 | },
84 | "lint": {
85 | "builder": "@angular-devkit/build-angular:tslint",
86 | "options": {
87 | "tsConfig": [
88 | "src/tsconfig.app.json",
89 | "src/tsconfig.spec.json"
90 | ],
91 | "exclude": [
92 | "**/node_modules/**"
93 | ]
94 | }
95 | }
96 | }
97 | },
98 | "WeatherClient-e2e": {
99 | "root": "e2e/",
100 | "projectType": "application",
101 | "architect": {
102 | "e2e": {
103 | "builder": "@angular-devkit/build-angular:protractor",
104 | "options": {
105 | "protractorConfig": "e2e/protractor.conf.js",
106 | "devServerTarget": "WeatherClient:serve"
107 | },
108 | "configurations": {
109 | "production": {
110 | "devServerTarget": "WeatherClient:serve:production"
111 | }
112 | }
113 | },
114 | "lint": {
115 | "builder": "@angular-devkit/build-angular:tslint",
116 | "options": {
117 | "tsConfig": "e2e/tsconfig.e2e.json",
118 | "exclude": [
119 | "**/node_modules/**"
120 | ]
121 | }
122 | }
123 | }
124 | }
125 | },
126 | "defaultProject": "WeatherClient"
127 | }
--------------------------------------------------------------------------------
/client/e2e/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './src/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | onPrepare() {
23 | require('ts-node').register({
24 | project: require('path').join(__dirname, './tsconfig.e2e.json')
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
--------------------------------------------------------------------------------
/client/e2e/src/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 |
3 | describe('workspace-project App', () => {
4 | let page: AppPage;
5 |
6 | beforeEach(() => {
7 | page = new AppPage();
8 | });
9 |
10 | it('should display welcome message', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('Welcome to WeatherClient!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/client/e2e/src/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class AppPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('app-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "types": [
8 | "jasmine",
9 | "jasminewd2",
10 | "node"
11 | ]
12 | }
13 | }
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "weather-client",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "test": "ng test",
9 | "lint": "ng lint",
10 | "e2e": "ng e2e"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "^6.1.0",
15 | "@angular/common": "^6.1.0",
16 | "@angular/compiler": "^6.1.0",
17 | "@angular/core": "^6.1.0",
18 | "@angular/forms": "^6.1.0",
19 | "@angular/http": "^6.1.0",
20 | "@angular/platform-browser": "^6.1.0",
21 | "@angular/platform-browser-dynamic": "^6.1.0",
22 | "@angular/router": "^6.1.0",
23 | "angular-fittext": "^2.1.1",
24 | "core-js": "^2.5.4",
25 | "rxjs": "^6.0.0",
26 | "zone.js": "~0.8.26"
27 | },
28 | "devDependencies": {
29 | "@angular-devkit/build-angular": "~0.7.0",
30 | "@angular/cli": "~6.1.1",
31 | "@angular/compiler-cli": "^6.1.0",
32 | "@angular/language-service": "^6.1.0",
33 | "@types/jasmine": "~2.8.6",
34 | "@types/jasminewd2": "~2.0.3",
35 | "@types/node": "~8.9.4",
36 | "codelyzer": "~4.2.1",
37 | "jasmine-core": "~2.99.1",
38 | "jasmine-spec-reporter": "~4.2.1",
39 | "karma": "~1.7.1",
40 | "karma-chrome-launcher": "~2.2.0",
41 | "karma-coverage-istanbul-reporter": "~2.0.0",
42 | "karma-jasmine": "~1.1.1",
43 | "karma-jasmine-html-reporter": "^0.2.2",
44 | "protractor": "^5.4.0",
45 | "ts-node": "~5.0.1",
46 | "tslint": "~5.9.1",
47 | "typescript": "~2.7.2"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/src/app/api.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { APIService } from './api.service';
4 |
5 | describe('APIService', () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [APIService]
9 | });
10 | });
11 |
12 | it('should be created', inject([APIService], (service: APIService) => {
13 | expect(service).toBeTruthy();
14 | }));
15 | });
16 |
--------------------------------------------------------------------------------
/client/src/app/api.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 |
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class APIService {
9 |
10 | constructor(private http: HttpClient) { }
11 |
12 | getWeather() {
13 | const url = 'http://localhost:8000/api/weather';
14 | console.log('Accessing: ' + url);
15 | return this.http.get(url);
16 | }
17 |
18 | getForecast() {
19 | const url = 'http://localhost:8000/api/forecast';
20 | console.log('Accessing: ' + url);
21 | return this.http.get(url);
22 | }
23 |
24 | getIndoor() {
25 | const url = 'http://localhost:8000/api/indoor';
26 | console.log('Accessing: ' + url);
27 | return this.http.get(url);
28 | }
29 |
30 | configureSettings(zip, unit) {
31 | let url = 'http://localhost:8000/api/settings?';
32 | if (zip != null) { url += '&zip=' + zip; }
33 | if (unit != null) { url += '&units=' + unit; }
34 | console.log('Accessing: ' + url);
35 | return this.http.post(url, null, {observe: 'response'});
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/app/app.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/app/app.component.css
--------------------------------------------------------------------------------
/client/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/client/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, async } from '@angular/core/testing';
2 | import { AppComponent } from './app.component';
3 | describe('AppComponent', () => {
4 | beforeEach(async(() => {
5 | TestBed.configureTestingModule({
6 | declarations: [
7 | AppComponent
8 | ],
9 | }).compileComponents();
10 | }));
11 | it('should create the app', async(() => {
12 | const fixture = TestBed.createComponent(AppComponent);
13 | const app = fixture.debugElement.componentInstance;
14 | expect(app).toBeTruthy();
15 | }));
16 | it(`should have as title 'WeatherDash'`, async(() => {
17 | const fixture = TestBed.createComponent(AppComponent);
18 | const app = fixture.debugElement.componentInstance;
19 | expect(app.title).toEqual('WeatherDash');
20 | }));
21 | it('should render title in a h1 tag', async(() => {
22 | const fixture = TestBed.createComponent(AppComponent);
23 | fixture.detectChanges();
24 | const compiled = fixture.debugElement.nativeElement;
25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to WeatherDash!');
26 | }));
27 | });
28 |
--------------------------------------------------------------------------------
/client/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.css']
7 | })
8 | export class AppComponent {
9 | title = 'WeatherDash';
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 | import { HttpClientModule } from '@angular/common/http';
4 |
5 | import {AngularFittextModule} from 'angular-fittext';
6 |
7 | import { AppComponent } from './app.component';
8 | import { WeatherdashComponent } from './weatherdash/weatherdash.component';
9 | import { APIService } from './api.service';
10 |
11 | @NgModule({
12 | declarations: [
13 | AppComponent,
14 | WeatherdashComponent
15 | ],
16 | imports: [
17 | BrowserModule,
18 | HttpClientModule,
19 | AngularFittextModule
20 | ],
21 | providers: [APIService],
22 | bootstrap: [AppComponent]
23 | })
24 | export class AppModule { }
25 |
--------------------------------------------------------------------------------
/client/src/app/touchscroll.module.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * TouchScroll - using dom overflow:scroll
3 | * by kmturley
4 | * https://github.com/kmturley/touch-scroll
5 | */
6 |
7 | /*globals window, document */
8 |
9 |
10 | export class TouchScroll {
11 | axis = 'x';
12 | drag = false;
13 | zoom = 1;
14 | time = 0.04;
15 | isIE = window.navigator.userAgent.toLowerCase().indexOf('msie') > -1;
16 | isFirefox = window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
17 | options = null;
18 | el = null;
19 | elzoom = null;
20 | body = null;
21 | prev = null;
22 | next = null;
23 | diffx = null;
24 | diffy = null;
25 | startx = null;
26 | starty = null;
27 |
28 | /**
29 | * @method init
30 | */
31 | init(options) {
32 | const me = this;
33 | this.options = options;
34 |
35 | // find target element or fall back to body
36 | if (options && options.id) {
37 | this.el = document.getElementById(options.id);
38 | }
39 | if (!this.el) {
40 | if (this.isIE || this.isFirefox) {
41 | this.el = document.documentElement;
42 | } else {
43 | this.el = document.body;
44 | }
45 | }
46 |
47 | // if draggable option is enabled add events
48 | if (options.draggable === true) {
49 | if (this.isIE) {
50 | document.ondragstart = function () { return false; };
51 | }
52 | if (this.isIE || this.isFirefox) {
53 | this.body = document.documentElement;
54 | } else {
55 | this.body = document.body;
56 | }
57 | this.addEvent('mousedown', this.el, function (e) { me.onMouseDown(e); });
58 | this.addEvent('mousemove', this.el, function (e) { me.onMouseMove(e); });
59 | this.addEvent('mouseup', this.body, function (e) { me.onMouseUp(e); });
60 | }
61 |
62 | // if zoom option exists add mouse wheel functionality to element
63 | if (options && options.zoom) {
64 | this.elzoom = document.getElementById(options.zoom);
65 | if (this.isFirefox) {
66 | this.addEvent('DOMMouseScroll', this.el, function (e) { me.onMouseWheel(e); });
67 | } else {
68 | this.addEvent('mousewheel', this.el, function (e) { me.onMouseWheel(e); });
69 | }
70 | }
71 |
72 | // if scroll options exist add events
73 | if (options && options.prev) {
74 | this.prev = document.getElementById(options.prev);
75 | this.addEvent('mousedown', this.prev, function (e) {
76 | me.onMouseDown(e);
77 | });
78 | this.addEvent('mouseup', this.prev, function (e) {
79 | me.diffx = options.distance ? (-options.distance / 11) : -11;
80 | me.onMouseUp(e);
81 | });
82 | }
83 | if (options && options.next) {
84 | this.next = document.getElementById(options.next);
85 | this.addEvent('mousedown', this.next, function (e) {
86 | me.onMouseDown(e);
87 | });
88 | this.addEvent('mouseup', this.next, function (e) {
89 | me.diffx = options.distance ? (options.distance / 11) : 11;
90 | me.onMouseUp(e);
91 | });
92 | }
93 | }
94 |
95 | /**
96 | * @method addEvent
97 | */
98 | addEvent(name, el, func) {
99 | if (el.addEventListener) {
100 | el.addEventListener(name, func, false);
101 | } else if (el.attachEvent) {
102 | el.attachEvent('on' + name, func);
103 | } else {
104 | el[name] = func;
105 | }
106 | }
107 |
108 | /**
109 | * @method cancelEvent
110 | */
111 | cancelEvent(e) {
112 | if (!e) { e = window.event; }
113 | if (e.target && e.target.nodeName === 'IMG') {
114 | e.preventDefault();
115 | } else if (e.srcElement && e.srcElement.nodeName === 'IMG') {
116 | e.returnValue = false;
117 | }
118 | }
119 |
120 | /**
121 | * @method onMouseDown
122 | */
123 | onMouseDown(e) {
124 | if (this.drag === false || this.options.wait === false) {
125 | this.drag = true;
126 | this.cancelEvent(e);
127 | this.startx = e.clientX + this.el.scrollLeft;
128 | this.starty = e.clientY + this.el.scrollTop;
129 | this.diffx = 0;
130 | this.diffy = 0;
131 | }
132 | }
133 |
134 | /**
135 | * @method onMouseMove
136 | */
137 | onMouseMove(e) {
138 | if (this.drag === true) {
139 | this.cancelEvent(e);
140 | this.diffx = (this.startx - (e.clientX + this.el.scrollLeft));
141 | this.diffy = (this.starty - (e.clientY + this.el.scrollTop));
142 | this.el.scrollLeft += this.diffx;
143 | this.el.scrollTop += this.diffy;
144 | }
145 | }
146 |
147 | /**
148 | * @method onMouseMove
149 | */
150 | onMouseUp(e) {
151 | if (this.drag === true) {
152 | if (!this.options.wait) {
153 | this.drag = null;
154 | }
155 | this.cancelEvent(e);
156 | const me = this;
157 | let start = 1,
158 | animateId = null;
159 | const animate = function() {
160 | const step = Math.sin(start);
161 | if (step <= 0) {
162 | me.diffx = 0;
163 | me.diffy = 0;
164 | window.cancelAnimationFrame(animateId);
165 | me.drag = false;
166 | } else {
167 | me.el.scrollLeft += me.diffx * step;
168 | me.el.scrollTop += me.diffy * step;
169 | start -= me.time;
170 | animateId = window.requestAnimationFrame(animate);
171 | }
172 | };
173 | animate();
174 | }
175 | }
176 |
177 | /**
178 | * @method onMouseMove
179 | */
180 | onMouseWheel(e) {
181 | this.cancelEvent(e);
182 | if (e.detail) {
183 | this.zoom -= e.detail;
184 | } else {
185 | this.zoom += (e.wheelDelta / 1200);
186 | }
187 | if (this.zoom < 1) {
188 | this.zoom = 1;
189 | } else if (this.zoom > 10) {
190 | this.zoom = 10;
191 | }
192 | /*
193 | this.elzoom.style.OTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')';
194 | this.elzoom.style.MozTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')';
195 | this.elzoom.style.msTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')';
196 | this.elzoom.style.WebkitTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')';
197 | this.elzoom.style.transform = 'scale(' + this.zoom + ', ' + this.zoom + ')';
198 | */
199 | this.elzoom.style.zoom = this.zoom * 100 + '%';
200 | // this.el.scrollLeft += e.wheelDelta / 10;
201 | // this.el.scrollTop += e.wheelDelta / 8;
202 | }
203 |
204 | }
205 |
--------------------------------------------------------------------------------
/client/src/app/weatherdash/ecobee.types.ts:
--------------------------------------------------------------------------------
1 |
2 | class Page {
3 | page = null;
4 | totalPages = null;
5 | pageSize = null;
6 | total = null;
7 |
8 | constructor(obj) {
9 | if (obj != null) {
10 | if (obj.hasOwnProperty('page')) { this.page = obj['page']; }
11 | if (obj.hasOwnProperty('totalPages')) { this.totalPages = obj['totalPages']; }
12 | if (obj.hasOwnProperty('pageSize')) { this.pageSize = obj['pageSize']; }
13 | if (obj.hasOwnProperty('total')) { this.total = obj['total']; }
14 | }
15 | }
16 | }
17 |
18 | class Status {
19 | code = null;
20 | message = null;
21 |
22 | constructor(obj) {
23 | if (obj != null) {
24 | if (obj.hasOwnProperty('code')) { this.code = obj['code']; }
25 | if (obj.hasOwnProperty('message')) { this.message = obj['message']; }
26 | }
27 | }
28 | }
29 |
30 | class Runtime {
31 | runtimeRev = null;
32 | connected = null;
33 | firstConnected = null;
34 | connectDateTime = null;
35 | disconnectDateTime = null;
36 | lastModified = null;
37 | lastStatusModified = null;
38 | runtimeDate = null;
39 | runtimeInterval = null;
40 | actualTemperature = null;
41 | actualHumidity = null;
42 | desiredHeat = null;
43 | desiredCool = null;
44 | desiredHumidity = null;
45 | desiredDehumidity = null;
46 | desiredFanMode = null;
47 | desiredHeatRange: number[] = null;
48 | desiredCoolRange: number[] = null;
49 |
50 | constructor(obj) {
51 | if (obj != null) {
52 | if (obj.hasOwnProperty('runtimeRev')) { this.runtimeRev = obj['runtimeRev']; }
53 | if (obj.hasOwnProperty('connected')) { this.connected = obj['connected']; }
54 | if (obj.hasOwnProperty('firstConnected')) { this.firstConnected = obj['firstConnected']; }
55 | if (obj.hasOwnProperty('connectDateTime')) { this.connectDateTime = obj['connectDateTime']; }
56 | if (obj.hasOwnProperty('disconnectDateTime')) { this.disconnectDateTime = obj['disconnectDateTime']; }
57 | if (obj.hasOwnProperty('lastModified')) { this.lastModified = obj['lastModified']; }
58 | if (obj.hasOwnProperty('lastStatusModified')) { this.lastStatusModified = obj['lastStatusModified']; }
59 | if (obj.hasOwnProperty('runtimeDate')) { this.runtimeDate = obj['runtimeDate']; }
60 | if (obj.hasOwnProperty('runtimeInterval')) { this.runtimeInterval = obj['runtimeInterval']; }
61 | if (obj.hasOwnProperty('actualTemperature')) { this.actualTemperature = obj['actualTemperature']; }
62 | if (obj.hasOwnProperty('actualHumidity')) { this.actualHumidity = obj['actualHumidity']; }
63 | if (obj.hasOwnProperty('desiredHeat')) { this.desiredHeat = obj['desiredHeat']; }
64 | if (obj.hasOwnProperty('desiredCool')) { this.desiredCool = obj['desiredCool']; }
65 | if (obj.hasOwnProperty('desiredHumidity')) { this.desiredHumidity = obj['desiredHumidity']; }
66 | if (obj.hasOwnProperty('desiredDehumidity')) { this.desiredDehumidity = obj['desiredDehumidity']; }
67 | if (obj.hasOwnProperty('desiredFanMode')) { this.desiredFanMode = obj['desiredFanMode']; }
68 | if (obj.hasOwnProperty('desiredHeatRange')) { this.desiredHeatRange = obj['desiredHeatRange']; }
69 | if (obj.hasOwnProperty('desiredCoolRange')) { this.desiredCoolRange = obj['desiredCoolRange']; }
70 | }
71 | }
72 | }
73 |
74 | class Thermostat {
75 | identifier = null;
76 | name = null;
77 | thermostatRev = null;
78 | isRegistered = null;
79 | modelNumber = null;
80 | brand = null;
81 | features = null;
82 | lastModified = null;
83 | thermostatTime = null;
84 | utcTime = null;
85 |
86 | runtime: Runtime = null;
87 |
88 | // settings:Settings = null;
89 | // alerts:Alert[] = null;
90 | // events:TEvent[] = null;
91 | // audio:Audio = null;
92 | // reminders:Reminder[] = null;
93 | // extendedRuntime:ExtendedRuntime = null;
94 | // electricity:Electricity = null;
95 | // devices:Device[] = null;
96 | // location:Location = null;
97 | // energy:Energy = null;
98 | // technician:Technician = null;
99 | // utility:Utility = null;
100 | // management:Management = null;
101 | // weather:Weather = null;
102 | // program: Program = null;
103 | // houseDetails:HouseDetails = null;
104 | // oemCfg:TherostatOemCfg = null;
105 | // equipmentStatus = null;
106 | // notificationSettings:NotificationSettings = null;
107 | // privacy:ThermostatPrivacy = null;
108 | // version:Version = null;
109 | // securitySettings:SecuritySettings = null;
110 | // remoteSensors:RemoteSensor[] = null;
111 |
112 | constructor(obj) {
113 | if (obj != null) {
114 | if (obj.hasOwnProperty('identifier')) { this.identifier = obj['identifier']; }
115 | if (obj.hasOwnProperty('name')) { this.name = obj['name']; }
116 | if (obj.hasOwnProperty('thermostatRev')) { this.thermostatRev = obj['thermostatRev']; }
117 | if (obj.hasOwnProperty('isRegistered')) { this.isRegistered = obj['isRegistered']; }
118 | if (obj.hasOwnProperty('modelNumber')) { this.modelNumber = obj['modelNumber']; }
119 | if (obj.hasOwnProperty('brand')) { this.brand = obj['brand']; }
120 | if (obj.hasOwnProperty('features')) { this.features = obj['features']; }
121 | if (obj.hasOwnProperty('lastModified')) { this.lastModified = obj['lastModified']; }
122 | if (obj.hasOwnProperty('thermostatTime')) { this.thermostatTime = obj['thermostatTime']; }
123 | if (obj.hasOwnProperty('utcTime')) { this.utcTime = obj['utcTime']; }
124 | if (obj.hasOwnProperty('runtime')) { this.runtime = new Runtime(obj['runtime']); }
125 | }
126 | }
127 | }
128 |
129 | export class EcobeeIndoor {
130 | status: Status = null;
131 | page: Page = null;
132 | thermostatList: Thermostat[] = null;
133 |
134 | constructor(obj) {
135 | if (obj != null) {
136 | if (obj.hasOwnProperty('status')) { this.status = new Status(obj['status']); }
137 | if (obj.hasOwnProperty('page')) { this.page = new Page(obj['page']); }
138 | if (obj.hasOwnProperty('thermostatList')) {
139 | this.thermostatList = [];
140 | for (const idx in obj['thermostatList']) {
141 | if (!idx) { continue; }
142 | const elem = obj['thermostatList'][idx];
143 | const thermostat = new Thermostat(elem);
144 | this.thermostatList.push(thermostat);
145 | }
146 | }
147 | }
148 | }
149 |
150 | }
151 |
--------------------------------------------------------------------------------
/client/src/app/weatherdash/owm.types.ts:
--------------------------------------------------------------------------------
1 |
2 | const OWM_ICON_URL = 'https://openweathermap.org/img/w/';
3 | export function formOWMIconURL(icon) {
4 | return OWM_ICON_URL + icon + '.png';
5 | }
6 |
7 |
8 |
9 | class Coord {
10 | lat = null;
11 | lon = null;
12 |
13 | constructor(obj) {
14 | if (obj != null) {
15 | if (obj.hasOwnProperty('lon')) { this.lon = obj['lon']; }
16 | if (obj.hasOwnProperty('lat')) { this.lat = obj['lat']; }
17 | }
18 | }
19 | }
20 |
21 | class Weather {
22 | id = null;
23 | main = null;
24 | description = null;
25 | icon = null;
26 |
27 | constructor(obj) {
28 | if (obj != null) {
29 | if (obj.hasOwnProperty('id')) { this.id = obj['id']; }
30 | if (obj.hasOwnProperty('main')) { this.main = obj['main']; }
31 | if (obj.hasOwnProperty('description')) { this.description = obj['description']; }
32 | if (obj.hasOwnProperty('icon')) { this.icon = obj['icon']; }
33 | }
34 | }
35 | }
36 |
37 | class Main {
38 | temp = null;
39 | pressure = null;
40 | humidity = null;
41 | temp_min = null;
42 | temp_max = null;
43 | sea_level = null;
44 | grnd_level = null;
45 |
46 | constructor(obj) {
47 | if (obj != null) {
48 | if (obj.hasOwnProperty('temp')) { this.temp = obj['temp']; }
49 | if (obj.hasOwnProperty('pressure')) { this.pressure = obj['pressure']; }
50 | if (obj.hasOwnProperty('humidity')) { this.humidity = obj['humidity']; }
51 | if (obj.hasOwnProperty('temp_min')) { this.temp_min = obj['temp_min']; }
52 | if (obj.hasOwnProperty('temp_max')) { this.temp_max = obj['temp_max']; }
53 | if (obj.hasOwnProperty('sea_level')) { this.sea_level = obj['sea_level']; }
54 | if (obj.hasOwnProperty('grnd_level')) { this.grnd_level = obj['grnd_level']; }
55 | }
56 | }
57 | }
58 |
59 | class Wind {
60 | speed = null;
61 | deg = null;
62 |
63 | constructor(obj) {
64 | if (obj != null) {
65 | if (obj.hasOwnProperty('speed')) { this.speed = obj['speed']; }
66 | if (obj.hasOwnProperty('deg')) { this.deg = obj['deg']; }
67 | }
68 | }
69 | }
70 |
71 | class Cloud {
72 | all = null;
73 |
74 | constructor(obj) {
75 | if (obj != null) {
76 | if (obj.hasOwnProperty('all')) { this.all = obj['all']; }
77 | }
78 | }
79 | }
80 |
81 | class Rain {
82 | threehour = null;
83 |
84 | constructor(obj) {
85 | if (obj != null) {
86 | if (obj.hasOwnProperty('3h')) { this.threehour = obj['3h']; } // converted '3h' to 'threehour', a valid JS variable name
87 | }
88 | }
89 | }
90 |
91 | class Snow {
92 | threehour = null;
93 |
94 | constructor(obj) {
95 | if (obj != null) {
96 | if (obj.hasOwnProperty('3h')) { this.threehour = obj['3h']; } // converted '3h' to 'threehour', a valid JS variable name
97 | }
98 | }
99 | }
100 |
101 | class Sys {
102 | type = null;
103 | id = null;
104 | message = null;
105 | country = null;
106 | sunrise = null;
107 | sunset = null;
108 |
109 | constructor(obj) {
110 | if (obj != null) {
111 | if (obj.hasOwnProperty('type')) { this.type = obj['type']; }
112 | if (obj.hasOwnProperty('id')) { this.id = obj['id']; }
113 | if (obj.hasOwnProperty('message')) { this.message = obj['message']; }
114 | if (obj.hasOwnProperty('country')) { this.country = obj['country']; }
115 | if (obj.hasOwnProperty('sunrise')) { this.sunrise = obj['sunrise'] * 1000; } // convert to ms to be compatible with Date lib
116 | if (obj.hasOwnProperty('sunset')) { this.sunset = obj['sunset'] * 1000; } // convert to ms to be compatible with Date lib
117 | }
118 | }
119 | }
120 |
121 | export class OWMWeather {
122 | code = null;
123 | name = null;
124 | id = null;
125 | dt = null;
126 | sys: Sys = null;
127 | rain: Rain = null;
128 | snow: Snow = null;
129 | clouds: Cloud = null;
130 | wind: Wind = null;
131 | main: Main = null;
132 | base = null;
133 | weather: Weather[] = null;
134 | coord: Coord = null;
135 |
136 | constructor(obj) {
137 | if (obj.hasOwnProperty('cod')) { this.code = obj['cod']; }
138 | if (obj.hasOwnProperty('name')) { this.name = obj['name']; }
139 | if (obj.hasOwnProperty('id')) { this.id = obj['id']; }
140 | if (obj.hasOwnProperty('dt')) { this.dt = obj['dt'] * 1000; } // convert to ms to be compatible with Date lib
141 | if (obj.hasOwnProperty('base')) { this.base = obj['base']; }
142 |
143 | if (obj.hasOwnProperty('sys')) { this.sys = new Sys(obj['sys']); }
144 | if (obj.hasOwnProperty('rain')) { this.rain = new Rain(obj['rain']); }
145 | if (obj.hasOwnProperty('snow')) { this.snow = new Snow(obj['snow']); }
146 | if (obj.hasOwnProperty('clouds')) { this.clouds = new Cloud(obj['clouds']); }
147 | if (obj.hasOwnProperty('wind')) { this.wind = new Wind(obj['wind']); }
148 | if (obj.hasOwnProperty('main')) { this.main = new Main(obj['main']); }
149 | if (obj.hasOwnProperty('coord')) { this.coord = new Coord(obj['coord']); }
150 |
151 | if (obj.hasOwnProperty('weather')) {
152 | this.weather = [];
153 | for (const idx in obj['weather']) {
154 | if (!idx) { continue; }
155 | const elem = obj['weather'][idx];
156 | const report = new Weather(elem);
157 | this.weather.push(report);
158 | }
159 | }
160 | }
161 | }
162 |
163 | class City {
164 | id = null;
165 | name = null;
166 | coord: Coord = null;
167 | country = null;
168 |
169 | constructor(obj) {
170 | if (obj != null) {
171 | if (obj.hasOwnProperty('id')) { this.id = obj['id']; }
172 | if (obj.hasOwnProperty('name')) { this.name = obj['name']; }
173 | if (obj.hasOwnProperty('country')) { this.country = obj['country']; }
174 |
175 | if (obj.hasOwnProperty('coord')) { this.coord = new Coord(obj['coord']); }
176 | }
177 | }
178 | }
179 |
180 | class Forecast {
181 | dt = null;
182 | main: Main = null;
183 | weather: Weather[] = null;
184 | clouds: Cloud = null;
185 | wind: Wind = null;
186 | rain: Rain = null;
187 | snow: Snow = null;
188 | dt_txt = null;
189 |
190 | constructor(obj) {
191 | if (obj != null) {
192 | if (obj.hasOwnProperty('dt')) { this.dt = obj['dt'] * 1000; } // convert to ms to be compatible with Date lib
193 | if (obj.hasOwnProperty('dt_txt')) { this.dt_txt = obj['dt_txt']; }
194 |
195 | if (obj.hasOwnProperty('main')) { this.main = new Main(obj['main']); }
196 | if (obj.hasOwnProperty('clouds')) { this.clouds = new Cloud(obj['clouds']); }
197 | if (obj.hasOwnProperty('wind')) { this.wind = new Wind(obj['wind']); }
198 | if (obj.hasOwnProperty('rain')) { this.rain = new Rain(obj['rain']); }
199 | if (obj.hasOwnProperty('snow')) { this.snow = new Snow(obj['snow']); }
200 |
201 | if (obj.hasOwnProperty('weather')) {
202 | this.weather = [];
203 | for (const idx in obj['weather']) {
204 | if (!idx) { continue; }
205 | const elem = obj['weather'][idx];
206 | const report = new Weather(elem);
207 | this.weather.push(report);
208 | }
209 | }
210 | }
211 | }
212 | }
213 |
214 | export class OWMForecast {
215 | code = null;
216 | message = null;
217 | city: City = null;
218 | cnt = null;
219 | list: Forecast[] = null;
220 |
221 | constructor(obj) {
222 | if (obj.hasOwnProperty('cod')) { this.code = obj['cod']; }
223 | if (obj.hasOwnProperty('message')) { this.message = obj['message']; }
224 | if (obj.hasOwnProperty('cnt')) { this.cnt = obj['cnt']; }
225 |
226 | if (obj.hasOwnProperty('city')) { this.city = new City(obj['city']); }
227 |
228 | if (obj.hasOwnProperty('list')) {
229 | this.list = [];
230 | for (const idx in obj['list']) {
231 | if (!idx) { continue; }
232 | const elem = obj['list'][idx];
233 | const forecast = new Forecast(elem);
234 | this.list.push(forecast);
235 | }
236 | }
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/client/src/app/weatherdash/weatherdash.component.css:
--------------------------------------------------------------------------------
1 | .digital {
2 | font-family: 'digital7', sans-serif;
3 | }
4 |
5 | .no-select {
6 | -webkit-touch-callout: none;
7 | -webkit-user-select: none;
8 | -khtml-user-select: none;
9 | -moz-user-select: none;
10 | -ms-user-select: none;
11 | user-select: none;
12 | }
13 |
14 | .no-select:active {
15 | cursor: move;
16 | cursor: -moz-grabbing;
17 | cursor: -webkit-grabbing;
18 | }
19 |
20 | ::-webkit-scrollbar {
21 | display: none;
22 | }
23 |
24 | .label {
25 | font-weight: bold;
26 | padding: 2px 0px;
27 | border-bottom: 1px solid white;
28 | }
29 |
30 | .dash {
31 | width: 100%;
32 | height: 100%;
33 | box-sizing: border-box;
34 | padding: 1px;
35 | }
36 |
37 | .dash-grid {
38 | display: grid;
39 | height: 100%;
40 | width: 100%;
41 | overflow: auto;
42 | grid-template-columns: 1fr 1fr;
43 | grid-template-rows: 1fr 2fr 2fr;
44 | grid-template-areas: "clock clock" "indoor outdoor" "forecast forecast";
45 | grid-gap: 10px 5px;
46 |
47 | font-size: 5vw;
48 | }
49 |
50 | .dash-grid > * {
51 | border: 2px solid white;
52 | border-radius: 10px;
53 | }
54 |
55 | .clock-grid {
56 | grid-area: clock;
57 | display: grid;
58 | height: 100%;
59 | grid-template-rows: 1fr;
60 | grid-template-columns: 25% 50% 25%;
61 | grid-template-areas: "day time date";
62 | font-size: 150%;
63 | }
64 |
65 | .day {
66 | grid-area: day;
67 | display: flex;
68 | align-items: center;
69 | justify-content: center;
70 | }
71 |
72 | .time {
73 | grid-area: time;
74 | cursor: pointer;
75 | display: flex;
76 | align-items: center;
77 | justify-content: center;
78 | font-size: 150%;
79 | }
80 |
81 | .date {
82 | grid-area: date;
83 | display: flex;
84 | align-items: center;
85 | justify-content: center;
86 | }
87 |
88 | .indoor {
89 | grid-area: indoor;
90 | height: 100%;
91 | max-height: 100%;
92 | overflow: hidden;
93 | display: grid;
94 | grid-template-rows: 1fr 3fr;
95 | grid-template-columns: 1fr 6fr 3fr;
96 | grid-template-areas: "unit location environment" "unit temp environment"
97 | }
98 |
99 | .thermSelect {
100 | grid-area: indoor;
101 | height: 100%;
102 | max-height: 100%;
103 | font-size: 50%;
104 | display: flex;
105 | flex-direction: column;
106 | }
107 |
108 | .thermSelect > .label {
109 | padding: 10px;
110 | display: flex;
111 | justify-content: center;
112 | }
113 |
114 | .thermSelect > .options {
115 | overflow-y: auto;
116 | }
117 |
118 | .option{
119 | border-bottom: 1px solid white;
120 | padding: 10px;
121 | }
122 |
123 | .option a:hover {
124 | cursor: pointer;
125 | }
126 |
127 | .unit {
128 | grid-area: unit;
129 | display: flex;
130 | flex-direction: column;
131 | justify-content: space-between;
132 | }
133 |
134 | .tempsymbol {
135 | cursor: pointer;
136 | }
137 |
138 | .errdot {
139 | height: 20px;
140 | width: 20px;
141 | background-color: #FF0000DD;
142 | border-radius: 50%;
143 | border: 2px solid white;
144 | margin: 12px;
145 | display: inline-block;
146 | cursor: pointer;
147 | }
148 |
149 | .location {
150 | grid-area: location;
151 | margin: 0px 10px;
152 | border-bottom: 1px solid white;
153 | border-left: 1px solid white;
154 | border-right: 1px solid white;
155 | border-radius: 0px 0px 10px 10px;
156 | -moz-border-radius: 0px 0px 10px 10px;
157 | display: flex;
158 | align-items: center;
159 | justify-content: center;
160 | cursor: pointer;
161 | }
162 |
163 | .temp {
164 | grid-area: temp;
165 | height: 100%;
166 | display: flex;
167 | align-items: center;
168 | justify-content: center;
169 | flex-direction: column;
170 | font-size: 325%
171 | }
172 |
173 | .environment {
174 | grid-area: environment;
175 | display: flex;
176 | align-items: center;
177 | justify-content: space-between;
178 | flex-direction: column;
179 | padding: 10px 5px;
180 | font-size: 75%;
181 | }
182 |
183 | .environment > * {
184 | display: flex;
185 | align-items: center;
186 | justify-content: center;
187 | flex-direction: column;
188 | font-size: 125%;
189 | }
190 |
191 | .outdoor {
192 | grid-area: outdoor;
193 | height: 100%;
194 | max-height: 100%;
195 | overflow: hidden;
196 | display: grid;
197 | grid-template-rows: 1fr 3fr;
198 | grid-template-columns: 1fr 6fr 3fr;
199 | grid-template-areas: "unit location environment" "unit temp environment"
200 | }
201 |
202 | .forecast {
203 | grid-area: forecast;
204 | display: flex;
205 | align-items: center;
206 | overflow-x: auto;
207 | overflow-y: hidden;
208 | }
209 |
210 | .forecast > * {
211 | margin-top: 5px;
212 | padding: 5px;
213 | min-width: 18%;
214 | min-height: 85%;
215 | display: flex;
216 | flex-direction: column;
217 | justify-content: space-between;
218 | align-items: center;
219 | }
220 |
221 | .forecast > *:not(:last-child) {
222 | border-right: 1px solid white;
223 | }
224 |
225 | .report-day {
226 | font-size: 50%;
227 | }
228 | .report-time {
229 | font-size: 50%;
230 | }
231 | .report-status {
232 | display: flex;
233 | flex-direction: column;
234 | align-items: center;
235 | justify-content: center;
236 | }
237 | .report-status-temp {
238 | padding-bottom: 5px;
239 | }
240 | .report-status-icon {
241 | }
242 | .report-status-desc {
243 | font-size: 25%;
244 | }
245 |
--------------------------------------------------------------------------------
/client/src/app/weatherdash/weatherdash.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ clock | date:'EEE' | uppercase}}
7 |
8 |
9 | {{ clock | date:'(#W)' }}
10 |
11 |
12 |
13 |
14 | {{ clock | date:'hh:mm'}}
15 |
16 |
17 | {{ clock | date:'a'}}
18 |
19 |
20 |
21 |
22 | {{ clock | date:'MM/dd'}}
23 |
24 |
25 |
1 && !thermostatId; then selectThermostat; else displayThermostat">
26 |
27 |
28 |
29 |
30 | Select Thermostat:
31 |
32 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {{ settings.getTempSymbol() }}
47 |
48 |
49 |
50 |
INSIDE
51 |
52 |
53 | {{ (indoor?.thermostatList[getThermostatIdx()]?.runtime?.actualTemperature / 10 | number:'2.0-0') || '??' }}.{{ (indoor?.thermostatList[getThermostatIdx()]?.runtime?.actualTemperature % 10 | number:'1.0-0') || '?' }}
54 |
55 |
56 |
57 |
58 |
HUMIDITY
59 |
60 | {{ indoor?.thermostatList[getThermostatIdx()]?.runtime?.actualHumidity || '??' }}%
61 |
62 |
63 |
64 |
TARGET
65 |
66 | {{ indoor?.thermostatList[getThermostatIdx()]?.runtime?.desiredHeat / 10 | number:'2.0-0' || '??' }}-{{ indoor?.thermostatList[getThermostatIdx()]?.runtime?.desiredCool / 10 | number:'2.0-0' || '??' }}
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {{ settings.getTempSymbol() }}
77 |
78 |
79 |
80 |
{{ weather?.name || 'OUTSIDE'}}
81 |
82 |
83 | {{ (weather?.main?.temp - (weather?.main?.temp % 1) | number:'2.0-0') || '??' }}.{{ (weather?.main?.temp % 1 * 10 | number:'1.0-0') || '?' }}
84 |
85 |
86 |
87 |
88 |
HUMIDITY
89 |
90 | {{ weather?.main?.humidity || '??' }}%
91 |
92 |
93 |
94 |
WIND
95 |
96 | {{ (weather?.wind?.speed | number:'1.0-0' ) || '??' }}{{ settings.getSpeedSymbol() || '??'}}
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
Today
105 |
Tomorrow
106 |
{{ report.dt | date:'EEEE' }}
107 |
108 |
{{ report?.main?.temp | number:'2.0-0' }}
109 |
110 |
111 |
112 |
{{ report?.weather[0]?.description }}
113 |
114 |
115 | {{ report.dt | date:'hh:mm a' }}
116 |
117 |
118 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/client/src/app/weatherdash/weatherdash.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { WeatherdashComponent } from './weatherdash.component';
4 |
5 | describe('WeatherdashComponent', () => {
6 | let component: WeatherdashComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ WeatherdashComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(WeatherdashComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/client/src/app/weatherdash/weatherdash.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { interval } from 'rxjs';
3 |
4 | import { APIService } from '../api.service';
5 | import { OWMWeather, OWMForecast, formOWMIconURL } from './owm.types';
6 | import { EcobeeIndoor } from './ecobee.types';
7 | import { Settings } from './weatherdash.config';
8 | import { TouchScroll } from '../touchscroll.module';
9 |
10 | const HTTP_STATUS_OK = 200;
11 |
12 | @Component({
13 | selector: 'app-weatherdash',
14 | templateUrl: './weatherdash.component.html',
15 | styleUrls: ['./weatherdash.component.css']
16 | })
17 | export class WeatherdashComponent implements OnInit {
18 | settings = null;
19 | clockTimer;
20 | clock = null ;
21 | weatherTimer;
22 | weather = null;
23 | weatherErr = true;
24 | forecastTimer;
25 | forecast = null;
26 | indoorTimer;
27 | indoor = null;
28 | indoorErr = true;
29 | thermostatId = null;
30 | idx = null;
31 | forecastScroll = null;
32 |
33 | constructor(private apiService: APIService) {
34 | this.clockTimer = interval(300);
35 | this.weatherTimer = interval(1000 * 30);
36 | this.forecastTimer = interval(1000 * 60 * 1);
37 | this.indoorTimer = interval(1000 * 30);
38 | }
39 |
40 | ngOnInit() {
41 | const self = this;
42 | this.settings = new Settings();
43 | this.forecastScroll = new TouchScroll();
44 | this.forecastScroll.init({
45 | id: 'forecast',
46 | draggable: true,
47 | wait: false
48 | });
49 |
50 | this.clockTimer.subscribe(() => {
51 | this.clock = Date.now();
52 | });
53 |
54 | this.updateSettings(this.settings.ZIP, this.settings.UNIT, true);
55 |
56 | this.weatherTimer.subscribe(() => {
57 | self.updateWeather();
58 | });
59 | this.forecastTimer.subscribe(() => {
60 | self.updateForecast();
61 | });
62 | this.indoorTimer.subscribe(() => {
63 | self.updateIndoor();
64 | });
65 | }
66 |
67 | updateSettings(zip, unit, refresh = true) {
68 | const self = this;
69 | if (refresh) {
70 | self.weather = self.forecast = self.indoor = null;
71 | }
72 | return new Promise(resolve => {
73 | self.apiService.configureSettings(zip, unit).subscribe(response => {
74 | resolve(Number(response.status) === HTTP_STATUS_OK);
75 | });
76 | }).then(success => {
77 | if (success && refresh) {
78 | self.updateWeather();
79 | self.updateForecast();
80 | self.updateIndoor();
81 | } else {
82 | window.alert('Error configuring settings. Refresh recommended.');
83 | }
84 | });
85 | }
86 |
87 | updateWeather() {
88 | this.apiService.getWeather().subscribe((data) => {
89 | const weather = new OWMWeather(data);
90 | console.log('Weather:', weather);
91 | if (Number(weather.code) === HTTP_STATUS_OK) {
92 | this.weatherErr = false;
93 | this.weather = weather;
94 | } else {
95 | this.weatherErr = true;
96 | }
97 | });
98 | }
99 |
100 | updateForecast() {
101 | this.apiService.getForecast().subscribe((data) => {
102 | const forecast = new OWMForecast(data);
103 | console.log('Forecast:', forecast);
104 | if (Number(forecast.code) === HTTP_STATUS_OK) {
105 | this.forecast = forecast;
106 | }
107 | });
108 | }
109 |
110 | updateIndoor() {
111 | this.apiService.getIndoor().subscribe((data) => {
112 | const indoor = new EcobeeIndoor(data);
113 | console.log('Indoor:', indoor);
114 | if (Number(indoor.status.code) === 0) {
115 | this.indoorErr = false;
116 | this.indoor = indoor;
117 | } else {
118 | this.indoorErr = true;
119 | }
120 | });
121 | }
122 |
123 | daysAway(ts) {
124 | const msInDay = 1000 * 60 * 60 * 24;
125 | const today = new Date();
126 | today.setHours(0, 0, 0, 0);
127 | const todayTs = today.getTime();
128 | const delta = ts - todayTs;
129 | const days = delta / msInDay;
130 | return Math.floor(days);
131 | }
132 |
133 | dayOrNight(ts) {
134 | const hour = (new Date(ts)).getHours();
135 | if (hour >= 6 && hour < 18) { return 'd'; } else { return 'n'; }
136 | }
137 |
138 | formIconUrl(icon) {
139 | return formOWMIconURL(icon);
140 | }
141 |
142 | getThermostatIdx() {
143 | const self = this;
144 | if (this.thermostatId == null) {
145 | return 0;
146 | }
147 | const index = this.indoor.thermostatList.findIndex(function(thermostat) {
148 | return thermostat.identifier === self.thermostatId;
149 | });
150 | if (index === -1) {
151 | this.thermostatId = null;
152 | return 0;
153 | }
154 | return index;
155 | }
156 |
157 | setThermostatId(id) {
158 | this.thermostatId = id;
159 | }
160 |
161 | promptZip() {
162 | const self = this;
163 | const zip = window.prompt('Please enter a 5 digit ZIP code:');
164 | if (zip != null) {
165 | if (/(^\d{5}$)/.test(zip)) {
166 | this.updateSettings(zip, null).then( () => {
167 | self.settings.setZip(zip);
168 | });
169 | } else {
170 | window.alert('Invalid 5 digit ZIP!');
171 | }
172 | }
173 | }
174 |
175 | toggleUnit() {
176 | const self = this;
177 | const unit = this.settings.nextUnit();
178 | self.settings.setUnit(unit);
179 | this.updateSettings(null, unit);
180 | }
181 |
182 | refreshPage() {
183 | window.location.reload();
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/client/src/app/weatherdash/weatherdash.config.ts:
--------------------------------------------------------------------------------
1 |
2 | const UNIT_IMPERIAL = 'imperial';
3 | const UNIT_METRIC = 'metric';
4 | const UNIT_LIST = [UNIT_IMPERIAL, UNIT_METRIC];
5 | const TEMP_UNITS = {[UNIT_IMPERIAL]: '°F', [UNIT_METRIC]: '°C'};
6 | const SPEED_UNITS = {[UNIT_IMPERIAL]: 'mph', [UNIT_METRIC]: 'm/s'};
7 |
8 |
9 | // Change these to desired values
10 | const defaultZip = '60613';
11 | const defaultUnit = UNIT_IMPERIAL;
12 |
13 |
14 | export class Settings {
15 | UNIT_KEY = 'WEATHERDASH_UNIT';
16 | ZIP_KEY = 'WEATHERDASH_ZIP';
17 |
18 | UNIT = defaultUnit;
19 | ZIP = defaultZip;
20 |
21 | constructor() {
22 | if (typeof(Storage) !== 'undefined') {
23 | const unit = localStorage.getItem(this.UNIT_KEY);
24 | if (unit != null) {
25 | this.UNIT = unit;
26 | }
27 | const zip = localStorage.getItem(this.ZIP_KEY);
28 | if (zip != null) {
29 | this.ZIP = zip;
30 | }
31 | }
32 | }
33 |
34 | getTempSymbol() {
35 | return TEMP_UNITS[this.UNIT];
36 | }
37 |
38 | getSpeedSymbol() {
39 | return SPEED_UNITS[this.UNIT];
40 | }
41 |
42 | nextUnit() {
43 | const idx = UNIT_LIST.indexOf(this.UNIT);
44 | return UNIT_LIST[ (idx + 1) % UNIT_LIST.length ];
45 | }
46 |
47 | setUnit(unit) {
48 | if (typeof(Storage) !== 'undefined') {
49 | localStorage.setItem(this.UNIT_KEY, unit);
50 | }
51 | this.UNIT = unit;
52 |
53 | }
54 |
55 | setZip(zip) {
56 | if (typeof(Storage) !== 'undefined') {
57 | localStorage.setItem(this.ZIP_KEY, zip);
58 | }
59 | this.ZIP = zip;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/client/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/assets/.gitkeep
--------------------------------------------------------------------------------
/client/src/assets/fonts/README.md:
--------------------------------------------------------------------------------
1 | # OpenWeatherMap Font
2 |
3 | ## About
4 | OpenWeatherMap Font is designed to match to response codes from OpenWeatherMap API. CSS rules are based on Font-Awesome font, symbols are created by Deniz Fuchidzhiev (websygen.com).
5 |
6 | ## Demo
7 | http://websygen.github.io/owfont/
8 |
9 | ## License
10 | * owfont licensed under SIL OFL 1.1
11 | * Code licensed under MIT License
12 | * Documentation licensed under CC BY 3.0
13 |
14 |
--------------------------------------------------------------------------------
/client/src/assets/fonts/digital-7 (italic).ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/assets/fonts/digital-7 (italic).ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/digital-7 (mono italic).ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/assets/fonts/digital-7 (mono italic).ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/digital-7 (mono).ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/assets/fonts/digital-7 (mono).ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/digital-7.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/assets/fonts/digital-7.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/owfont-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/assets/fonts/owfont-regular.eot
--------------------------------------------------------------------------------
/client/src/assets/fonts/owfont-regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/assets/fonts/owfont-regular.otf
--------------------------------------------------------------------------------
/client/src/assets/fonts/owfont-regular.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
395 |
--------------------------------------------------------------------------------
/client/src/assets/fonts/owfont-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/assets/fonts/owfont-regular.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/owfont-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/assets/fonts/owfont-regular.woff
--------------------------------------------------------------------------------
/client/src/assets/fonts/readme.txt:
--------------------------------------------------------------------------------
1 | True Type Fonts: DIGITAL-7 version 1.02
2 |
3 |
4 | EULA
5 | -==-
6 | The fonts Digital-7 is freeware for home using.
7 |
8 |
9 | DESCRIPTION
10 | -=========-
11 |
12 | This font created specially for program Calculator-7 (download shareware version: http://www.styleseven.com/ and use 7 days fo free).
13 |
14 | The program Calculator-7 offers you the following possibilities:
15 | * calculate using seven operator: addition, subtraction, multiply, divide, percent, square root, 1 divide to X;
16 | * set decimal position (0, 2, 3, float) and round type (up, mathematical, down);
17 | * customize an appearance of work window: scale, fonts for digital panel and buttons, background color;
18 | * customize an appearance of number in digital panel: leading zero for decimal, thousand separator, decimal separator, digit grouping;
19 | * calculate total from clipboard (copy data to clipboard from table or text and press one button).
20 |
21 |
22 | Files in digital-7_font.zip:
23 | readme.txt this file;
24 | digital-7.ttf digital-7 regular font;
25 | digital-7 (italic).ttf digital-7 italic font;
26 | digital-7 (mono).ttf digital-7 mono font;
27 | digital-7 (mono italic).ttf digital-7 mono font.
28 |
29 | Please visit http://www.styleseven.com/ for download our other products as freeware as shareware.
30 | We will welcome any useful suggestions and comments; please send them to ms-7@styleseven.com
31 |
32 |
33 | FREEWARE USE (NOTES)
34 | -=================-
35 | Also you may:
36 | * Use the font in freeware software (credit needed);
37 | * Use the font for your education process.
38 |
39 |
40 | COMMERCIAL OR BUSINESS USE
41 | -========================-
42 |
43 | You can buy font for commercial use here ($24.95): http://store.esellerate.net/s.aspx?s=STR0331655240
44 | You may:
45 | * Include the font to your installation;
46 | * Use one license up to 100 computers in your office.
47 | Please contact us for any questions.
48 |
49 |
50 | WHAT IS NEW?
51 | -==========-
52 |
53 | Version 1.01 April 05 2009
54 | --------------------------
55 | * Change Typeface name for fonts "Digital-7 (mono)" and "Digital-7 (italic)" (now available all fonts for select in application, for example Word Pad).
56 | * Corrected symbol ':'.
57 |
58 | Version 1.01 April 07 2011
59 | --------------------------
60 | * Embedding is allowed.
61 |
62 | Version 1.1 June 07 2013
63 | --------------------------
64 | * Mono Italic font is added.
65 |
66 |
67 | AUTHOR
68 | -====-
69 |
70 | Sizenko Alexander
71 | Style-7
72 | http://www.styleseven.com
73 | Created: October 7 2008
--------------------------------------------------------------------------------
/client/src/assets/styles/owfont-regular.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * owfont-regular 1.0.0 by Deniz Fuchidzhiev - http://websygen.com
3 | * License - font: SIL OFL 1.1, css: MIT License
4 | */
5 | /* FONT PATH
6 | * -------------------------- */
7 | @font-face{font-family:owfont;src:url(../fonts/owfont-regular.eot?v=1.0.0);src:url(../fonts/owfont-regular.eot?#iefix&v=1.0.0) format('embedded-opentype'),url(../fonts/owfont-regular.woff) format('woff'),url(../fonts/owfont-regular.ttf) format('truetype'),url(../fonts/owfont-regular.svg#owf-regular) format('svg');font-weight:400;font-style:normal}.owf{display:inline-block;font:normal normal normal 14px/1 owfont;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transform:translate(0,0)}.owf-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.owf-2x{font-size:2em}.owf-3x{font-size:3em}.owf-4x{font-size:4em}.owf-5x{font-size:5em}.owf-fw{width:1.28571429em;text-align:center}.owf-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.owf-ul>li{position:relative}.owf-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.owf-li.owf-lg{left:-1.85714286em}.owf-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.owf-pull-right{float:right}.owf-pull-left{float:left}.owf.owf-pull-left{margin-right:.3em}.owf.owf-pull-right{margin-left:.3em}.owf-200-d:before,.owf-200-n:before,.owf-200:before{content:"\EB28"}.owf-201-d:before,.owf-201-n:before,.owf-201:before{content:"\EB29"}.owf-202-d:before,.owf-202-n:before,.owf-202:before{content:"\EB2A"}.owf-210-d:before,.owf-210-n:before,.owf-210:before{content:"\EB32"}.owf-211-d:before,.owf-211-n:before,.owf-211:before{content:"\EB33"}.owf-212-d:before,.owf-212-n:before,.owf-212:before{content:"\EB34"}.owf-221-d:before,.owf-221-n:before,.owf-221:before{content:"\EB3D"}.owf-230-d:before,.owf-230-n:before,.owf-230:before{content:"\EB46"}.owf-231-d:before,.owf-231-n:before,.owf-231:before{content:"\EB47"}.owf-232-d:before,.owf-232-n:before,.owf-232:before{content:"\EB48"}.owf-300-d:before,.owf-300-n:before,.owf-300:before{content:"\EB8C"}.owf-301-d:before,.owf-301-n:before,.owf-301:before{content:"\EB8D"}.owf-302-d:before,.owf-302-n:before,.owf-302:before{content:"\EB8E"}.owf-310-d:before,.owf-310-n:before,.owf-310:before{content:"\EB96"}.owf-311-d:before,.owf-311-n:before,.owf-311:before{content:"\EB97"}.owf-312-d:before,.owf-312-n:before,.owf-312:before{content:"\EB98"}.owf-313-d:before,.owf-313-n:before,.owf-313:before{content:"\EB99"}.owf-314-d:before,.owf-314-n:before,.owf-314:before{content:"\EB9A"}.owf-321-d:before,.owf-321-n:before,.owf-321:before{content:"\EBA1"}.owf-500-d:before,.owf-500-n:before,.owf-500:before{content:"\EC54"}.owf-501-d:before,.owf-501-n:before,.owf-501:before{content:"\EC55"}.owf-502-d:before,.owf-502-n:before,.owf-502:before{content:"\EC56"}.owf-503-d:before,.owf-503-n:before,.owf-503:before{content:"\EC57"}.owf-504-d:before,.owf-504-n:before,.owf-504:before{content:"\EC58"}.owf-511-d:before,.owf-511-n:before,.owf-511:before{content:"\EC5F"}.owf-520-d:before,.owf-520-n:before,.owf-520:before{content:"\EC68"}.owf-521-d:before,.owf-521-n:before,.owf-521:before{content:"\EC69"}.owf-522-d:before,.owf-522-n:before,.owf-522:before{content:"\EC6A"}.owf-531-d:before,.owf-531-n:before,.owf-531:before{content:"\EC73"}.owf-600-d:before,.owf-600-n:before,.owf-600:before{content:"\ECB8"}.owf-601-d:before,.owf-601-n:before,.owf-601:before{content:"\ECB9"}.owf-602-d:before,.owf-602-n:before,.owf-602:before{content:"\ECBA"}.owf-611-d:before,.owf-611-n:before,.owf-611:before{content:"\ECC3"}.owf-612-d:before,.owf-612-n:before,.owf-612:before{content:"\ECC4"}.owf-615-d:before,.owf-615-n:before,.owf-615:before{content:"\ECC7"}.owf-616-d:before,.owf-616-n:before,.owf-616:before{content:"\ECC8"}.owf-620-d:before,.owf-620-n:before,.owf-620:before{content:"\ECCC"}.owf-621-d:before,.owf-621-n:before,.owf-621:before{content:"\ECCD"}.owf-622-d:before,.owf-622-n:before,.owf-622:before{content:"\ECCE"}.owf-701-d:before,.owf-701-n:before,.owf-701:before{content:"\ED1D"}.owf-711-d:before,.owf-711-n:before,.owf-711:before{content:"\ED27"}.owf-721-d:before,.owf-721-n:before,.owf-721:before{content:"\ED31"}.owf-731-d:before,.owf-731-n:before,.owf-731:before{content:"\ED3B"}.owf-741-d:before,.owf-741-n:before,.owf-741:before{content:"\ED45"}.owf-751-d:before,.owf-751-n:before,.owf-751:before{content:"\ED4F"}.owf-761-d:before,.owf-761-n:before,.owf-761:before{content:"\ED59"}.owf-762-d:before,.owf-762-n:before,.owf-762:before{content:"\ED5A"}.owf-771-d:before,.owf-771-n:before,.owf-771:before{content:"\ED63"}.owf-781-d:before,.owf-781-n:before,.owf-781:before{content:"\ED6D"}.owf-800-d:before,.owf-800:before,.owf-951-d:before,.owf-951:before{content:"\ED80"}.owf-800-n:before,.owf-951-n:before{content:"\F168"}.owf-801-d:before,.owf-801:before{content:"\ED81"}.owf-801-n:before{content:"\F169"}.owf-802-d:before,.owf-802:before{content:"\ED82"}.owf-802-n:before{content:"\F16A"}.owf-803-d:before,.owf-803-n:before,.owf-803:before{content:"\ED83"}.owf-804-d:before,.owf-804-n:before,.owf-804:before{content:"\ED84"}.owf-900-d:before,.owf-900-n:before,.owf-900:before{content:"\EDE4"}.owf-901-d:before,.owf-901-n:before,.owf-901:before{content:"\EDE5"}.owf-902-d:before,.owf-902-n:before,.owf-902:before{content:"\EDE6"}.owf-903-d:before,.owf-903-n:before,.owf-903:before{content:"\EDE7"}.owf-904-d:before,.owf-904-n:before,.owf-904:before{content:"\EDE8"}.owf-905-d:before,.owf-905-n:before,.owf-905:before{content:"\EDE9"}.owf-906-d:before,.owf-906-n:before,.owf-906:before{content:"\EDEA"}.owf-950-d:before,.owf-950-n:before,.owf-950:before{content:"\EE16"}.owf-952-d:before,.owf-952-n:before,.owf-952:before{content:"\EE18"}.owf-953-d:before,.owf-953-n:before,.owf-953:before{content:"\EE19"}.owf-954-d:before,.owf-954-n:before,.owf-954:before{content:"\EE1A"}.owf-955-d:before,.owf-955-n:before,.owf-955:before{content:"\EE1B"}.owf-956-d:before,.owf-956-n:before,.owf-956:before{content:"\EE1C"}.owf-957-d:before,.owf-957-n:before,.owf-957:before{content:"\EE1D"}.owf-958-d:before,.owf-958-n:before,.owf-958:before{content:"\EE1E"}.owf-959-d:before,.owf-959-n:before,.owf-959:before{content:"\EE1F"}.owf-960-d:before,.owf-960-n:before,.owf-960:before{content:"\EE20"}.owf-961-d:before,.owf-961-n:before,.owf-961:before{content:"\EE21"}.owf-962-d:before,.owf-962-n:before,.owf-962:before{content:"\EE22"}
--------------------------------------------------------------------------------
/client/src/browserslist:
--------------------------------------------------------------------------------
1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed
5 | > 0.5%
6 | last 2 versions
7 | Firefox ESR
8 | not dead
9 | # IE 9-11
--------------------------------------------------------------------------------
/client/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/client/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * In development mode, to ignore zone related error stack frames such as
11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can
12 | * import the following file, but please comment it out in production mode
13 | * because it will have performance impact when throw error
14 | */
15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
16 |
--------------------------------------------------------------------------------
/client/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/client/src/favicon.ico
--------------------------------------------------------------------------------
/client/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WeatherDash
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/client/src/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, '../coverage'),
20 | reports: ['html', 'lcovonly'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false
30 | });
31 | };
--------------------------------------------------------------------------------
/client/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.log(err));
13 |
--------------------------------------------------------------------------------
/client/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** IE10 and IE11 requires the following for the Reflect API. */
41 | // import 'core-js/es6/reflect';
42 |
43 |
44 | /** Evergreen browsers require these. **/
45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
46 | import 'core-js/es7/reflect';
47 |
48 |
49 | /**
50 | * Web Animations `@angular/platform-browser/animations`
51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
53 | **/
54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
55 |
56 | /**
57 | * By default, zone.js will patch all possible macroTask and DomEvents
58 | * user can disable parts of macroTask/DomEvents patch by setting following flags
59 | */
60 |
61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
64 |
65 | /*
66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
68 | */
69 | // (window as any).__Zone_enable_cross_context_check = true;
70 |
71 | /***************************************************************************************************
72 | * Zone JS is required by default for Angular itself.
73 | */
74 | import 'zone.js/dist/zone'; // Included with Angular CLI.
75 |
76 |
77 |
78 | /***************************************************************************************************
79 | * APPLICATION IMPORTS
80 | */
81 |
--------------------------------------------------------------------------------
/client/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | @font-face{
3 | font-family:'digital7';
4 | src: url('assets/fonts/digital-7.ttf');
5 | }
6 |
7 | * {
8 | margin: 0;
9 | padding: 0;
10 | color: white;
11 | }
12 |
13 | body {
14 | background-color: black;
15 | height: 100vh;
16 | width: 100vw;
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | );
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/);
19 | // And load the modules.
20 | context.keys().map(context);
21 |
--------------------------------------------------------------------------------
/client/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "types": []
6 | },
7 | "exclude": [
8 | "src/test.ts",
9 | "**/*.spec.ts"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "test.ts",
12 | "polyfills.ts"
13 | ],
14 | "exclude": [
15 | "**/*"
16 | ],
17 | "include": [
18 | "**/*.spec.ts",
19 | "**/*.d.ts"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tslint.json",
3 | "rules": {
4 | "directive-selector": [
5 | true,
6 | "attribute",
7 | "app",
8 | "camelCase"
9 | ],
10 | "component-selector": [
11 | true,
12 | "element",
13 | "app",
14 | "kebab-case"
15 | ]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "module": "es2015",
9 | "moduleResolution": "node",
10 | "emitDecoratorMetadata": true,
11 | "experimentalDecorators": true,
12 | "target": "es5",
13 | "typeRoots": [
14 | "node_modules/@types"
15 | ],
16 | "lib": [
17 | "es2017",
18 | "dom"
19 | ]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs/Rx"
22 | ],
23 | "import-spacing": true,
24 | "indent": [
25 | true,
26 | "spaces"
27 | ],
28 | "interface-over-type-literal": true,
29 | "label-position": true,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-arg": true,
47 | "no-bitwise": true,
48 | "no-console": [
49 | true,
50 | "debug",
51 | "info",
52 | "time",
53 | "timeEnd",
54 | "trace"
55 | ],
56 | "no-construct": true,
57 | "no-debugger": true,
58 | "no-duplicate-super": true,
59 | "no-empty": false,
60 | "no-empty-interface": true,
61 | "no-eval": true,
62 | "no-inferrable-types": [
63 | true,
64 | "ignore-params"
65 | ],
66 | "no-misused-new": true,
67 | "no-non-null-assertion": true,
68 | "no-shadowed-variable": true,
69 | "no-string-literal": false,
70 | "no-string-throw": true,
71 | "no-switch-case-fall-through": true,
72 | "no-trailing-whitespace": true,
73 | "no-unnecessary-initializer": true,
74 | "no-unused-expression": true,
75 | "no-use-before-declare": true,
76 | "no-var-keyword": true,
77 | "object-literal-sort-keys": false,
78 | "one-line": [
79 | true,
80 | "check-open-brace",
81 | "check-catch",
82 | "check-else",
83 | "check-whitespace"
84 | ],
85 | "prefer-const": true,
86 | "quotemark": [
87 | true,
88 | "single"
89 | ],
90 | "radix": true,
91 | "semicolon": [
92 | true,
93 | "always"
94 | ],
95 | "triple-equals": [
96 | true,
97 | "allow-null-check"
98 | ],
99 | "typedef-whitespace": [
100 | true,
101 | {
102 | "call-signature": "nospace",
103 | "index-signature": "nospace",
104 | "parameter": "nospace",
105 | "property-declaration": "nospace",
106 | "variable-declaration": "nospace"
107 | }
108 | ],
109 | "unified-signatures": true,
110 | "variable-name": false,
111 | "whitespace": [
112 | true,
113 | "check-branch",
114 | "check-decl",
115 | "check-operator",
116 | "check-separator",
117 | "check-type"
118 | ],
119 | "no-output-on-prefix": true,
120 | "use-input-property-decorator": true,
121 | "use-output-property-decorator": true,
122 | "use-host-property-decorator": true,
123 | "no-input-rename": true,
124 | "no-output-rename": true,
125 | "use-life-cycle-interface": true,
126 | "use-pipe-transform-interface": true,
127 | "component-class-suffix": true,
128 | "directive-class-suffix": true
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/images/WeatherDashUI.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrjohns42/WeatherDash/034c807346363a21a8644847ee8666fee051ceb8/images/WeatherDashUI.PNG
--------------------------------------------------------------------------------
/images/test:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/server/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/server/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "strongloop",
3 | "parserOptions": {
4 | "ecmaVersion": 6
5 | },
6 | "env": {
7 | "es6": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/server/apis.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const request = require('request');
4 | const fs = require('fs');
5 |
6 | const config = require('./config');
7 |
8 | module.exports = APIS;
9 |
10 | const HTTP_STATUS_OK = 200;
11 | const OWM_URL = 'https://api.openweathermap.org/data/2.5';
12 | const ECOBEE_URL = 'https://api.ecobee.com';
13 |
14 | const TERM_FG_GREEN = '\x1b[32m';
15 | const TERM_FG_BLACK = '\x1b[30m';
16 | const TERM_BG_WHITE = '\x1b[47m';
17 | const TERM_RESET = '\x1b[0m';
18 |
19 | function APIS() {
20 | this.UNIT = config.DEFAULT_UNIT;
21 | this.ZIP = config.DEFAULT_ZIP_CODE;
22 |
23 | this.WEATHER_JSON = null;
24 | this.FORECAST_JSON = null;
25 | this.SUMMARY_JSON = null;
26 | this.INDOOR_JSON = null;
27 |
28 | this.TOKEN_TIMER = null;
29 | this.ECOBEE_ACCESS_TOKEN = null;
30 | this.ECOBEE_REFRESH_TOKEN = null;
31 | }
32 |
33 | APIS.prototype.constructor = APIS;
34 |
35 | APIS.prototype.getOWMWeather = function() {
36 | var self = this;
37 | var url = OWM_URL + '/' + 'weather?';
38 | url += '&zip=' + this.ZIP;
39 | if (this.UNIT) {
40 | url += '&units=' + this.UNIT;
41 | }
42 | url += '&APPID=' + config.OWM_API_KEY;
43 | console.info('Accessing: ' + url);
44 | return new Promise(resolve => {
45 | request(url, function(err, res, body) {
46 | if (!res) {
47 | console.error('Weather Error: No repsonse');
48 | resolve(false);
49 | } else if (err || res.statusCode !== HTTP_STATUS_OK) {
50 | console.error('Weather Error: ', err, 'Status: ', res.statusCode);
51 | resolve(false);
52 | } else {
53 | let json = JSON.parse(body);
54 | self.WEATHER_JSON = json;
55 | console.log('Updated weather data!');
56 | resolve(true);
57 | }
58 | });
59 | });
60 | };
61 |
62 | APIS.prototype.getOWMForecast = function() {
63 | var self = this;
64 | var url = OWM_URL + '/' + 'forecast?';
65 | url += '&zip=' + this.ZIP;
66 | if (this.UNIT) {
67 | url += '&units=' + this.UNIT;
68 | }
69 | url += '&APPID=' + config.OWM_API_KEY;
70 | console.info('Accessing: ' + url);
71 | return new Promise(resolve => {
72 | request(url, function(err, res, body) {
73 | if (!res) {
74 | console.error('Forecast Error: No repsonse');
75 | resolve(false);
76 | } else if (err || res.statusCode !== HTTP_STATUS_OK) {
77 | console.error('Forecast Error: ', err, 'Status: ', res.statusCode);
78 | resolve(false);
79 | } else {
80 | let json = JSON.parse(body);
81 | self.FORECAST_JSON = json;
82 | console.log('Updated forecast data!');
83 | resolve(true);
84 | }
85 | });
86 | });
87 | };
88 |
89 | APIS.prototype.ecobeeAuth = function() {
90 | var self = this;
91 | return new Promise(resolve => {
92 | if (fs.existsSync(config.ECOBEE_TOKENS_FILE)) {
93 | var tokens = JSON.parse(
94 | fs.readFileSync(config.ECOBEE_TOKENS_FILE, 'utf8')
95 | );
96 | self.ECOBEE_REFRESH_TOKEN = tokens['ECOBEE_REFRESH_TOKEN'];
97 | self.ECOBEE_ACCESS_TOKEN = tokens['ECOBEE_ACCESS_TOKEN'];
98 | self.ecobeeRefreshTokens().then(success => {
99 | if (success) {
100 | console.log('Refreshed Ecobee tokens!');
101 | resolve(true);
102 | } else resolve(false);
103 | });
104 | return;
105 | }
106 |
107 | var url = ECOBEE_URL + '/' + 'authorize?';
108 | url += 'response_type=ecobeePin';
109 | url += '&scope=smartRead';
110 | url += '&client_id=' + config.ECOBEE_API_KEY;
111 | console.info('Accessing: ' + url);
112 | request(url, function(err, res, body) {
113 | if (!res) {
114 | console.error('Authorize Error: No repsonse');
115 | resolve(false);
116 | } else if (err || res.statusCode !== HTTP_STATUS_OK) {
117 | console.error('Authorize Error: ', err, 'Status: ', res.statusCode);
118 | resolve(false);
119 | } else {
120 | let json = JSON.parse(body);
121 | console.log(
122 | TERM_FG_GREEN,
123 | '\n\n Please log into your Ecobee account at: ' +
124 | 'https://www.ecobee.com' +
125 | '\n Navigate to Settings (upper right icon) => My Apps' +
126 | '\n Enter the following pin:',
127 | TERM_FG_BLACK,
128 | TERM_BG_WHITE,
129 | json['ecobeePin'],
130 | TERM_RESET,
131 | '\n'
132 | );
133 | console.log('Pin expires in', json['expires_in'], 'minutes');
134 | console.log('Polling at', json['interval'], 'seconds');
135 | self.TOKEN_TIMER = setInterval(() => {
136 | self.ecobeeGetTokens(json['code']).then(success => {
137 | if (success) {
138 | console.log('Authenticated with Ecobee!');
139 | clearInterval(self.TOKEN_TIMER);
140 | resolve(true);
141 | }
142 | });
143 | }, json['interval'] * 1000);
144 |
145 | }
146 | });
147 | });
148 | };
149 |
150 | APIS.prototype.ecobeeGetTokens = function(authKey) {
151 | var self = this;
152 | var url = ECOBEE_URL + '/' + 'token?';
153 | url += 'grant_type=ecobeePin';
154 | url += '&client_id=' + config.ECOBEE_API_KEY;
155 | url += '&code=' + authKey;
156 | console.info('Accessing: ' + url);
157 | return new Promise(resolve => {
158 | request({url: url, method: 'POST'}, function(err, res, body) {
159 | if (!res) {
160 | console.error('Token Error: No repsonse');
161 | resolve(false);
162 | } else if (res.statusCode === 401) {
163 | console.warn('Not yet authenticated!');
164 | resolve(false);
165 | } else if (err || res.statusCode !== HTTP_STATUS_OK) {
166 | console.error('Token Error: ', err, 'Status: ', res.statusCode);
167 | resolve(false);
168 | } else {
169 | let json = JSON.parse(body);
170 | self.ECOBEE_REFRESH_TOKEN = json['refresh_token'];
171 | self.ECOBEE_ACCESS_TOKEN = json['access_token'];
172 | if (fs.existsSync(config.ECOBEE_TOKENS_FILE)) {
173 | fs.unlinkSync(config.ECOBEE_TOKENS_FILE);
174 | }
175 | var ECOBEE_ACCESS_TOKEN = self.ECOBEE_ACCESS_TOKEN;
176 | var ECOBEE_REFRESH_TOKEN = self.ECOBEE_REFRESH_TOKEN;
177 | fs.writeFileSync(config.ECOBEE_TOKENS_FILE,
178 | JSON.stringify({ECOBEE_ACCESS_TOKEN, ECOBEE_REFRESH_TOKEN})
179 | );
180 | console.log('Ecobee tokens saved to:', config.ECOBEE_TOKENS_FILE);
181 | resolve(true);
182 | }
183 | });
184 | });
185 | };
186 |
187 | APIS.prototype.ecobeeRefreshTokens = function() {
188 | var self = this;
189 | var url = ECOBEE_URL + '/' + 'token?';
190 | url += 'grant_type=refresh_token';
191 | url += '&code=' + this.ECOBEE_REFRESH_TOKEN;
192 | url += '&client_id=' + config.ECOBEE_API_KEY;
193 | console.info('Accessing: ' + url);
194 | return new Promise(resolve => {
195 | request({url: url, method: 'POST'}, function(err, res, body) {
196 | if (!res) {
197 | console.error('Refresh Error: No repsonse');
198 | resolve(false);
199 | } else if (err || res.statusCode !== HTTP_STATUS_OK) {
200 | console.error('Refresh Error: ', err, 'Status: ', res.statusCode);
201 | resolve(false);
202 | } else {
203 | let json = JSON.parse(body);
204 | self.ECOBEE_REFRESH_TOKEN = json['refresh_token'];
205 | self.ECOBEE_ACCESS_TOKEN = json['access_token'];
206 | if (fs.existsSync(config.ECOBEE_TOKENS_FILE)) {
207 | fs.unlinkSync(config.ECOBEE_TOKENS_FILE);
208 | }
209 | var ECOBEE_ACCESS_TOKEN = self.ECOBEE_ACCESS_TOKEN;
210 | var ECOBEE_REFRESH_TOKEN = self.ECOBEE_REFRESH_TOKEN;
211 | fs.writeFileSync(config.ECOBEE_TOKENS_FILE,
212 | JSON.stringify({ECOBEE_ACCESS_TOKEN, ECOBEE_REFRESH_TOKEN})
213 | );
214 | console.log('Ecobee tokens saved to:', config.ECOBEE_TOKENS_FILE);
215 | resolve(true);
216 | }
217 | });
218 | });
219 | };
220 |
221 | APIS.prototype.getEcobeeSummary = function() {
222 | var self = this;
223 | var url = ECOBEE_URL + '/' + '1' + '/' + 'thermostatSummary?';
224 | var body =
225 | {
226 | selection:
227 | {
228 | selectionType: 'registered',
229 | selectionMatch: '',
230 | includeEquipmentStatus: 'true',
231 | },
232 | };
233 | url += 'json=' + encodeURIComponent(JSON.stringify(body));
234 | var options = {
235 | url: url,
236 | headers: {
237 | Authorization: 'Bearer ' + this.ECOBEE_ACCESS_TOKEN,
238 | 'content-type': 'application/json',
239 | },
240 | method: 'GET',
241 | };
242 | console.info('Accessing: ' + url);
243 | return new Promise(resolve => {
244 | request(options, function(err, res, body) {
245 | if (!res) {
246 | console.error('Summary Error: No repsonse');
247 | resolve(false);
248 | } else if (err || res.statusCode !== HTTP_STATUS_OK) {
249 | console.error('Summary Error: ', err, 'Status: ', res.statusCode);
250 | if (res.statusCode === 500) {
251 | self.ecobeeRefreshTokens();
252 | }
253 | resolve(false);
254 | } else {
255 | let json = JSON.parse(body);
256 | switch (json['status']['code']) {
257 | case 14:
258 | self.ecobeeRefreshTokens();
259 | resolve(false);
260 | break;
261 | case 0:
262 | if (JSON.stringify(json) !== JSON.stringify(self.SUMMARY_JSON)) {
263 | self.SUMMARY_JSON = json;
264 | console.log('Updated summary data!');
265 | resolve(true);
266 | } else {
267 | console.log('No new summary data!');
268 | resolve(false);
269 | }
270 | break;
271 | default:
272 | console.error(
273 | 'Summary Status Error' +
274 | json['status']['code'] +
275 | ':' +
276 | json['status']['message']
277 | );
278 | resolve(false);
279 | break;
280 | }
281 | }
282 | });
283 | });
284 | };
285 |
286 |
287 | APIS.prototype.getEcobeeThermostats = function() {
288 | var self = this;
289 | var url = ECOBEE_URL + '/' + '1' + '/' + 'thermostat?';
290 | var body =
291 | {
292 | selection:
293 | {
294 | selectionType: 'registered',
295 | selectionMatch: '',
296 | includeAlerts: 'true',
297 | includeEvents: 'true',
298 | includeSettings: 'true',
299 | includeRuntime: 'true',
300 | },
301 | };
302 | url += 'json=' + encodeURIComponent(JSON.stringify(body));
303 | var options = {
304 | url: url,
305 | headers: {
306 | Authorization: 'Bearer ' + this.ECOBEE_ACCESS_TOKEN,
307 | 'content-type': 'application/json',
308 | },
309 | method: 'GET',
310 | };
311 | console.info('Accessing: ' + url);
312 | return new Promise(resolve => {
313 | request(options, function(err, res, body) {
314 | if (!res) {
315 | console.error('Thermostat Error: No repsonse');
316 | resolve(false);
317 | } else if (err || res && res.statusCode !== HTTP_STATUS_OK) {
318 | console.error('Thermostat Error: ', err, 'Status: ', res.statusCode);
319 | resolve(false);
320 | } else {
321 | let json = JSON.parse(body);
322 | switch (json['status']['code']) {
323 | case 14:
324 | self.ecobeeRefreshTokens();
325 | resolve(false);
326 | break;
327 | case 0:
328 | if (self.UNIT === config.UNIT_METRIC) {
329 | console.log('Converting to metric');
330 | self.ecobeeTempConvert(json, convertDegreesFtoC);
331 | }
332 | self.INDOOR_JSON = json;
333 | console.log('Updated thermostat data!');
334 | resolve(true);
335 | break;
336 | default:
337 | console.error(
338 | 'Thermostat Status Error' +
339 | json['status']['code'] +
340 | ':' +
341 | json['status']['message']
342 | );
343 | resolve(false);
344 | break;
345 | }
346 | }
347 | });
348 | });
349 | };
350 |
351 | APIS.prototype.ecobeeTempConvert = function(json, conversionFn) {
352 | for (var therm of json['thermostatList']) {
353 | if (therm.hasOwnProperty('runtime')) {
354 | var rt = therm['runtime'];
355 | rt['actualTemperature'] = conversionFn(rt['actualTemperature']);
356 | rt['desiredCool'] = conversionFn(rt['desiredCool']);
357 | rt['desiredHeat'] = conversionFn(rt['desiredHeat']);
358 | for (let temp of rt['desiredCoolRange']) {
359 | temp = conversionFn(temp);
360 | }
361 | for (let temp of rt['desiredHeatRange']) {
362 | temp = conversionFn(temp);
363 | }
364 | }
365 | }
366 | };
367 |
368 | function convertDegreesFtoC(degreesF) {
369 | var degreesC = (degreesF - 320) * 5 / 9;
370 | return degreesC;
371 | }
372 |
373 |
374 |
--------------------------------------------------------------------------------
/server/betterlog.js:
--------------------------------------------------------------------------------
1 | // BetterLog
2 | // Inspired by log-prefix and log-timestamp
3 | 'use strict';
4 |
5 | var util = require('util');
6 |
7 | var funcs = {
8 | log: console.log.bind(console),
9 | info: console.info.bind(console),
10 | warn: console.warn.bind(console),
11 | error: console.error.bind(console),
12 | };
13 |
14 | function patch(fn, logType) {
15 | Object.keys(funcs).forEach(function(k) {
16 | console[k] = function() {
17 | var s = typeof fn === 'function' ? fn() : fn;
18 | arguments[0] = util.format(s, arguments[0]);
19 | if (logType) {
20 | arguments[0] = k.toUpperCase().padEnd(5) + ' ' + arguments[0];
21 | }
22 | funcs[k].apply(console, arguments);
23 | };
24 | });
25 | }
26 |
27 | // the default date format to print
28 | function timestamp() {
29 | return '[' + new Date().toISOString() + ']';
30 | }
31 |
32 | function betterLog(fn, logType) {
33 | if (!logType) { logType = false; }
34 | patch(fn || timestamp, logType);
35 | }
36 |
37 |
38 | betterLog(); // initialize with default
39 | module.exports = betterLog;
40 |
--------------------------------------------------------------------------------
/server/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 |
5 | const UNIT_IMPERIAL = 'imperial';
6 | const UNIT_METRIC = 'metric';
7 | const UNIT_CHOICES = [UNIT_IMPERIAL, UNIT_METRIC];
8 |
9 |
10 | // Change these to desired values
11 | const DEFAULT_UNIT = UNIT_IMPERIAL;
12 | const DEFAULT_ZIP_CODE = '60613';
13 |
14 |
15 | const OWM_WEATHER_POLL_FREQ_MS = (1000 * 30);
16 | const OWM_FORECAST_POLL_FREQ_MS = (1000 * 60 * 15);
17 | const ECOBEE_POLL_FREQ_MS = (1000 * 60 * 1);
18 |
19 | const ECOBEE_TOKENS_FILE = './keys/ecobee_tokens.json';
20 |
21 | const OWM_API_KEY_FILE = './keys/owm.key';
22 | const OWM_API_KEY = readKey(OWM_API_KEY_FILE);
23 |
24 | const ECOBEE_API_KEY_FILE = './keys/ecobee.key';
25 | const ECOBEE_API_KEY = readKey(ECOBEE_API_KEY_FILE);
26 |
27 | function readKey(keyFile) {
28 | if (fs.existsSync(keyFile)){
29 | var contents = fs.readFileSync(keyFile, {encoding: 'utf8'});
30 | contents = contents.trim();
31 | if (contents === '') {
32 | console.error('API key file is empty:', keyFile);
33 | console.error('Please populate this file with an API key in plaintext');
34 | process.exit(1);
35 | } else return contents;
36 | } else {
37 | console.error('Couldn\'t find API key file:', keyFile);
38 | console.error(
39 | 'Please create a file at this location ' +
40 | 'and populate with API key in plaintext'
41 | );
42 | process.exit(1);
43 | }
44 | }
45 |
46 |
47 | module.exports = {
48 | DEFAULT_ZIP_CODE,
49 | DEFAULT_UNIT,
50 | UNIT_IMPERIAL,
51 | UNIT_METRIC,
52 | UNIT_CHOICES,
53 | OWM_WEATHER_POLL_FREQ_MS,
54 | OWM_FORECAST_POLL_FREQ_MS,
55 | OWM_API_KEY,
56 | ECOBEE_POLL_FREQ_MS,
57 | ECOBEE_API_KEY,
58 | ECOBEE_TOKENS_FILE,
59 | };
60 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const dateFormat = require('dateformat');
4 | require('./betterlog')(function() {
5 | var now = new Date();
6 | var date = dateFormat(now, '[yyyy-mm-dd HH:MM:ss.l]');
7 | var log = date + ' ' + '%s';
8 | return log;
9 | }, true);
10 |
11 | const express = require('express');
12 | const cors = require('cors');
13 |
14 | const config = require('./config');
15 | const APIS = require('./apis');
16 |
17 | var api = new APIS();
18 | var WEATHER_TIMER = null;
19 | var FORECAST_TIMER = null;
20 | var INDOOR_TIMER = null;
21 |
22 | function startup() {
23 | scheduleAPICalls().then(() => {
24 | startAPIServer();
25 | });
26 | }
27 |
28 | function startAPIServer() {
29 | console.log('Starting API Server');
30 | const app = express();
31 | var corsOptions = {
32 | origin: 'http://localhost:4200',
33 | optionsSuccessStatus: 200,
34 | };
35 |
36 | app.use(cors(corsOptions));
37 |
38 | app.route('/api/reset').get((req, res) => {
39 | console.info('Client accessing reset!');
40 | unscheduleAPICalls();
41 | api.WEATHER_JSON = null;
42 | api.FORECAST_JSON = null;
43 | api.SUMMARY_JSON = null;
44 | api.INDOOR_JSON = null;
45 | startup();
46 | res.send();
47 | });
48 | app.route('/api/weather').get((req, res) => {
49 | console.info('Client accessing weather!');
50 | res.send(api.WEATHER_JSON);
51 | });
52 | app.route('/api/forecast').get((req, res) => {
53 | console.info('Client accessing forecast!');
54 | res.send(api.FORECAST_JSON);
55 | });
56 | app.route('/api/indoor').get((req, res) => {
57 | console.info('Client accessing indoor!');
58 | res.send(api.INDOOR_JSON);
59 | });
60 | app.route('/api/settings').post((req, res) => {
61 | console.info('Client accessing settings!');
62 | if (req.query.units) {
63 | if (config.UNIT_CHOICES.includes(req.query.units)) {
64 | api.UNIT = req.query.units;
65 | } else {
66 | res.status(422).send('Bad value for unit parameter');
67 | return;
68 | }
69 | }
70 | if (req.query.zip) {
71 | if (/(^\d{5}$)/.test(req.query.zip)) {
72 | api.ZIP = req.query.zip;
73 | } else {
74 | res.status(422).send('Bad value for zip parameter');
75 | return;
76 | }
77 | }
78 | api.WEATHER_JSON = null;
79 | api.FORECAST_JSON = null;
80 | api.SUMMARY_JSON = null;
81 | api.INDOOR_JSON = null;
82 | var weatherPromise = api.getOWMWeather();
83 | var forecastPromise = api.getOWMForecast();
84 | var thermostatPromise = api.getEcobeeThermostats();
85 | Promise.all([weatherPromise, forecastPromise, thermostatPromise])
86 | .then(data => {
87 | var weatherStatus = data[0];
88 | var forecastStatus = data[1];
89 | var thermostatStatus = data[2];
90 | if (!weatherStatus) {
91 | res.status(500).send('Failed to update weather data');
92 | return;
93 | }
94 | if (!forecastStatus) {
95 | res.status(500).send('Failed to update forecast data');
96 | return;
97 | }
98 | if (!thermostatStatus) {
99 | res.status(500).send('Failed to update thermostat data');
100 | return;
101 | }
102 | res.send();
103 | });
104 | });
105 |
106 | app.listen(8000, () => {
107 | console.log('Server started!');
108 | });
109 | }
110 |
111 | function scheduleAPICalls() {
112 | return api.ecobeeAuth().then(success => {
113 | var weatherPromise = api.getOWMWeather();
114 | var forecastPromise = api.getOWMForecast();
115 | var thermostatPromise = api.getEcobeeThermostats();
116 | Promise.all([weatherPromise, forecastPromise, thermostatPromise])
117 | .then(data => {
118 | WEATHER_TIMER = setInterval(
119 | function() { api.getOWMWeather(); },
120 | config.OWM_WEATHER_POLL_FREQ_MS
121 | );
122 | FORECAST_TIMER = setInterval(
123 | function() { api.getOWMForecast(); },
124 | config.OWM_FORECAST_POLL_FREQ_MS
125 | );
126 | INDOOR_TIMER = setInterval(
127 | function() {
128 | api.getEcobeeSummary().then(success => {
129 | if (success) api.getEcobeeThermostats();
130 | });
131 | }, config.ECOBEE_POLL_FREQ_MS
132 | );
133 | });
134 | }, (err) => { console.error(err); });
135 | }
136 |
137 | function unscheduleAPICalls() {
138 | clearInterval(WEATHER_TIMER);
139 | clearInterval(FORECAST_TIMER);
140 | clearInterval(INDOOR_TIMER);
141 | }
142 |
143 | startup();
144 |
--------------------------------------------------------------------------------
/server/keys/.gitignore:
--------------------------------------------------------------------------------
1 | *.*
2 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "weatherserver",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "pretest": "eslint . && echo \"Lint Passed\"",
8 | "test": "echo \"No Test Specified\"",
9 | "start": "node index.js"
10 | },
11 | "author": "Matthew R. Johnson",
12 | "license": "Beerware",
13 | "dependencies": {
14 | "cors": "^2.8.4",
15 | "dateformat": "^3.0.3",
16 | "express": "^4.16.3",
17 | "request": "^2.88.0"
18 | },
19 | "devDependencies": {
20 | "eslint": "^5.3.0",
21 | "eslint-config-strongloop": "^2.1.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/startup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -xe
2 |
3 | SCRIPT_PATH=$(readlink -f "$0")
4 | SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
5 |
6 | SERVER_DIR=${SCRIPT_DIR}/server
7 | SERVER_HOST=localhost
8 | SERVER_PORT=8000
9 | SERVER_ADDR=http://${SERVER_HOST}:${SERVER_PORT}
10 |
11 | CLIENT_DIR=${SCRIPT_DIR}/client
12 | CLIENT_HOST=localhost
13 | CLIENT_PORT=4200
14 | CLIENT_ADDR=http://${CLIENT_HOST}:${CLIENT_PORT}
15 |
16 | echo "WEATHERDASH STARTUP"
17 |
18 | cd ${SERVER_DIR}
19 | echo "Starting API Server in background.."
20 | x-terminal-emulator -t "WeatherDash Server" -e bash -c "npm start 2>&1 | tee ${SCRIPT_DIR}/server.log"
21 |
22 | cd ${CLIENT_DIR}
23 | echo "Building Client Application..."
24 | ng build --prod --aot --build-optimizer --output-hashing=none
25 | echo "Build Complete."
26 |
27 | cd ${CLIENT_DIR}/dist/WeatherClient
28 | echo "Starting Client Host in background..."
29 | x-terminal-emulator -t "WeatherDash Client" -e bash -c "angular-http-server -p ${CLIENT_PORT} 2>&1 | tee ${SCRIPT_DIR}/client.log"
30 |
31 | echo "Waiting for API Server to come online..."
32 | while ! nc -z ${SERVER_HOST} ${SERVER_PORT}
33 | do
34 | sleep 1
35 | done
36 | echo "API Server online."
37 |
38 | echo "Waiting for Client Host to come online..."
39 | while ! nc -z ${CLIENT_HOST} ${CLIENT_PORT}
40 | do
41 | sleep 1
42 | done
43 | echo "Client Host online."
44 |
45 | cd ${SCRIPT_DIR}
46 | echo "Cleaning up Chromium"
47 | #Delete SingletonLock
48 | rm -f ~/.config/chromium/SingletonLock
49 | rm -rf ~/.cache/chromium
50 | #Clean up the randomly-named file(s)
51 | for i in ~/.config/chromium/Default/.org.chromium.Chromium.*; do
52 | sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' $i || true
53 | sed -i 's/"exit_state": "Crashed"/"exit_state": "Normal"/' $i || true
54 | sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' $i || true
55 | done
56 | #Clean up Preferences
57 | sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' ~/.config/chromium/Default/Preferences
58 | sed -i 's/"exit_state": "Crashed"/"exit_state": "Normal"/' ~/.config/chromium/Default/Preferences
59 | sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' ~/.config/chromium/Default/Preferences
60 | #Clean up Local State
61 | sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' ~/.config/chromium/"Local State"
62 |
63 | echo "Launching WeatherDash"
64 | x-terminal-emulator -t "WeatherDash Console" -e bash -c "chromium-browser --start-fullscreen --enable-logging=stderr -rv=1 ${CLIENT_ADDR} 2>&1 | tee ${SCRIPT_DIR}/browser.log"
65 |
66 | echo "DONE"
67 |
--------------------------------------------------------------------------------