├── client ├── src │ ├── assets │ │ ├── .gitkeep │ │ ├── gh.png │ │ ├── ewelink.png │ │ ├── preview.svg │ │ └── github.svg │ ├── app │ │ ├── app.component.scss │ │ ├── home │ │ │ ├── home.component.scss │ │ │ ├── home.component.spec.ts │ │ │ ├── home.component.ts │ │ │ └── home.component.html │ │ ├── menu-info │ │ │ ├── menu-info.component.scss │ │ │ ├── menu-info.component.ts │ │ │ ├── menu-info.component.html │ │ │ └── menu-info.component.spec.ts │ │ ├── login │ │ │ ├── login.component.scss │ │ │ ├── login.component.spec.ts │ │ │ ├── login.component.html │ │ │ └── login.component.ts │ │ ├── models │ │ │ ├── authLogin.ts │ │ │ ├── response.ts │ │ │ ├── change_value.ts │ │ │ ├── menu_link.ts │ │ │ ├── setStateWithURL.ts │ │ │ ├── clientInfo.ts │ │ │ ├── data_login.ts │ │ │ ├── device.ts │ │ │ ├── ewelink_enums.ts │ │ │ ├── deviceInfo.ts │ │ │ ├── user.ts │ │ │ └── wssPayload.ts │ │ ├── switch │ │ │ ├── switch.component.scss │ │ │ ├── switch.component.spec.ts │ │ │ ├── switch.component.html │ │ │ └── switch.component.ts │ │ ├── app.component.html │ │ ├── services │ │ │ ├── auth.service.spec.ts │ │ │ ├── switch.service.spec.ts │ │ │ ├── theme.service.spec.ts │ │ │ ├── event.service.ts │ │ │ ├── auth.service.ts │ │ │ ├── theme.service.ts │ │ │ ├── switch.service.ts │ │ │ └── socket.service.ts │ │ ├── guard │ │ │ ├── login.guard.spec.ts │ │ │ ├── logout.guard.spec.ts │ │ │ ├── logout.guard.ts │ │ │ └── login.guard.ts │ │ ├── device │ │ │ ├── device.component.spec.ts │ │ │ ├── device.component.scss │ │ │ ├── device.component.html │ │ │ └── device.component.ts │ │ ├── json-pretty-dialog │ │ │ ├── json-pretty-dialog.component.html │ │ │ ├── json-pretty-dialog.component.spec.ts │ │ │ ├── json-pretty-dialog.component.scss │ │ │ └── json-pretty-dialog.component.ts │ │ ├── app.component.ts │ │ ├── app-routing.module.ts │ │ ├── material-definitions │ │ │ └── material-definitions.module.ts │ │ ├── app.component.spec.ts │ │ └── app.module.ts │ ├── favicon.ico │ ├── main.ts │ ├── test.ts │ ├── environments │ │ ├── environment.qa.ts │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── index.html │ ├── polyfills.ts │ └── styles.scss ├── e2e │ ├── tsconfig.json │ ├── src │ │ ├── app.po.ts │ │ └── app.e2e-spec.ts │ └── protractor.conf.js ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── browserslist ├── tsconfig.json ├── .gitignore ├── README.md ├── karma.conf.js ├── package.json ├── tslint.json └── angular.json ├── .github ├── assets │ └── image-20210111000940316.png ├── dev-notes │ └── research │ │ └── index.md ├── docs │ └── INSTALL.md └── README.md ├── server ├── Dockerfile ├── scripts │ └── pull_spinup-dev.sh ├── src │ ├── components │ │ └── ewelink │ │ │ ├── ewelink.routes.js │ │ │ ├── ewelink.validator.js │ │ │ └── ewelink.controller.js │ └── index.js └── package.json ├── .gitignore └── LICENSE /client/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/menu-info/menu-info.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/login/login.component.scss: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 75%; 3 | } 4 | -------------------------------------------------------------------------------- /client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbatty19/ewelink-web-ui/HEAD/client/src/favicon.ico -------------------------------------------------------------------------------- /client/src/assets/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbatty19/ewelink-web-ui/HEAD/client/src/assets/gh.png -------------------------------------------------------------------------------- /client/src/assets/ewelink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbatty19/ewelink-web-ui/HEAD/client/src/assets/ewelink.png -------------------------------------------------------------------------------- /client/src/app/models/authLogin.ts: -------------------------------------------------------------------------------- 1 | export default interface AuthLogin { 2 | email: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /.github/assets/image-20210111000940316.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbatty19/ewelink-web-ui/HEAD/.github/assets/image-20210111000940316.png -------------------------------------------------------------------------------- /client/src/app/models/response.ts: -------------------------------------------------------------------------------- 1 | export interface ResponseData { 2 | status: number; 3 | error: boolean; 4 | data: any; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/app/switch/switch.component.scss: -------------------------------------------------------------------------------- 1 | .padding { 2 | margin-top: 10px; 3 | padding-left: 10px; 4 | padding-right: 10px; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/app/models/change_value.ts: -------------------------------------------------------------------------------- 1 | export default interface ChangeValue { 2 | deviceid: string; 3 | params?: any; 4 | error?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/app/models/menu_link.ts: -------------------------------------------------------------------------------- 1 | export default interface MenuLink { 2 | link: string; 3 | icon: string; 4 | text: string; 5 | isSvg: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /client/src/app/models/setStateWithURL.ts: -------------------------------------------------------------------------------- 1 | export interface queryDataToSetState { 2 | deviceid: string; 3 | state: string; 4 | email: string; 5 | password: string; 6 | } 7 | -------------------------------------------------------------------------------- /client/src/app/models/clientInfo.ts: -------------------------------------------------------------------------------- 1 | export interface ClientInfo { 2 | model: string; 3 | os: string; 4 | imei: string; 5 | romVersion: string; 6 | appVersion: string; 7 | } 8 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 4231 12 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /client/src/app/models/data_login.ts: -------------------------------------------------------------------------------- 1 | import { User } from "./user"; 2 | 3 | export interface DataLogin { 4 | at: string; 5 | rt: string; 6 | user: User; 7 | region: string; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /client/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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/src/app/models/device.ts: -------------------------------------------------------------------------------- 1 | import { DeviceInfo } from "./deviceInfo"; 2 | import { StateEnum } from "./ewelink_enums"; 3 | 4 | export interface Device { 5 | status: string; 6 | state: StateEnum; 7 | name: string; 8 | deviceid: string; 9 | request: boolean; 10 | deviceInfo: DeviceInfo; 11 | } 12 | -------------------------------------------------------------------------------- /client/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /client/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 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /server/scripts/pull_spinup-dev.sh: -------------------------------------------------------------------------------- 1 | echo CI/CD Script running: 2 | sudo docker rmi $(sudo docker images 'api-ewe-link' -a -q) 3 | sudo docker stop $(sudo docker ps -a -q --filter ancestor=api-ewe-link) 4 | sudo docker rm $(sudo docker ps -a -q --filter ancestor=api-ewe-link) 5 | sudo docker build . -t api-ewe-link 6 | sudo docker run -d -p 4006:4006 api-ewe-link 7 | sudo docker ps -------------------------------------------------------------------------------- /client/src/app/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AuthService = TestBed.get(AuthService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /client/src/app/services/switch.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { SwitchService } from './switch.service'; 4 | 5 | describe('SwitchService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: SwitchService = TestBed.get(SwitchService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /client/src/app/guard/login.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, inject } from '@angular/core/testing'; 2 | 3 | import { LoginGuard } from './login.guard'; 4 | 5 | describe('LoginGuard', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [LoginGuard] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([LoginGuard], (guard: LoginGuard) => { 13 | expect(guard).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /client/src/app/guard/logout.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, inject } from '@angular/core/testing'; 2 | 3 | import { LogoutGuard } from './logout.guard'; 4 | 5 | describe('LogoutGuard', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [LogoutGuard] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([LogoutGuard], (guard: LogoutGuard) => { 13 | expect(guard).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /client/src/app/services/theme.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ThemeService } from './theme.service'; 4 | 5 | describe('ThemeService', () => { 6 | let service: ThemeService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ThemeService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'hammerjs'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | import { environment } from './environments/environment'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /server/src/components/ewelink/ewelink.routes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * IMPORTS 4 | * 5 | */ 6 | const { Login, GetDevices, GetDevice } = require('./ewelink.controller'); 7 | const { LoginValidator, GetDevicesValidator, GetDeviceValidator } = require('./ewelink.validator'); 8 | 9 | module.exports = (app) => { 10 | app.post('/login', LoginValidator, Login); 11 | app.post('/devices', GetDevicesValidator, GetDevices); 12 | app.post('/device', GetDeviceValidator, GetDevice); 13 | }; 14 | -------------------------------------------------------------------------------- /client/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /client/src/app/models/ewelink_enums.ts: -------------------------------------------------------------------------------- 1 | export enum StateEnum { 2 | on = 'on', 3 | off = 'off', 4 | onText = 'On', 5 | offText = 'Off', 6 | } 7 | 8 | export enum ThemeEnum { 9 | dark = 'darkMode', 10 | light = 'lightMode', 11 | darkText = 'Light Mode', 12 | lightText = 'Dark Mode', 13 | darkIcon = 'brightness_2', 14 | lightIcon = 'wb_sunny' 15 | } 16 | 17 | export enum JSONEnum { 18 | expanded = 'Expanded all', 19 | collapse = 'Collapse all', 20 | expandedIcon = 'unfold_more', 21 | collapseIcon = 'unfold_less' 22 | } 23 | -------------------------------------------------------------------------------- /client/src/app/menu-info/menu-info.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { environment } from 'src/environments/environment'; 3 | import MenuLink from '../models/menu_link'; 4 | 5 | @Component({ 6 | selector: 'app-menu-info', 7 | templateUrl: './menu-info.component.html', 8 | styleUrls: ['./menu-info.component.scss'] 9 | }) 10 | export class MenuInfoComponent { 11 | 12 | public menuLinks: MenuLink[] = environment.menuLinks; 13 | 14 | constructor() { } 15 | 16 | redirectComments(menulink: MenuLink) { 17 | window.open(menulink.link); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /client/src/app/menu-info/menu-info.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | 17 | -------------------------------------------------------------------------------- /client/src/app/guard/logout.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class LogoutGuard implements CanActivate { 8 | 9 | constructor(public router: Router){} 10 | 11 | canActivate(next: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean { 12 | const isLogin: boolean = !localStorage.getItem('data'); 13 | if(!isLogin) { 14 | this.router.navigate(['/home']); 15 | return false; 16 | } 17 | return true; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /client/src/app/guard/login.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class LoginGuard implements CanActivate { 8 | 9 | constructor(public router: Router){} 10 | 11 | canActivate(next: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean { 12 | const isHome: boolean = localStorage.getItem('data') !== null; 13 | if(!isHome) { 14 | this.router.navigate(['/login']); 15 | return false; 16 | } 17 | return true; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /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/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/environments/environment.qa.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | urlBase: 'https://ewelink-web-api-git-dev.rober191.vercel.app', 4 | menuLinks: [ 5 | { 6 | link: 'https://github.com/Rober19/ewelink-web-ui', 7 | icon: 'folder', 8 | text: 'App Repository' 9 | }, 10 | { 11 | link: 'https://github.com/Rober19', 12 | icon: 'github', 13 | text: 'Rober19' 14 | }, 15 | { 16 | link: 'https://github.com/DevelopGadget', 17 | icon: 'github', 18 | text: 'DevelopGadget' 19 | } 20 | ], 21 | urlWebSocket: (region = "us") => `wss://${region}-pconnect3.coolkit.cc:8080/api/ws`, 22 | alternativeIcon: 'https://static.thenounproject.com/png/252447-200.png' 23 | }; 24 | -------------------------------------------------------------------------------- /client/src/app/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/device/device.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DeviceComponent } from './device.component'; 4 | 5 | describe('DeviceComponent', () => { 6 | let component: DeviceComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DeviceComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DeviceComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": "./", 6 | "outDir": "./ ", 7 | "sourceMap": true, 8 | "resolveJsonModule": true, 9 | "declaration": false, 10 | "downlevelIteration": true, 11 | "experimentalDecorators": true, 12 | "module": "es2020", 13 | "moduleResolution": "node", 14 | "importHelpers": true, 15 | "target": "es2015", 16 | "skipLibCheck": true, 17 | "typeRoots": [ 18 | "node_modules/@types" 19 | ], 20 | "lib": [ 21 | "es2018", 22 | "dom" 23 | ] 24 | }, 25 | "angularCompilerOptions": { 26 | "fullTemplateTypeCheck": true, 27 | "strictInjectionParameters": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('switch-ewelink-client app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/switch/switch.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SwitchComponent } from './switch.component'; 4 | 5 | describe('SwitchComponent', () => { 6 | let component: SwitchComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SwitchComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SwitchComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/json-pretty-dialog/json-pretty-dialog.component.html: -------------------------------------------------------------------------------- 1 |

2 | JSON Viewer 3 | 7 |

8 | 9 | 10 |

11 |   
12 | 
13 | 14 | 15 | 16 | 20 | 21 | -------------------------------------------------------------------------------- /client/src/app/services/event.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class EventService { 8 | private dataSource; 9 | private data; 10 | 11 | constructor() { 12 | this.initialize(); 13 | } 14 | 15 | initialize() { 16 | this.dataSource = new BehaviorSubject({}); 17 | this.data = this.dataSource.asObservable(); 18 | } 19 | 20 | emit(type: EventType, data: any) { 21 | this.dataSource.next({ type, data }); 22 | } 23 | 24 | listen() { 25 | return this.data; 26 | } 27 | 28 | finish() { 29 | this.dataSource.next({}); 30 | } 31 | } 32 | 33 | export type EventType = 'LISTEN_STATE_CHANNEL' | 'SET_STATE_EVENT' | 'HOLA'; -------------------------------------------------------------------------------- /client/src/app/menu-info/menu-info.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MenuInfoComponent } from './menu-info.component'; 4 | 5 | describe('MenuInfoComponent', () => { 6 | let component: MenuInfoComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ MenuInfoComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MenuInfoComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * PACKAGES 4 | * 5 | */ 6 | const express = require('express'); 7 | const { json, urlencoded } = express; 8 | const app = express(); 9 | const morgan = require('morgan'); 10 | const cors = require('cors'); 11 | const { errors } = require('celebrate'); 12 | /* 13 | * 14 | * USE 15 | * 16 | */ 17 | app.use(morgan('dev')); 18 | app.use(cors()); 19 | app.use(json()); 20 | app.use(urlencoded({ extended: false })); 21 | 22 | 23 | /** 24 | * 25 | * <> ROUTES 26 | * 27 | */ 28 | require('./components/ewelink/ewelink.routes')(app); // ewelink routes 29 | 30 | 31 | /* 32 | * 33 | * USE - POST ROUTES 34 | * 35 | */ 36 | app.use(errors()); 37 | 38 | app.listen(process.env.PORT || 4006, () => { 39 | console.log('connected'); 40 | }); 41 | 42 | module.exports = app; 43 | -------------------------------------------------------------------------------- /client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | urlBase: 'https://ewelink-web-app-server-instance-1.azurewebsites.net', 4 | menuLinks: [ 5 | { 6 | link: 'https://github.com/Rober19/ewelink-web-ui', 7 | icon: 'folder', 8 | text: 'App Repository', 9 | isSvg: false 10 | }, 11 | { 12 | link: 'https://github.com/Rober19', 13 | icon: 'github', 14 | text: 'Rober19', 15 | isSvg: true 16 | }, 17 | { 18 | link: 'https://github.com/DevelopGadget', 19 | icon: 'github', 20 | text: 'DevelopGadget', 21 | isSvg: true 22 | } 23 | ], 24 | urlWebSocket: (region = "us") => `wss://${region}-pconnect3.coolkit.cc:8080/api/ws`, 25 | alternativeIcon: 'https://static.thenounproject.com/png/252447-200.png' 26 | }; 27 | -------------------------------------------------------------------------------- /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 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /server/src/components/ewelink/ewelink.validator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * PACKAGES 4 | * 5 | */ 6 | const { celebrate, Joi, Segments } = require('celebrate'); 7 | 8 | /** 9 | * 10 | */ 11 | exports.LoginValidator = celebrate({ 12 | [Segments.BODY]: Joi.object().keys({ 13 | email: Joi.string().email().required(), 14 | password: Joi.string().required(), 15 | }), 16 | }); 17 | 18 | /** 19 | * 20 | */ 21 | exports.GetDeviceValidator = celebrate({ 22 | [Segments.BODY]: Joi.object().keys({ 23 | at: Joi.string().required(), 24 | region: Joi.string().required(), 25 | deviceid: Joi.string().required(), 26 | }), 27 | }); 28 | 29 | /** 30 | * 31 | */ 32 | exports.GetDevicesValidator = celebrate({ 33 | [Segments.BODY]: Joi.object().keys({ 34 | at: Joi.string().required(), 35 | region: Joi.string().required(), 36 | }), 37 | }); 38 | -------------------------------------------------------------------------------- /client/src/app/json-pretty-dialog/json-pretty-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { JsonPrettyDialogComponent } from './json-pretty-dialog.component'; 4 | 5 | describe('JsonPrettyDialogComponent', () => { 6 | let component: JsonPrettyDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ JsonPrettyDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(JsonPrettyDialogComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-ewe-link", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start:dev": "nodemon src/index.js", 8 | "start": "node src/index.js", 9 | "docker:reset": "sh scripts/pull_spinup-dev.sh", 10 | "docker:start": "docker build . -t api-ewe-link && docker run -d -p 4006:4006 api-ewe-link" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "celebrate": "^13.0.4", 17 | "cors": "^2.8.5", 18 | "crypto-js": "^4.1.1", 19 | "ewelink-api": "3.1.1", 20 | "ewelink-api-fixed": "^3.1.1", 21 | "express": "^4.17.1", 22 | "morgan": "^1.9.1", 23 | "quick-encrypt": "^1.0.8", 24 | "write": "^2.0.0" 25 | }, 26 | "engines": { 27 | "node": "14" 28 | }, 29 | "devDependencies": { 30 | "nodemon": "^2.0.15" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/src/app/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { environment } from 'src/environments/environment'; 5 | import AuthLogin from '../models/authLogin'; 6 | import { DataLogin } from '../models/data_login'; 7 | import { tap } from 'rxjs/operators'; 8 | import { ResponseData } from '../models/response'; 9 | import * as CryptoJS from 'crypto-js'; 10 | 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class AuthService { 15 | 16 | constructor(private http: HttpClient) { } 17 | 18 | loginAuth(auth: AuthLogin): Observable> { 19 | return this.http.post>(environment.urlBase + '/login', auth).pipe( 20 | tap(res => localStorage.setItem('data', JSON.stringify(res.data))), 21 | ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /.github/dev-notes/research/index.md: -------------------------------------------------------------------------------- 1 | 2 | 1. socket timer example 3 | 4 | ````json 5 | { 6 | "action": "update", 7 | "deviceid": "10009aee9f", 8 | "apikey": "6613294f-2a51-4c0c-9a88-9c5329959d82", 9 | "userAgent": "app", 10 | "sequence": "1578951872889", 11 | "ts": 0, 12 | "params": { 13 | "timers": [ 14 | { 15 | "enabled": 1, 16 | "coolkit_timer_type": "repeat", 17 | "at": "0 7 * * 1,2,3,4,5,6,0", 18 | "type": "repeat", 19 | "do": { 20 | "switch": "off" 21 | }, 22 | "mId": "a6cdac3a-ab18-caeb-73f1-28f805f13d05" 23 | }, 24 | { 25 | "enabled": 1, 26 | "coolkit_timer_type": "delay", 27 | "at": "2020-01-13T23:37:00.171Z", 28 | "period": "120", 29 | "type": "once", 30 | "do": { 31 | "switch": "off" 32 | }, 33 | "mId": "e7f8d26f-f44a-f5e1-f8b6-d152880d80d6" 34 | } 35 | ] 36 | }, 37 | "tempRec": "10009aee9f" 38 | } 39 | 40 | ```` 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .vscode/ 4 | # See http://help.github.com/ignore-files/ for more about ignoring files. 5 | 6 | # compiled output 7 | /dist 8 | /tmp 9 | /out-tsc 10 | # Only exists if Bazel was run 11 | /bazel-out 12 | 13 | # dependencies 14 | /node_modules 15 | 16 | # profiling files 17 | chrome-profiler-events*.json 18 | speed-measure-plugin*.json 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | .history/* 36 | 37 | # misc 38 | /.sass-cache 39 | /connect.lock 40 | /coverage 41 | /libpeerconnection.log 42 | npm-debug.log 43 | yarn-error.log 44 | testem.log 45 | /typings 46 | 47 | # System Files 48 | .DS_Store 49 | Thumbs.db 50 | 51 | nodemon.json -------------------------------------------------------------------------------- /client/src/app/models/deviceInfo.ts: -------------------------------------------------------------------------------- 1 | export interface DeviceInfo { 2 | online: boolean; 3 | deviceid: string; 4 | name: string; 5 | type: string; 6 | params: DeviceInfoParams; 7 | brandName: string; 8 | showBrand: boolean; 9 | brandLogoUrl: string; 10 | productModel: string; 11 | } 12 | 13 | export interface DeviceInfoParams { 14 | version: number; 15 | sledOnline: string; 16 | switch: string; 17 | fwVersion: string; 18 | rssi: number; 19 | staMac: string; 20 | startup: string; 21 | init: number; 22 | pulse: string; 23 | pulseWidth: number; 24 | ssid: string; 25 | bssid: string; 26 | at: string; 27 | apiKey: string; 28 | deviceId: string; 29 | params: Params; 30 | } 31 | 32 | 33 | export interface Params { 34 | switch: string; 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /client/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatIconRegistry } from '@angular/material'; 3 | import { DomSanitizer } from '@angular/platform-browser'; 4 | import { ThemeService } from './services/theme.service'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'] 10 | }) 11 | export class AppComponent { 12 | 13 | 14 | constructor(private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer, private themeService: ThemeService) { 15 | 16 | this.themeService.initTheme(); 17 | 18 | this.matIconRegistry.addSvgIcon( 19 | "github", 20 | this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/github.svg') 21 | ); 22 | 23 | this.matIconRegistry.addSvgIcon( 24 | 'preview', 25 | this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/preview.svg') 26 | ); 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /client/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { ThemeEnum } from '../models/ewelink_enums'; 4 | import { SwitchService } from '../services/switch.service'; 5 | import { ThemeService } from '../services/theme.service'; 6 | 7 | @Component({ 8 | selector: 'app-home', 9 | templateUrl: './home.component.html', 10 | styleUrls: ['./home.component.scss'] 11 | }) 12 | export class HomeComponent implements OnInit, OnDestroy { 13 | 14 | public themeEnum = ThemeEnum; 15 | 16 | constructor(private router: Router, private switchService: SwitchService, public themeService: ThemeService) { } 17 | 18 | ngOnInit() { 19 | } 20 | 21 | 22 | LogOut() { 23 | localStorage.clear(); 24 | this.router.navigate(['/login']); 25 | } 26 | 27 | ngOnDestroy(): void { 28 | this.switchService.isNewState.unsubscribe(); 29 | } 30 | 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /client/src/app/models/user.ts: -------------------------------------------------------------------------------- 1 | import { ClientInfo } from './clientInfo'; 2 | 3 | export interface User { 4 | clientInfo: ClientInfo; 5 | _id: string; 6 | email: string; 7 | password: string; 8 | appId: string; 9 | countryCode: string; 10 | accountInfo: AccountInfo; 11 | apikey: string; 12 | createdAt: Date; 13 | extra: Extra; 14 | isAccepEmailAd: boolean; 15 | language: string; 16 | lang: string; 17 | currentFamilyId: string; 18 | appInfos: AppInfo[]; 19 | online: boolean; 20 | onlineTime: Date; 21 | ip: string; 22 | location: string; 23 | offlineTime: Date; 24 | } 25 | 26 | export interface AccountInfo { 27 | level: number; 28 | } 29 | 30 | export interface AppInfo { 31 | os: string; 32 | appVersion: string; 33 | } 34 | 35 | export interface Extra { 36 | lastEmailAdAt: Date; 37 | ipCountry: string; 38 | } 39 | -------------------------------------------------------------------------------- /client/src/app/models/wssPayload.ts: -------------------------------------------------------------------------------- 1 | import nonce from "nonce" 2 | import { DataLogin } from "src/app/models/data_login"; 3 | 4 | export const wssLoginPayload = (user: DataLogin) => { 5 | const timeStamp: any = new Date().getTime() / 1000; 6 | const ts = Math.floor(timeStamp); 7 | const sequence = Math.floor(timeStamp * 1000); 8 | const payload = { 9 | action: "userOnline", 10 | userAgent: "app", 11 | version: 8, 12 | nonce: `${nonce()}`, 13 | at: user.at, 14 | apikey: user.user.apikey, 15 | ts: `${ts}`, 16 | sequence 17 | }; 18 | return JSON.stringify(payload); 19 | }; 20 | 21 | export const wssUpdatePayload = ({ apikey, params, deviceid }) => { 22 | const timeStamp = (new Date()).getTime() / 1000; 23 | const sequence = Math.floor(timeStamp * 1000); 24 | const payload = { 25 | action: 'update', 26 | deviceid, 27 | apikey, 28 | selfApikey: apikey, 29 | params, 30 | sequence, 31 | userAgent: 'app', 32 | }; 33 | return JSON.stringify(payload); 34 | }; 35 | -------------------------------------------------------------------------------- /client/src/app/switch/switch.component.html: -------------------------------------------------------------------------------- 1 |
2 |
8 | 9 | 20 | 21 | 30 |
31 |
32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /client/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { AppComponent } from './app.component'; 4 | import { LoginGuard } from './guard/login.guard'; 5 | import { LogoutGuard } from './guard/logout.guard'; 6 | import { HomeComponent } from './home/home.component'; 7 | import { LoginComponent } from './login/login.component'; 8 | import { SwitchComponent } from './switch/switch.component' 9 | 10 | 11 | const routes: Routes = [ 12 | { path: 'login', component: LoginComponent, canActivate: [LogoutGuard] }, 13 | { 14 | path: 'home', 15 | component: HomeComponent, 16 | canActivate: [LoginGuard], 17 | children: [ 18 | { path: 'switch', component: SwitchComponent, canActivate: [LoginGuard] }, 19 | { path: '', redirectTo: 'switch', pathMatch: 'full' }, 20 | ] 21 | }, 22 | { path: '', redirectTo: 'login', pathMatch: 'full' }, 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [RouterModule.forRoot(routes)], 27 | exports: [RouterModule] 28 | }) 29 | export class AppRoutingModule { } 30 | -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | EweLink Web Tool 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Roberto Batty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # SwitchEwelinkClient 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.22. 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/src/app/material-definitions/material-definitions.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { 3 | MatButtonModule, 4 | MatCardModule, 5 | MatFormFieldModule, 6 | MatIconModule, 7 | MatInputModule, 8 | MatProgressSpinnerModule, 9 | MatSliderModule, 10 | MatToolbarModule, 11 | MatMenuModule, 12 | MatDialogModule, 13 | MatSlideToggleModule 14 | } from '@angular/material'; 15 | 16 | @NgModule({ 17 | imports: [ 18 | MatToolbarModule, 19 | MatMenuModule, 20 | MatCardModule, 21 | MatButtonModule, 22 | MatFormFieldModule, 23 | MatIconModule, 24 | MatInputModule, 25 | MatProgressSpinnerModule, 26 | MatIconModule, 27 | MatSliderModule, 28 | MatSlideToggleModule, 29 | MatDialogModule, 30 | ], 31 | exports: [ 32 | MatToolbarModule, 33 | MatMenuModule, 34 | MatCardModule, 35 | MatButtonModule, 36 | MatFormFieldModule, 37 | MatIconModule, 38 | MatInputModule, 39 | MatProgressSpinnerModule, 40 | MatIconModule, 41 | MatSliderModule, 42 | MatSlideToggleModule, 43 | MatDialogModule 44 | ] 45 | }) 46 | export class MaterialDefinitionsModule { } 47 | -------------------------------------------------------------------------------- /client/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/switch-ewelink-client'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 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 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /client/src/app/services/theme.service.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; 3 | import { ThemeEnum } from '../models/ewelink_enums'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class ThemeService { 9 | 10 | private renderer: Renderer2; 11 | private currentTheme: ThemeEnum = ThemeEnum.light; 12 | 13 | get isDarkMode(): boolean { 14 | return this.currentTheme === ThemeEnum.dark; 15 | } 16 | 17 | constructor( 18 | @Inject(DOCUMENT) private document: Document, 19 | rendererFactory: RendererFactory2 20 | ) { 21 | this.renderer = rendererFactory.createRenderer(null, null); 22 | } 23 | 24 | initTheme() { 25 | this.currentTheme = ThemeEnum.light === localStorage.getItem('activeTheme') ? ThemeEnum.light : ThemeEnum.dark; 26 | this.renderer.setAttribute(this.document.body, 'class', this.currentTheme); 27 | } 28 | 29 | switchMode() { 30 | this.currentTheme = this.isDarkMode ? ThemeEnum.light : ThemeEnum.dark; 31 | this.renderer.setAttribute(this.document.body, 'class', this.currentTheme); 32 | localStorage.setItem('activeTheme', this.currentTheme); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/docs/INSTALL.md: -------------------------------------------------------------------------------- 1 | 2 | # How to 3 | 4 | > Consider that this app is just an experimental tool, we're still research about the devices, feel free to add new features and send Pull Request to this repo if you want to. 5 | 6 | Instructions: 7 | 8 | ## Requirements 9 | 10 | You need whole NodeJS environment to try this out (I'm using NodeJS v14 right now, but this was build on Node v10, I recommend to use Node v14 anyway) 11 | 12 | 13 | ## folder: `./server` 14 | 15 | Locate the terminal inside the folder and run this commands: 16 | 17 | 1. Install NPM dependencies: 18 | ``` 19 | npm i 20 | ``` 21 | 22 | 2. Start the app 23 | ``` 24 | npm start 25 | ``` 26 | 27 | or using `nodemon` 28 | 29 | ``` 30 | npm start: dev 31 | ``` 32 | 33 | 3. go to the app once is loaded, `http://localhost:4231` 34 | 35 | 4. Routes 36 | 37 | - HTTP **POST** `/login` 38 | - HTTP **POST** `/device` 39 | - HTTP **POST** `/devices` 40 | 41 | 42 | 43 | 44 | ## folder: `./client` 45 | 46 | Locate the terminal inside the folder and run this commands: 47 | 48 | 1. Install NPM dependencies: 49 | ``` 50 | npm i 51 | ``` 52 | 53 | 2. Start the app 54 | ``` 55 | npm start 56 | ``` 57 | 58 | 3. go to the app once is loaded, `http://localhost:4200` 59 | -------------------------------------------------------------------------------- /client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'switch-ewelink-client'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app.title).toEqual('switch-ewelink-client'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('switch-ewelink-client app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /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 | urlBase: 'http://localhost:4006', 8 | menuLinks: [ 9 | { 10 | link: 'https://github.com/Rober19/ewelink-web-ui', 11 | icon: 'folder', 12 | text: 'App Repository', 13 | isSvg: false 14 | }, 15 | { 16 | link: 'https://github.com/Rober19', 17 | icon: 'github', 18 | text: 'Rober19', 19 | isSvg: true 20 | }, 21 | { 22 | link: 'https://github.com/DevelopGadget', 23 | icon: 'github', 24 | text: 'DevelopGadget', 25 | isSvg: true 26 | } 27 | ], 28 | urlWebSocket: (region = "us") => `wss://${region}-pconnect3.coolkit.cc:8080/api/ws`, 29 | alternativeIcon: 'https://static.thenounproject.com/png/252447-200.png' 30 | }; 31 | 32 | /* 33 | * For easier debugging in development mode, you can import the following file 34 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 35 | * 36 | * This import should be commented out in production mode because it will have a negative impact 37 | * on performance if an error is thrown. 38 | */ 39 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 40 | -------------------------------------------------------------------------------- /client/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
9 | 15 |
16 | ewelink 17 | EweLink Web Tool 18 |
19 |
20 | 21 | 22 | 25 | 26 | 34 | 38 | 39 |
40 |
41 |
42 | 43 |
44 | 45 |
46 |
47 | -------------------------------------------------------------------------------- /client/src/assets/preview.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/json-pretty-dialog/json-pretty-dialog.component.scss: -------------------------------------------------------------------------------- 1 | $type-colors: ( 2 | string: #ff2600ce, 3 | number: #06d4c0, 4 | boolean: #ff00d4, 5 | date: #0597d1, 6 | array: #999, 7 | object: #999, 8 | function: #999, 9 | "null": #fff, 10 | undefined: #fff, 11 | ); 12 | 13 | .darkJson { 14 | @each $type, $color in $type-colors { 15 | ::ng-deep .segment-type-#{$type} > .segment-main > .segment-value { 16 | color: $color !important; 17 | } 18 | ::ng-deep .segment-type-#{$type} > .segment-main > .segment-key { 19 | color: #2196f3 !important; 20 | } 21 | } 22 | } 23 | 24 | .not-display { 25 | display: none; 26 | } 27 | 28 | .preClass.darkJson { 29 | ::ng-deep .json-key { 30 | color: #2196f3; 31 | } 32 | ::ng-deep .json-string { 33 | color: #FF6B6B; 34 | } 35 | ::ng-deep .json-number { 36 | color: #009688; 37 | } 38 | ::ng-deep .json-boolean { 39 | color: #b938a4; 40 | } 41 | ::ng-deep .json-null { 42 | color: #fff; 43 | } 44 | ::ng-deep a.json-link { 45 | color: #FF6B6B; 46 | transition: all 400ms; 47 | } 48 | ::ng-deep a.json-link:visited { 49 | color: #FF6B6B 50 | } 51 | ::ng-deep a.json-link:hover { 52 | color: #c75555; 53 | } 54 | ::ng-deep a.json-link:active { 55 | color: slategray; 56 | } 57 | } 58 | 59 | .preClass { 60 | ::ng-deep .json-key { 61 | color: #4E187C; 62 | } 63 | ::ng-deep .json-string { 64 | color: #ff2600ce; 65 | } 66 | ::ng-deep .json-number { 67 | color: #06d4c0; 68 | } 69 | ::ng-deep .json-boolean { 70 | color: #ff00d4; 71 | } 72 | ::ng-deep .json-null { 73 | color: #fff; 74 | } 75 | ::ng-deep a.json-link { 76 | color: #ff2600ce; 77 | transition: all 400ms; 78 | } 79 | ::ng-deep a.json-link:visited { 80 | color: #ff2600ce; 81 | } 82 | ::ng-deep a.json-link:hover { 83 | color: #ff5e42ce; 84 | } 85 | ::ng-deep a.json-link:active { 86 | color: slategray; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /client/src/app/json-pretty-dialog/json-pretty-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; 2 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; 3 | import { DeviceInfo } from '../models/deviceInfo'; 4 | import { JSONEnum } from '../models/ewelink_enums'; 5 | import { ThemeService } from '../services/theme.service'; 6 | 7 | declare let prettyPrintJson: any; 8 | 9 | @Component({ 10 | selector: 'app-json-pretty-dialog', 11 | templateUrl: './json-pretty-dialog.component.html', 12 | styleUrls: ['./json-pretty-dialog.component.scss'] 13 | }) 14 | export class JsonPrettyDialogComponent implements OnInit { 15 | 16 | public colorClass = ''; 17 | @ViewChild('jsonPretty', { static: true }) jsonPre: ElementRef; 18 | public isExpanded: boolean = false; 19 | public isJsonWithExpanded: boolean = true; 20 | public expandedText: string = JSONEnum.expanded; 21 | public expandedIcon: string = JSONEnum.expandedIcon; 22 | 23 | constructor(@Inject(MAT_DIALOG_DATA) public deviceInfo: DeviceInfo, public dialogRef: MatDialogRef, public themeService: ThemeService) { 24 | if (this.themeService.isDarkMode) this.colorClass = 'darkJson'; 25 | } 26 | 27 | ngOnInit(): void { 28 | this.jsonPre.nativeElement.innerHTML = prettyPrintJson.toHtml(this.deviceInfo, { linkUrls: true }); 29 | this.jsonPre.nativeElement.classList.add('not-display'); 30 | this.jsonPre.nativeElement.classList.add(this.colorClass); 31 | } 32 | 33 | close() { 34 | this.dialogRef.close(); 35 | } 36 | 37 | changeExpanded() { 38 | this.isExpanded = !this.isExpanded; 39 | this.expandedText = this.isExpanded ? JSONEnum.collapse : JSONEnum.expanded; 40 | this.expandedIcon = this.isExpanded ? JSONEnum.collapseIcon : JSONEnum.expandedIcon; 41 | } 42 | 43 | changeView() { 44 | this.isJsonWithExpanded = !this.isJsonWithExpanded; 45 | if (this.isJsonWithExpanded) { 46 | this.jsonPre.nativeElement.classList.add('not-display'); 47 | }else { 48 | this.jsonPre.nativeElement.classList.remove('not-display'); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /client/src/app/services/switch.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { HttpClient } from "@angular/common/http"; 3 | import { environment } from "src/environments/environment"; 4 | import { DataLogin } from "../models/data_login"; 5 | import { catchError } from "rxjs/operators"; 6 | import { Subject, throwError } from "rxjs"; 7 | import { Device } from "../models/device"; 8 | import { ResponseData } from "../models/response"; 9 | import ChangeValue from "../models/change_value"; 10 | 11 | @Injectable({ 12 | providedIn: "root" 13 | }) 14 | export class SwitchService { 15 | 16 | public isNewState: Subject = new Subject(); 17 | 18 | constructor(private http: HttpClient) { } 19 | 20 | initSubject() { 21 | this.isNewState = new Subject(); 22 | } 23 | 24 | getDevices() { 25 | const userData = this.getAuth(); 26 | return this.http.post>(`${environment.urlBase}/devices`, { region: userData.region, at: userData.at }) 27 | .pipe(catchError(this.errorHandle)); 28 | } 29 | 30 | getDevice(deviceid: string) { 31 | const userData = this.getAuth(); 32 | return this.http 33 | .post>(`${environment.urlBase}/device`, { deviceid: deviceid, region: userData.region, at: userData.at }) 34 | .pipe(catchError(this.errorHandle)); 35 | } 36 | 37 | getCredentials() { 38 | return this.http.get(`${environment.urlBase}/ewecredentials`) 39 | .pipe(catchError(this.errorHandle)); 40 | } 41 | 42 | setDeviceStatus(state: any, deviceid: string) { 43 | return this.http 44 | .post(`${environment.urlBase}/setstatus?state=${state}`, { deviceid }) 45 | .pipe(catchError(this.errorHandle)); 46 | } 47 | 48 | toggleDevice(deviceid: string) { 49 | return this.http 50 | .post(`${environment.urlBase}/toggle`, { deviceid }) 51 | .pipe(catchError(this.errorHandle)); 52 | } 53 | 54 | getAuth(): DataLogin { 55 | return JSON.parse(localStorage.getItem('data')); 56 | } 57 | 58 | private errorHandle(res) { 59 | // localStorage.clear(); 60 | return throwError(res); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "switch-ewelink-client", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "start:dev": "ng serve", 9 | "build": "ng build", 10 | "test": "ng update @angular/core@10 @angular/cli@10 --force", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "~10.2.4", 17 | "@angular/cdk": "^8.2.3", 18 | "@angular/common": "~10.2.4", 19 | "@angular/compiler": "~10.2.4", 20 | "@angular/core": "~10.2.4", 21 | "@angular/flex-layout": "^11.0.0-beta.33", 22 | "@angular/forms": "~10.2.4", 23 | "@angular/material": "^8.2.3", 24 | "@angular/platform-browser": "~10.2.4", 25 | "@angular/platform-browser-dynamic": "~10.2.4", 26 | "@angular/router": "~10.2.4", 27 | "angular-notifier": "^6.0.1", 28 | "buffer": "^5.4.3", 29 | "compass-mixins": "^0.12.10", 30 | "crypto-js": "^4.1.1", 31 | "delay": "^4.3.0", 32 | "fs-extra": "^8.1.0", 33 | "hammerjs": "^2.0.8", 34 | "ngx-json-viewer": "^3.1.0", 35 | "nonce": "^1.0.4", 36 | "quick-encrypt": "^1.0.8", 37 | "rxjs": "~6.6.3", 38 | "tslib": "^2.0.0", 39 | "websocket": "^1.0.33", 40 | "websocket-as-promised": "^1.1.0", 41 | "zone.js": "~0.10.2" 42 | }, 43 | "devDependencies": { 44 | "@angular-devkit/build-angular": "~0.1002.1", 45 | "@angular/cli": "~10.2.1", 46 | "@angular/compiler-cli": "~10.2.4", 47 | "@angular/language-service": "~10.2.4", 48 | "@types/jasmine": "~3.3.8", 49 | "@types/jasminewd2": "~2.0.3", 50 | "@types/node": "^12.11.1", 51 | "codelyzer": "^5.1.2", 52 | "jasmine-core": "~3.5.0", 53 | "jasmine-spec-reporter": "~5.0.0", 54 | "karma": "~5.0.0", 55 | "karma-chrome-launcher": "~3.1.0", 56 | "karma-coverage-istanbul-reporter": "~3.0.2", 57 | "karma-jasmine": "~4.0.0", 58 | "karma-jasmine-html-reporter": "^1.5.0", 59 | "protractor": "~7.0.0", 60 | "ts-node": "~7.0.0", 61 | "tslint": "~6.1.0", 62 | "typescript": "~4.0.5" 63 | }, 64 | "browser": { 65 | "fs": false, 66 | "path": false, 67 | "net": false, 68 | "tls": false, 69 | "os": false 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /client/src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | ewelink 7 | EweLink Web Tool 8 |
9 |
10 | 11 | 15 |
16 |
17 |
18 | 19 |
20 | 21 | 22 | Login 23 | 24 | 25 |

( Ewelink credentials )

26 |
28 | 29 | Email 30 | 31 | alternate_email 32 | 33 | 34 | Password 35 | 36 | vpn_key 37 | 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 |
48 | -------------------------------------------------------------------------------- /client/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from "@angular/platform-browser"; 2 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; 3 | 4 | import { AppRoutingModule } from "./app-routing.module"; 5 | import { AppComponent } from "./app.component"; 6 | import { SwitchComponent } from "./switch/switch.component"; 7 | import { HttpClientModule } from "@angular/common/http"; 8 | import { LoginComponent } from './login/login.component'; 9 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 10 | import { BreakPointRegistry, FlexFillStyleBuilder, FlexLayoutModule, FlexOrderStyleBuilder, FlexStyleBuilder, LayoutAlignStyleBuilder, LayoutGapStyleBuilder, LayoutStyleBuilder, MediaMarshaller, PrintHook, ShowHideStyleBuilder, StylesheetMap, StyleUtils, ɵMatchMedia } from "@angular/flex-layout"; 11 | import { ReactiveFormsModule } from "@angular/forms"; 12 | import { NotifierModule } from "angular-notifier"; 13 | import { HomeComponent } from './home/home.component'; 14 | import { DeviceComponent } from './device/device.component'; 15 | import { CommonModule } from "@angular/common"; 16 | import { MaterialDefinitionsModule } from "./material-definitions/material-definitions.module"; 17 | import { MenuInfoComponent } from './menu-info/menu-info.component'; 18 | import { JsonPrettyDialogComponent } from './json-pretty-dialog/json-pretty-dialog.component'; 19 | // import { NgxJsonViewerModule } from "ngx-json-viewer"; 20 | 21 | @NgModule({ 22 | declarations: [ 23 | AppComponent, 24 | SwitchComponent, 25 | LoginComponent, 26 | HomeComponent, 27 | DeviceComponent, 28 | MenuInfoComponent, 29 | JsonPrettyDialogComponent, 30 | ], 31 | entryComponents: [JsonPrettyDialogComponent], 32 | imports: [ 33 | BrowserModule, 34 | CommonModule, 35 | AppRoutingModule, 36 | HttpClientModule, 37 | BrowserAnimationsModule, 38 | FlexLayoutModule, 39 | ReactiveFormsModule, 40 | MaterialDefinitionsModule, 41 | // NgxJsonViewerModule, 42 | NotifierModule.withConfig({ 43 | position: { vertical: { position: 'top', distance: 74 }, horizontal: { position: "right" } } 44 | }) 45 | ], 46 | providers: [ 47 | StyleUtils, 48 | StylesheetMap, 49 | MediaMarshaller, 50 | ɵMatchMedia, 51 | BreakPointRegistry, 52 | PrintHook, 53 | LayoutStyleBuilder, 54 | FlexStyleBuilder, 55 | ShowHideStyleBuilder, 56 | FlexOrderStyleBuilder, 57 | FlexFillStyleBuilder, 58 | LayoutGapStyleBuilder, 59 | LayoutAlignStyleBuilder 60 | ], 61 | bootstrap: [AppComponent], 62 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 63 | }) 64 | export class AppModule { } 65 | -------------------------------------------------------------------------------- /client/src/app/services/socket.service.ts: -------------------------------------------------------------------------------- 1 | import * as websocket from "websocket"; 2 | import WSAP from "websocket-as-promised"; 3 | import { DataLogin } from "../models/data_login"; 4 | import { environment } from "src/environments/environment"; 5 | import { StateEnum } from "../models/ewelink_enums"; 6 | import { wssLoginPayload, wssUpdatePayload } from "../models/wssPayload"; 7 | import { Injectable } from "@angular/core"; 8 | import { SwitchService } from "./switch.service"; 9 | 10 | @Injectable({ 11 | providedIn: "root" 12 | }) 13 | export class SocketService { 14 | 15 | private W3CWebSocket = websocket.w3cwebsocket; 16 | private WebSocket: WSAP; 17 | 18 | constructor(private switchService: SwitchService) { 19 | 20 | } 21 | 22 | /** 23 | * Open a socket connection to eWeLink 24 | * and execute callback function with server message as argument 25 | * 26 | * @param callback 27 | * @param heartbeat 28 | */ 29 | async openWebSocket() { 30 | 31 | const user = this.switchService.getAuth() 32 | 33 | const payloadLogin = wssLoginPayload(user); 34 | 35 | this.WebSocket = new WSAP(environment.urlWebSocket(user.region), { 36 | createWebSocket: wss => new this.W3CWebSocket(wss) 37 | }); 38 | 39 | this.WebSocket.onMessage.addListener(message => { 40 | // console.log(message) 41 | try { 42 | // 43 | const data = JSON.parse(message); 44 | // 45 | if (data.params) { 46 | // 47 | // console.log(data) 48 | // 49 | if (data?.params?.switch || data?.params?.switches) 50 | this.switchService.isNewState.next({ 51 | deviceid: data.deviceid, 52 | params: data.params 53 | }); 54 | } else 55 | if (data.deviceid && !data.error) { 56 | // 57 | this.switchService.isNewState.next({ 58 | deviceid: data.deviceid, 59 | error: Boolean(data?.error) 60 | }); 61 | } 62 | } catch (error) { 63 | console.log({ error, message }); 64 | } 65 | }); 66 | 67 | await this.WebSocket.open(); 68 | this.WebSocket.send(payloadLogin); 69 | 70 | setInterval(async () => { 71 | this.WebSocket.send("ping"); 72 | }, 120000); 73 | 74 | return 'done'; 75 | } 76 | 77 | sendMessageWebSocket({ deviceid, params }: any) { 78 | // 79 | const { user } = this.switchService.getAuth() 80 | 81 | if (!user) return; 82 | 83 | console.log(params) 84 | // 85 | const payload = wssUpdatePayload({ apikey: user.apikey, params, deviceid }); 86 | 87 | console.log(payload) 88 | 89 | this.WebSocket.send(payload); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /client/src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { Router, ActivatedRoute } from '@angular/router'; 4 | import { NotifierService } from 'angular-notifier'; 5 | import { ThemeEnum } from '../models/ewelink_enums'; 6 | import { queryDataToSetState } from '../models/setStateWithURL'; 7 | import { AuthService } from '../services/auth.service'; 8 | import { SocketService } from '../services/socket.service'; 9 | import { ThemeService } from '../services/theme.service'; 10 | 11 | @Component({ 12 | selector: 'app-login', 13 | templateUrl: './login.component.html', 14 | styleUrls: ['./login.component.scss'] 15 | }) 16 | export class LoginComponent implements OnInit { 17 | 18 | public isLoading: boolean = false; 19 | public themeEnum = ThemeEnum; 20 | 21 | public formLogin: FormGroup = new FormGroup({ 22 | email: new FormControl('', [Validators.required, Validators.email]), 23 | password: new FormControl('', [Validators.required]), 24 | }); 25 | 26 | 27 | constructor(public notifierService: NotifierService, public authService: AuthService, public router: Router, public activeRoute: ActivatedRoute, public themeService: ThemeService) { 28 | } 29 | 30 | ngOnInit(): void { 31 | this.test(); 32 | } 33 | 34 | test() { 35 | this.activeRoute.queryParams.subscribe((res: queryDataToSetState) => { 36 | 37 | /** 38 | * deviceid 39 | * state 40 | * email 41 | * password 42 | */ 43 | const { email, password, deviceid, state } = res; 44 | 45 | 46 | if (email && password && deviceid && state) { 47 | this.authService.loginAuth({ email, password }).subscribe(async res => { 48 | this.router.navigate(['/home'], { 49 | queryParams: { 50 | deviceid, 51 | state 52 | } 53 | }); 54 | // this.eventService.emit('SET_STATE_EVENT', { deviceid, params: { newstate: state } }) 55 | }, err => { 56 | // this.notifierService.notify('error', 'Unauthorized'); 57 | // this.isLoading = false; 58 | }) 59 | console.log('response', { res }) 60 | } 61 | }) 62 | } 63 | 64 | LoginApp() { 65 | if (this.formLogin.valid) { 66 | this.isLoading = true; 67 | this.authService.loginAuth(this.formLogin.value).subscribe(res => { 68 | this.router.navigate(['/home']); 69 | this.isLoading = false; 70 | }, err => { 71 | this.notifierService.notify('error', 'Something Wrong'); 72 | this.isLoading = false; 73 | }) 74 | } else { 75 | this.notifierService.notify('error', 'Is not valid input'); 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # Ewelink Web App 2 | 3 | 4 | 5 | 6 | I am pleased to show you the simplest web tool for ewelink. 7 | 8 | ![image-20210111000940316](assets/image-20210111000940316.png) 9 | > Full open-source project 10 | 11 | # Instructions 12 | 13 | Go to the [Guide](docs/INSTALL.md): `docs/INSTALL.md` 14 | 15 | # Privacy 16 | 17 | The only use that we give to the credentials (email / password) is to obtain access from __ewelink__ 18 | 19 | ```js 20 | const { email, password } = req.body; 21 | 22 | const connection = new ewelink({ 23 | email, 24 | password, 25 | }); 26 | 27 | const data = await connection.getCredentials(); 28 | ``` 29 | 30 | ( email / password are data for 1 use only, they are not stored for anything ) 31 | 32 | 33 | This is what the access data for the Ewelink account and devices looks like 34 | 35 | ```JSON 36 | { 37 | "at": "*************", 38 | "rt": "*************", 39 | "user": { 40 | "clientInfo": { 41 | "model": "iPhone 11 Pro_iPhone12,3", 42 | "os": "iOS", 43 | "imei": "*************", 44 | "romVersion": "14.3", 45 | "appVersion": "4.8.1" 46 | }, 47 | "_id": "*************", 48 | "email": "usertest@gmail.com", 49 | "password": "*************", 50 | "appId": "*************", 51 | "isAccepEmailAd": false, 52 | "createdAt": "2019-08-01T22:25:08.703Z", 53 | "apikey": "*************", 54 | "__v": 0, 55 | "lang": "en", 56 | "online": false, 57 | "onlineTime": "2021-01-10T23:15:14.852Z", 58 | "appInfos": [ 59 | { "os": "ios", "appVersion": "" }, 60 | { "os": "android", "appVersion": "4.6.0" } 61 | ], 62 | "ip": "179.13.51.15", 63 | "location": "", 64 | "offlineTime": "2021-01-10T23:16:21.459Z", 65 | "bindInfos": { "gaction": ["ewelink-google-home-v1", "ewelinkGoogleHome"] }, 66 | "userStatus": "2", 67 | "nickname": "usertest", 68 | "countryCode": "+86", 69 | "currentFamilyId": "*************", 70 | "language": "en", 71 | "extra": { "ipCountry": "CO" } 72 | }, 73 | "region": "us" 74 | } 75 | 76 | ``` 77 | 78 | Once the access JSON is obtained, we use the data obtained `at`,` region`, etc... 79 | 80 | ```js 81 | const connection = new ewelink({ 82 | at, 83 | region, 84 | }); 85 | ``` 86 | 87 | **References**: 88 | 89 | > [eWelink API](https://ewelink-api.now.sh/docs/introduction) 90 | 91 | # Feedback 92 | 93 | Feel free to make suggestions or feedback through github issues or [Reddit](https://www.reddit.com/r/Echo_dev/) -------------------------------------------------------------------------------- /client/src/assets/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /client/src/app/device/device.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles.scss"; 2 | 3 | mat-card-title > .img { 4 | @include png-icon(70%); 5 | } 6 | 7 | .darkModeContrast { 8 | filter: contrast(100); 9 | } 10 | 11 | @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro); 12 | 13 | .containerIcon { 14 | position: absolute; 15 | right: 10px; 16 | top: 8px; 17 | cursor: pointer; 18 | } 19 | 20 | .mat-card { 21 | border-radius: 10px; 22 | z-index: 1; 23 | } 24 | 25 | .device { 26 | padding: 10px; 27 | } 28 | 29 | .no-click { 30 | pointer-events: none; 31 | } 32 | 33 | .it-click { 34 | // pointer-events: fill; 35 | cursor: pointer; 36 | } 37 | 38 | .no-click-switch { 39 | pointer-events: none; 40 | opacity: 0.2; 41 | } 42 | 43 | .switch { 44 | display: block; 45 | position: relative; 46 | width: 55px; 47 | height: 80px; 48 | border-radius: 50px; 49 | 50 | background: #dae5e6; 51 | background: linear-gradient(#fdfbf6, rgb(245, 223, 223)); 52 | border: 1px solid rgba(0,0,0,0.1); 53 | 54 | box-shadow: 55 | inset 0 7px 0 #fdfdfd, 56 | 0 2px 3px rgba(170, 160, 140,.3); 57 | 58 | cursor: pointer; 59 | } 60 | 61 | .switch:before { 62 | content: ""; 63 | position: absolute; 64 | top: -10px; bottom: -10px; 65 | left: -5px; right: -5px; 66 | z-index: -1; 67 | 68 | background: #f2f1ed; 69 | border-radius: inherit; 70 | 71 | box-shadow: 72 | 0 1px 1px rgba(#8aa7be,0.2), 73 | 0 3px 3px rgba(158, 142, 111, 0.4), 74 | inset 0 1px 0 rgba(255,255,255,0.8), 75 | 0 0 5px rgba(170, 160, 140, 0.5); 76 | } 77 | 78 | .switch:after { 79 | content: ""; 80 | position:absolute; 81 | width: 55px; 82 | height: 80px; 83 | border-radius: 50%; 84 | z-index: -1; 85 | left: 5px; 86 | top: 5px; 87 | background: linear-gradient(160deg, rgba(170, 160, 140, 0.7), rgba(170, 160, 140, 0)); 88 | background: -webkit-linear-gradient(290deg, rgba(170, 160, 140, 0.75), rgba(170, 160, 140, 0)); 89 | 90 | -webkit-filter: blur(1px); 91 | } 92 | 93 | .checkbox { 94 | clip: rect(0 0 0 0); 95 | position: absolute; 96 | visibility: hidden; 97 | width: 55px; 98 | height: 80px; 99 | } 100 | 101 | .checkbox:checked ~ .switch { 102 | background: linear-gradient(#daeefd, rgb(255, 255, 255)); 103 | box-shadow: 104 | inset 0 -5px 0 #dbd3c8, 105 | 0 6px 5px rgba(170, 160, 140, 0.75), 106 | 3px 16px 5px rgba(170,160,140, 0.3); 107 | border-bottom: none; 108 | } 109 | 110 | .checkbox:checked ~ .switch:after { 111 | display: none; 112 | } 113 | 114 | .pointer { 115 | cursor: pointer; 116 | } 117 | 118 | a:link { 119 | color: green; 120 | background-color: transparent; 121 | text-decoration: none; 122 | } 123 | 124 | a:visited { 125 | color: pink; 126 | background-color: transparent; 127 | text-decoration: none; 128 | } 129 | 130 | a:hover { 131 | color: red; 132 | background-color: transparent; 133 | text-decoration: underline; 134 | } 135 | 136 | a:active { 137 | color: yellow; 138 | background-color: transparent; 139 | text-decoration: underline; 140 | } -------------------------------------------------------------------------------- /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/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | (window as any).global = window; 61 | // @ts-ignore 62 | window.Buffer = window.Buffer || require('buffer').Buffer; 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /client/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~compass-mixins/lib/compass"; 2 | 3 | @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro); 4 | 5 | @import url("https://fonts.googleapis.com/css2?family=Raleway:wght@400&display=swap"); 6 | 7 | // html, 8 | // body { 9 | // // height: 100%; 10 | // } 11 | 12 | body { 13 | margin: 0; 14 | background: #edece7; 15 | } 16 | 17 | @mixin md-icon-size($size: 24px) { 18 | font-size: $size !important; 19 | height: $size !important; 20 | width: $size !important; 21 | line-height: $size !important; 22 | } 23 | 24 | .width { 25 | width: 100%; 26 | } 27 | 28 | * { 29 | font-family: "Raleway", sans-serif; 30 | } 31 | 32 | @import "~angular-notifier/styles"; 33 | 34 | @import "~@angular/material/theming"; 35 | @include mat-core(); 36 | 37 | @mixin png-icon($sizeW: 24px) { 38 | margin: 5px auto auto; 39 | padding: 0; 40 | display: inline-block; 41 | background-repeat: no-repeat no-repeat; 42 | pointer-events: none; 43 | width: $sizeW; 44 | height: auto; 45 | } 46 | 47 | mat-toolbar > div > .img { 48 | @include png-icon(35px); 49 | margin: 0px; 50 | } 51 | 52 | body.darkMode { 53 | background-color: #535353 !important; 54 | -webkit-transition: background-color 300ms linear; 55 | -ms-transition: background-color 300ms linear; 56 | transition: background-color 300ms linear; 57 | $theming-material-components-primary: mat-palette($mat-blue); 58 | $theming-material-components-accent: mat-palette($mat-pink, A200, A100, A400); 59 | $theming-material-components-warn: mat-palette($mat-red); 60 | $theming-material-components-theme: mat-dark-theme( 61 | $theming-material-components-primary, 62 | $theming-material-components-accent, 63 | $theming-material-components-warn 64 | ); 65 | @include angular-material-theme($theming-material-components-theme); 66 | } 67 | 68 | body.lightMode { 69 | background-color: #edece7; 70 | -webkit-transition: background-color 300ms linear; 71 | -ms-transition: background-color 300ms linear; 72 | transition: background-color 300ms linear; 73 | $theming-material-components-primary: mat-palette($mat-blue, A200, A100, A900); 74 | $theming-material-components-accent: mat-palette($mat-pink, A200, A100, A900); 75 | $theming-material-components-warn: mat-palette($mat-red, A200, A100, A900); 76 | $theming-material-components-theme: mat-light-theme( 77 | $theming-material-components-primary, 78 | $theming-material-components-accent, 79 | $theming-material-components-warn 80 | ); 81 | @include angular-material-theme($theming-material-components-theme); 82 | } 83 | 84 | ::-webkit-scrollbar-track 85 | { 86 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 87 | background-color: #F5F5F5; 88 | border-radius: 10px; 89 | } 90 | 91 | ::-webkit-scrollbar 92 | { 93 | width: 10px; 94 | background-color: #F5F5F5; 95 | border-radius: 10px; 96 | } 97 | 98 | ::-webkit-scrollbar-thumb 99 | { 100 | border-radius: 10px; 101 | background-image: -webkit-gradient(linear, 102 | left bottom, 103 | left top, 104 | color-stop(0.44, rgb(122,153,217)), 105 | color-stop(0.72, rgb(73,125,189)), 106 | color-stop(0.86, rgb(28,58,148))); 107 | } 108 | 109 | .dialog-responsive { 110 | max-width: 40% !important; 111 | } 112 | 113 | @media only screen and (min-width: 761px) and (max-width: 1380px) { 114 | .dialog-responsive { 115 | max-width: 70% !important; 116 | } 117 | } 118 | 119 | @media only screen and (max-width: 760px) { 120 | .dialog-responsive { 121 | max-width: 90% !important; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /client/src/app/device/device.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 | 8 | 9 | brand logo 12 | 13 | 14 | 15 | 17 | {{ device.name }} 18 |
19 |
21 | {{ 22 | labelState }} 23 | 24 |
25 |
26 | 27 | 28 |
29 |

{{ item.name }}

30 |
32 | 33 | {{ item.switch }} 34 | 35 |
36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 | 45 | 46 |
47 |
48 | 49 |
50 |
51 | 53 | Brand: {{ device?.brandName }} 54 |
55 |

Device Tag: {{ device?.name }}

56 |
57 | Product Model: {{ device?.productModel }} 58 |
59 | We are having problems with your device, please contact us to 60 | improve and fix it. this is a open source app for the community. 61 |
62 |
63 | Feel free to open a ticket for solve the issue here: 64 | Github 65 | Repository 66 |
67 |
68 |
69 |
70 | 71 |
72 | 73 |
74 | 75 |
76 | -------------------------------------------------------------------------------- /client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "align": { 5 | "options": [ 6 | "parameters", 7 | "statements" 8 | ] 9 | }, 10 | "array-type": false, 11 | "arrow-parens": false, 12 | "arrow-return-shorthand": true, 13 | "deprecation": { 14 | "severity": "warning" 15 | }, 16 | "component-class-suffix": true, 17 | "contextual-lifecycle": true, 18 | "curly": true, 19 | "directive-class-suffix": true, 20 | "directive-selector": [ 21 | true, 22 | "attribute", 23 | "app", 24 | "camelCase" 25 | ], 26 | "component-selector": [ 27 | true, 28 | "element", 29 | "app", 30 | "kebab-case" 31 | ], 32 | "eofline": true, 33 | "import-blacklist": [ 34 | true, 35 | "rxjs/Rx" 36 | ], 37 | "import-spacing": true, 38 | "indent": { 39 | "options": [ 40 | "spaces" 41 | ] 42 | }, 43 | "interface-name": false, 44 | "max-classes-per-file": false, 45 | "max-line-length": [ 46 | true, 47 | 140 48 | ], 49 | "member-access": false, 50 | "member-ordering": [ 51 | true, 52 | { 53 | "order": [ 54 | "static-field", 55 | "instance-field", 56 | "static-method", 57 | "instance-method" 58 | ] 59 | } 60 | ], 61 | "no-consecutive-blank-lines": false, 62 | "no-console": [ 63 | true, 64 | "debug", 65 | "info", 66 | "time", 67 | "timeEnd", 68 | "trace" 69 | ], 70 | "no-empty": false, 71 | "no-inferrable-types": [ 72 | true, 73 | "ignore-params" 74 | ], 75 | "no-non-null-assertion": true, 76 | "no-redundant-jsdoc": true, 77 | "no-switch-case-fall-through": true, 78 | "no-var-requires": false, 79 | "object-literal-key-quotes": [ 80 | true, 81 | "as-needed" 82 | ], 83 | "object-literal-sort-keys": false, 84 | "ordered-imports": false, 85 | "quotemark": [ 86 | true, 87 | "single" 88 | ], 89 | "trailing-comma": false, 90 | "no-conflicting-lifecycle": true, 91 | "no-host-metadata-property": true, 92 | "no-input-rename": true, 93 | "no-inputs-metadata-property": true, 94 | "no-output-native": true, 95 | "no-output-on-prefix": true, 96 | "no-output-rename": true, 97 | "semicolon": { 98 | "options": [ 99 | "always" 100 | ] 101 | }, 102 | "space-before-function-paren": { 103 | "options": { 104 | "anonymous": "never", 105 | "asyncArrow": "always", 106 | "constructor": "never", 107 | "method": "never", 108 | "named": "never" 109 | } 110 | }, 111 | "no-outputs-metadata-property": true, 112 | "template-banana-in-box": true, 113 | "template-no-negated-async": true, 114 | "typedef-whitespace": { 115 | "options": [ 116 | { 117 | "call-signature": "nospace", 118 | "index-signature": "nospace", 119 | "parameter": "nospace", 120 | "property-declaration": "nospace", 121 | "variable-declaration": "nospace" 122 | }, 123 | { 124 | "call-signature": "onespace", 125 | "index-signature": "onespace", 126 | "parameter": "onespace", 127 | "property-declaration": "onespace", 128 | "variable-declaration": "onespace" 129 | } 130 | ] 131 | }, 132 | "use-lifecycle-interface": true, 133 | "use-pipe-transform-interface": true, 134 | "variable-name": { 135 | "options": [ 136 | "ban-keywords", 137 | "check-format", 138 | "allow-pascal-case" 139 | ] 140 | }, 141 | "whitespace": { 142 | "options": [ 143 | "check-branch", 144 | "check-decl", 145 | "check-operator", 146 | "check-separator", 147 | "check-type", 148 | "check-typecast" 149 | ] 150 | } 151 | }, 152 | "rulesDirectory": [ 153 | "codelyzer" 154 | ] 155 | } -------------------------------------------------------------------------------- /client/src/app/switch/switch.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { SwitchService, } from '../services/switch.service'; 3 | import { ActivatedRoute, Data, Router } from '@angular/router'; 4 | import { NotifierService } from 'angular-notifier'; 5 | import { Device } from '../models/device'; 6 | import { StateEnum } from '../models/ewelink_enums'; 7 | import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; 8 | import { SocketService } from '../services/socket.service'; 9 | import { EventService, EventType } from '../services/event.service'; 10 | 11 | @Component({ 12 | selector: 'app-switch', 13 | templateUrl: './switch.component.html', 14 | styleUrls: ['./switch.component.scss'], 15 | host: { class: 'width' } 16 | }) 17 | export class SwitchComponent implements OnInit { 18 | 19 | public devices: Device[] = []; 20 | public devicesRaw: any[] = []; 21 | public devicesGroup: FormGroup; 22 | 23 | @ViewChild("customNotification", { static: true }) customNotificationTmpl; 24 | 25 | constructor( 26 | public switchService: SwitchService, 27 | private router: Router, 28 | private notifier: NotifierService, 29 | private fb: FormBuilder, 30 | private socketService: SocketService, 31 | private eventService: EventService, 32 | public activeRoute: ActivatedRoute, 33 | ) { 34 | this.devicesGroup = this.fb.group({ 35 | checkControls: this.fb.array([]) 36 | }); 37 | } 38 | 39 | ngOnInit() { 40 | this.switchService.initSubject(); 41 | this.getAllDevices(); 42 | this.notifier.show({ 43 | message: "Loading", 44 | type: "warining", 45 | template: this.customNotificationTmpl, 46 | id: 'loading' 47 | }); 48 | // this.listenEvents(); 49 | } 50 | 51 | listenEvents() { 52 | this.eventService.listen().subscribe(res => { 53 | switch (res.type as EventType) { 54 | case 'LISTEN_STATE_CHANNEL': 55 | // console.log('recibido LISTEN_STATE_CHANNEL', res) 56 | this.devices = this.devices.map(device_item => { 57 | if (device_item.deviceid === res.data.deviceid) { 58 | // this.devices.push(device_item) 59 | // Object.assign(device_item, { 60 | // ...res.data 61 | // }) 62 | } 63 | return device_item; 64 | }) 65 | // console.log(this.devices) 66 | break; 67 | case 'SET_STATE_EVENT': 68 | this.changeState(res.data) 69 | break; 70 | } 71 | }); 72 | } 73 | 74 | //this method is for changing the device state from button 75 | async changeState({ deviceid, params }) { 76 | try { 77 | console.log('changeState ', { deviceid, params }) 78 | // 79 | this.socketService.sendMessageWebSocket({ 80 | deviceid, 81 | params 82 | }); 83 | 84 | } catch (error) { 85 | console.log('changeState', error) 86 | } 87 | } 88 | 89 | // this method get all devices data 90 | async getAllDevices() { 91 | 92 | const self = this; 93 | 94 | this.switchService.getDevices().subscribe(async (res: any) => { 95 | 96 | this.devices = res.data; 97 | if (!res?.data?.length) { 98 | this.devicesRaw = res.devicesRaw; 99 | console.log(this.devicesRaw) 100 | } 101 | console.log(this.devices) 102 | 103 | this.devicesControl.patchValue( 104 | Array.from( 105 | res.data, 106 | (v, k) => this.devicesControl.push( 107 | new FormControl(false) 108 | ) 109 | ) 110 | ); 111 | await this.socketService.openWebSocket(); 112 | this.notifier.hide('loading'); 113 | 114 | 115 | }, err => { 116 | this.notifier.hide('loading'); 117 | console.log(err); 118 | localStorage.clear(); 119 | this.router.navigate(['/login']); 120 | }); 121 | test() 122 | 123 | function test() { 124 | self.activeRoute.queryParams.subscribe((res: any) => { 125 | /** 126 | * deviceid 127 | * state 128 | */ 129 | const { deviceid, state } = res; 130 | 131 | if (deviceid && state) { 132 | console.log('LIST', self.devices) 133 | 134 | setTimeout(() => { 135 | self.changeState({ 136 | deviceid, 137 | params: 138 | { 139 | switch: state 140 | } 141 | }) 142 | }, 3000); 143 | 144 | } 145 | 146 | }, err => { 147 | // this.notifierService.notify('error', 'Unauthorized'); 148 | // this.isLoading = false; 149 | console.log('BAD LIST') 150 | }) 151 | 152 | } 153 | 154 | 155 | } 156 | 157 | get devicesControl(): FormArray { 158 | return this.devicesGroup.get('checkControls') as FormArray 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /server/src/components/ewelink/ewelink.controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * PACKAGES 4 | * 5 | */ 6 | const ewelink = require('ewelink-api-fixed'); 7 | const CryptoJS = require("crypto-js"); 8 | 9 | const app_keys = { 10 | // APP_ID: 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq', 11 | // APP_SECRET: '6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM', 12 | }; 13 | 14 | /** 15 | * 16 | * @param {*} req 17 | * @param {*} res 18 | */ 19 | exports.Login = async (req, res) => { 20 | try { 21 | const { email, password } = req.body; 22 | 23 | const connection = new ewelink({ 24 | email, 25 | password, 26 | ...app_keys, 27 | }); 28 | 29 | const data = await connection.getCredentials(); 30 | 31 | if (Number.isInteger(data?.error)) throw data; 32 | 33 | res.send({ status: 200, error: false, data }); 34 | } catch (error) { 35 | res.status(400).send({ status: 400, error: true, data: error }); 36 | } 37 | }; 38 | 39 | /** 40 | * 41 | * @param {*} req 42 | * @param {*} res 43 | */ 44 | exports.GetDevice = async (req, res) => { 45 | try { 46 | const { at, region, deviceid } = req.body; 47 | if (deviceid) { 48 | const connection = new ewelink({ 49 | at, 50 | region, 51 | ...app_keys, 52 | }); 53 | 54 | const status = await connection.getDevice(deviceid); 55 | // console.log(status); 56 | 57 | res.send({ status: 200, error: false, data: status }); 58 | } else { 59 | res.status(400).send({ 60 | status: 400, 61 | error: true, 62 | data: { 63 | body: { 64 | deviceid, 65 | }, 66 | }, 67 | }); 68 | } 69 | } catch (error) { 70 | res.status(400).send({ status: 400, error: true, data: error }); 71 | } 72 | }; 73 | 74 | /** 75 | * 76 | * @param {*} req 77 | * @param {*} res 78 | */ 79 | exports.GetDevices = async (req, res) => { 80 | try { 81 | const { at, region } = req.body; 82 | // 83 | const connection = new ewelink({ 84 | at, 85 | region, 86 | ...app_keys, 87 | }); 88 | // 89 | const devices = await connection.getDevices(); 90 | // 91 | let report = []; 92 | // 93 | try { 94 | for (const device of devices) { 95 | let deviceToAdd = { 96 | name: device.name, 97 | deviceid: device.deviceid, 98 | deviceInfo: device, 99 | online: device.online, 100 | request: false, // used in socket wsp 101 | showBrand: device.showBrand, 102 | }; 103 | // 104 | if (device?.tags?.ck_channel_name) { 105 | let deviceChannels = []; 106 | // [{ '0': '', '1': '' }] 107 | // console.log(device.tags.ck_channel_name) 108 | Object.values(device.tags.ck_channel_name).forEach((channel, index) => { 109 | // console.log(channel) 110 | deviceChannels.push({ 111 | name: channel, 112 | parentName: device?.name, 113 | parentDeviceId: device?.deviceid, 114 | channel: index, 115 | switch: device?.params?.switches[index].switch, 116 | state: device?.params?.switches[index].switch === 'on', 117 | }); 118 | }); 119 | deviceToAdd = { 120 | ...deviceToAdd, 121 | isMultipleChannelDevice: true, 122 | deviceChannels, 123 | }; 124 | } else if (device?.params?.switches?.length) { 125 | let deviceChannels = []; 126 | // 127 | Object.values(device?.params?.switches).forEach((channel, index) => { 128 | deviceChannels.push({ 129 | name: index, 130 | parentName: device?.name, 131 | parentDeviceId: device?.deviceid, 132 | channel: index, 133 | switch: device.params.switches[index].switch, 134 | state: device.params.switches[index].switch === 'on', 135 | }); 136 | }); 137 | deviceToAdd = { 138 | ...deviceToAdd, 139 | isMultipleChannelDevice: true, 140 | deviceChannels, 141 | }; 142 | } else { 143 | deviceToAdd = { 144 | ...deviceToAdd, 145 | isMultipleChannelDevice: false, 146 | switch: device.params.switch, 147 | // Device is turned off, it's offline 148 | state: !device.online ? false : device.params.switch === 'on', 149 | }; 150 | } 151 | // 152 | // console.log(deviceToAdd) 153 | report.push(deviceToAdd); 154 | } 155 | } catch (error) { 156 | console.error(error); 157 | } 158 | 159 | // 160 | if (devices?.error) { 161 | throw 'authorization error'; 162 | } 163 | 164 | res.send({ 165 | status: 200, 166 | error: false, 167 | data: report, 168 | devicesRaw: devices, 169 | }); 170 | // 171 | } catch (error) { 172 | res.status(400).send({ status: 400, error: true, data: error }); 173 | } 174 | }; 175 | -------------------------------------------------------------------------------- /client/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "switch-ewelink-client": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/switch-ewelink-client", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", 32 | "src/styles.scss" 33 | ], 34 | "scripts": [] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "fileReplacements": [ 39 | { 40 | "replace": "src/environments/environment.ts", 41 | "with": "src/environments/environment.prod.ts" 42 | } 43 | ], 44 | "optimization": true, 45 | "outputHashing": "all", 46 | "sourceMap": false, 47 | "extractCss": true, 48 | "namedChunks": false, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "budgets": [ 53 | { 54 | "type": "initial", 55 | "maximumWarning": "2mb", 56 | "maximumError": "5mb" 57 | }, 58 | { 59 | "type": "anyComponentStyle", 60 | "maximumWarning": "150kb", 61 | "maximumError": "200kb" 62 | } 63 | ] 64 | }, 65 | "qa": { 66 | "fileReplacements": [ 67 | { 68 | "replace": "src/environments/environment.ts", 69 | "with": "src/environments/environment.qa.ts" 70 | } 71 | ], 72 | "optimization": true, 73 | "outputHashing": "all", 74 | "sourceMap": false, 75 | "extractCss": true, 76 | "namedChunks": false, 77 | "extractLicenses": true, 78 | "vendorChunk": false, 79 | "buildOptimizer": true, 80 | "budgets": [ 81 | { 82 | "type": "initial", 83 | "maximumWarning": "2mb", 84 | "maximumError": "5mb" 85 | }, 86 | { 87 | "type": "anyComponentStyle", 88 | "maximumWarning": "75kb", 89 | "maximumError": "100kb" 90 | } 91 | ] 92 | } 93 | } 94 | }, 95 | "serve": { 96 | "builder": "@angular-devkit/build-angular:dev-server", 97 | "options": { 98 | "browserTarget": "switch-ewelink-client:build" 99 | }, 100 | "configurations": { 101 | "production": { 102 | "browserTarget": "switch-ewelink-client:build:production" 103 | } 104 | } 105 | }, 106 | "extract-i18n": { 107 | "builder": "@angular-devkit/build-angular:extract-i18n", 108 | "options": { 109 | "browserTarget": "switch-ewelink-client:build" 110 | } 111 | }, 112 | "test": { 113 | "builder": "@angular-devkit/build-angular:karma", 114 | "options": { 115 | "main": "src/test.ts", 116 | "polyfills": "src/polyfills.ts", 117 | "tsConfig": "tsconfig.spec.json", 118 | "karmaConfig": "karma.conf.js", 119 | "assets": [ 120 | "src/favicon.ico", 121 | "src/assets" 122 | ], 123 | "styles": [ 124 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", 125 | "src/styles.scss" 126 | ], 127 | "scripts": [] 128 | } 129 | }, 130 | "lint": { 131 | "builder": "@angular-devkit/build-angular:tslint", 132 | "options": { 133 | "tsConfig": [ 134 | "tsconfig.app.json", 135 | "tsconfig.spec.json", 136 | "e2e/tsconfig.json" 137 | ], 138 | "exclude": [ 139 | "**/node_modules/**" 140 | ] 141 | } 142 | }, 143 | "e2e": { 144 | "builder": "@angular-devkit/build-angular:protractor", 145 | "options": { 146 | "protractorConfig": "e2e/protractor.conf.js", 147 | "devServerTarget": "switch-ewelink-client:serve" 148 | }, 149 | "configurations": { 150 | "production": { 151 | "devServerTarget": "switch-ewelink-client:serve:production" 152 | } 153 | } 154 | } 155 | } 156 | } 157 | }, 158 | "defaultProject": "switch-ewelink-client", 159 | "cli": { 160 | "analytics": false 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /client/src/app/device/device.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | import { MatDialog } from '@angular/material'; 4 | import { environment } from 'src/environments/environment'; 5 | import { JsonPrettyDialogComponent } from '../json-pretty-dialog/json-pretty-dialog.component'; 6 | import ChangeValue from '../models/change_value'; 7 | import { Device } from '../models/device'; 8 | import { StateEnum } from '../models/ewelink_enums'; 9 | import { EventService } from '../services/event.service'; 10 | import { SwitchService } from '../services/switch.service'; 11 | import { ThemeService } from '../services/theme.service'; 12 | 13 | @Component({ 14 | selector: 'app-device', 15 | templateUrl: './device.component.html', 16 | styleUrls: ['./device.component.scss'] 17 | }) 18 | export class DeviceComponent implements OnInit { 19 | 20 | @Input() public device: Device | any; 21 | @Input() public errorFromDevices: boolean; 22 | @Input() public checkControl: FormControl; 23 | @Output() public onChange: EventEmitter = new EventEmitter(); 24 | 25 | public labelState: string; 26 | public alternativeIcon: string = environment.alternativeIcon; 27 | 28 | public loading: boolean; 29 | 30 | constructor(private switchService: SwitchService, public themeService: ThemeService, private eventService: EventService, private dialog: MatDialog) { } 31 | 32 | ngOnInit(): void { 33 | 34 | if (!("state" in this.device) && !this.device.isMultipleChannelDevice) return; 35 | // 36 | this.labelState = this.device.state ? StateEnum.onText : StateEnum.offText; 37 | // 38 | this.checkControl.setValue(this.device.state, { emitEvent: false }); 39 | // 40 | this.switchService.isNewState.subscribe(res => { 41 | if (res.deviceid === this.device.deviceid) { 42 | 43 | if (res.params?.switch) { 44 | this.initLoading(); 45 | // 46 | this.checkControl.setValue(res.params.switch === StateEnum.on, { emitEvent: false }); 47 | // 48 | this.labelState = res.params.switch === StateEnum.on ? StateEnum.onText : StateEnum.offText 49 | 50 | this.stopLoading(); 51 | return; 52 | } 53 | // 54 | if (res.params?.switches) { 55 | this.setDeviceChannels(res) 56 | return; 57 | } 58 | // 59 | if (!this.device.isMultipleChannelDevice && !res.error) { 60 | this.initLoading(); 61 | // 62 | this.checkControl.setValue(!this.device.state, { emitEvent: false }); 63 | // 64 | this.labelState = !this.device.state ? StateEnum.onText : StateEnum.offText; 65 | // 66 | this.device.switch = !this.device.state ? StateEnum.on : StateEnum.off; 67 | this.device.state = !this.device.state; 68 | 69 | this.stopLoading(); 70 | return; 71 | } 72 | if (this.device.isMultipleChannelDevice && !res.error) { 73 | this.initLoading(); 74 | // 75 | this.switchService.getDevice(this.device.deviceid).subscribe(async res => { 76 | // 77 | const device_data = res.data; 78 | // 79 | this.setDeviceChannels(device_data) 80 | }, err => { 81 | console.log(err) 82 | }); 83 | this.stopLoading(); 84 | return; 85 | } 86 | 87 | } 88 | this.stopLoading(); 89 | }); 90 | 91 | } 92 | 93 | /** 94 | * 95 | * @param device 96 | */ 97 | changeManully(device) { 98 | if (!device.online) return; 99 | 100 | this.initLoading(); 101 | // 102 | const new_state = { 103 | switch: !device.state ? StateEnum.on : StateEnum.off 104 | } 105 | // 106 | this.onChange.emit({ 107 | // 108 | deviceid: this.device.deviceid, 109 | params: { 110 | ...new_state 111 | } 112 | }); 113 | 114 | } 115 | 116 | /** 117 | * 118 | * @param item 119 | * @param device 120 | */ 121 | changeChannelManully(item: any, device: any) { 122 | if (!device.online) return; 123 | // 124 | this.initLoading(); 125 | // 126 | const new_channel_state = { 127 | switch: !item.state ? StateEnum.on : StateEnum.off, 128 | outlet: item.channel 129 | } 130 | 131 | let switches = device.deviceInfo.params.switches 132 | 133 | switches[item.channel] = new_channel_state 134 | 135 | this.onChange.emit( 136 | { 137 | deviceid: item.parentDeviceId, 138 | params: { 139 | switches: switches 140 | } 141 | } 142 | ); 143 | } 144 | 145 | 146 | setDeviceChannels(device_data) { 147 | this.device = { 148 | ...this.device, 149 | deviceChannels: this.device.deviceChannels.map((d_channel) => { 150 | 151 | const d_c_switch = device_data.params.switches[d_channel.channel].switch 152 | d_channel.state = d_c_switch === StateEnum.on; 153 | d_channel.switch = d_c_switch; 154 | 155 | return d_channel; 156 | }) 157 | } 158 | } 159 | 160 | initLoading() { 161 | this.loading = true; 162 | } 163 | 164 | stopLoading() { 165 | this.loading = false; 166 | } 167 | 168 | openJSONDialog() { 169 | 170 | return this.dialog.open(JsonPrettyDialogComponent, { 171 | autoFocus: false, 172 | data: this.device, 173 | hasBackdrop: true, 174 | maxHeight: '90%', 175 | panelClass: ['animate__animated', 'animate__zoomIn', 'dialog-responsive'] 176 | }); 177 | } 178 | 179 | } 180 | --------------------------------------------------------------------------------