├── .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 | ![TADA!](images/WeatherDashUI.PNG) 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 | 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 | 4 | 5 | Created by FontForge 20110222 at Wed Feb 11 17:55:33 2015 6 | By www-data 7 | 8 | 9 | 10 | 23 | 26 | 29 | 31 | 33 | 35 | 38 | 42 | 46 | 49 | 52 | 55 | 59 | 62 | 65 | 68 | 71 | 74 | 77 | 80 | 83 | 87 | 91 | 95 | 99 | 102 | 105 | 108 | 111 | 115 | 120 | 123 | 127 | 132 | 137 | 141 | 146 | 152 | 156 | 160 | 164 | 168 | 173 | 180 | 189 | 193 | 195 | 197 | 201 | 204 | 208 | 211 | 214 | 218 | 223 | 225 | 227 | 230 | 233 | 237 | 241 | 244 | 246 | 248 | 251 | 271 | 275 | 278 | 286 | 289 | 293 | 295 | 297 | 300 | 305 | 310 | 317 | 326 | 337 | 345 | 353 | 361 | 370 | 373 | 393 | 394 | 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 | --------------------------------------------------------------------------------