myenergi-api 0.0.4-0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "myenergi-api",
3
- "version": "0.0.4-0",
3
+ "version": "0.1.0",
4
4
  "description": "NodeJS implementation of MyEnergi API for controlling Zappi, Eddi and other MyEnergi products",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
- "test": "echo \"Error: no test specified\" && exit 1"
9
+ "test": "env TS_NODE_PROJECT='tsconfig.testing.json' mocha -r ts-node/register tests/**/*-spec.ts"
10
10
  },
11
11
  "repository": {
12
12
  "type": "git",
@@ -24,11 +24,17 @@
24
24
  "url": "https://github.com/bisand/myenergi-api/issues"
25
25
  },
26
26
  "homepage": "https://github.com/bisand/myenergi-api#readme",
27
+ "dependencies": {
28
+ "dotenv": "^10.0.0"
29
+ },
27
30
  "devDependencies": {
28
- "@types/node": "^16.11.8",
31
+ "@types/chai": "^4.2.22",
32
+ "@types/mocha": "^9.0.0",
33
+ "@types/node": "^16.11.9",
34
+ "chai": "^4.3.4",
35
+ "mocha": "^9.1.3",
36
+ "ts-node": "^10.4.0",
37
+ "tslint": "^6.1.3",
29
38
  "typescript": "^4.5.2"
30
- },
31
- "dependencies": {
32
- "digest-fetch": "^1.2.1"
33
39
  }
34
40
  }
@@ -0,0 +1,131 @@
1
+ import * as crypto from 'crypto';
2
+
3
+ export class AuthDigest {
4
+ private _password?: string | undefined;
5
+ private _username?: string | undefined;
6
+ private _initialized: boolean;
7
+ private _onError?: (errorMessage: string) => void;
8
+
9
+ public get username(): string | undefined {
10
+ return this._username;
11
+ }
12
+ private _realm?: string | undefined;
13
+ public get realm(): string | undefined {
14
+ return this._realm;
15
+ }
16
+ private _nonce?: string | undefined;
17
+ public get nonce(): string | undefined {
18
+ return this._nonce;
19
+ }
20
+ private _uri?: string | undefined;
21
+ public get uri(): string | undefined {
22
+ return this._uri;
23
+ }
24
+ private _algorithm: string;
25
+ public get algorithm(): string {
26
+ return this._algorithm;
27
+ }
28
+ private _response?: string | undefined;
29
+ public get response(): string | undefined {
30
+ return this._response;
31
+ }
32
+ private _opaque?: string | undefined;
33
+ public get opaque(): string | undefined {
34
+ return this._opaque;
35
+ }
36
+ private _qop?: string | undefined;
37
+ public get qop(): string | undefined {
38
+ return this._qop;
39
+ }
40
+ private _nc: number;
41
+ public get nc(): string | undefined {
42
+ this._nc++;
43
+ var myHex = ("0000000" + this._nc?.toString(16)).substr(-8);
44
+ return myHex;
45
+ }
46
+ private _cnonce?: string | undefined;
47
+ public get cnonce(): string | undefined {
48
+ this._cnonce = this.md5(String(new Date().getTime()));
49
+ return this._cnonce;
50
+ }
51
+
52
+ private md5(data: string) {
53
+ let md5 = crypto.createHash('md5');
54
+ let result = md5.update(data).digest('hex');
55
+ return result;
56
+ }
57
+
58
+ constructor(username: string, password: string, errorHandler?: (errorMessage: string) => void) {
59
+ this._username = username;
60
+ this._password = password;
61
+ this._nc = 0;
62
+ this._initialized = false;
63
+ this._algorithm = 'MD5';
64
+ if (errorHandler)
65
+ this._onError = errorHandler;
66
+ }
67
+
68
+ public init(wwwAuthHeader: string) {
69
+
70
+ let authSplit = wwwAuthHeader.split(',') as string[];
71
+
72
+ for (let item of authSplit) {
73
+ if (item.indexOf('realm=') >= 0) {
74
+ let realmSplit = item.split('="');
75
+ this._realm = realmSplit[realmSplit.length - 1];
76
+ this._realm = this._realm.substring(0, this._realm.length - 1);
77
+ }
78
+
79
+ if (item.indexOf('nonce=') >= 0) {
80
+ let nonceSplit = item.split('="');
81
+ this._nonce = nonceSplit[nonceSplit.length - 1];
82
+ this._nonce = this._nonce.substring(0, this._nonce.length - 1);
83
+ }
84
+
85
+ if (item.indexOf('qop=') >= 0) {
86
+ let qopSplit = item.split('="');
87
+ this._qop = qopSplit[qopSplit.length - 1];
88
+ this._qop = this._qop.substring(0, this._qop.length - 1);
89
+ }
90
+
91
+ if (item.indexOf('opaque=') >= 0) {
92
+ let opaqueSplit = item.split('="');
93
+ this._opaque = opaqueSplit[opaqueSplit.length - 1];
94
+ this._opaque = this._opaque.substring(0, this._opaque.length - 1);
95
+ }
96
+
97
+ if (item.indexOf('algorithm=') >= 0) {
98
+ let algorithmSplit = item.split('=');
99
+ this._algorithm = algorithmSplit[algorithmSplit.length - 1];
100
+ this._algorithm = this._algorithm.substring(0, this._algorithm.length);
101
+ if (this._onError && this.algorithm !== 'MD5') {
102
+ this._onError(`Algorithm ${this.algorithm} is ont supported. Only MD5 is supportet`);
103
+ }
104
+ }
105
+ }
106
+ this._initialized = true;
107
+ }
108
+
109
+ public getAuthorization(httpMethod: string, path: string): string {
110
+ if (!this._initialized)
111
+ return '';
112
+
113
+ const nc = this.nc;
114
+ const cnonce = this.cnonce;
115
+ let HA1 = this.md5(this.username + ':' + this.realm + ':' + this._password);
116
+ let HA2 = this.md5(httpMethod + ':' + path);
117
+ let response = this.md5(HA1 + ':' + this.nonce + ':' + nc + ':' + cnonce + ':' + this.qop + ':' + HA2);
118
+
119
+ let res = `Digest username="${this.username}",`;
120
+ res += `realm="${this.realm}",`;
121
+ res += `nonce="${this.nonce}",`;
122
+ res += `uri="${path}",`;
123
+ res += `cnonce="${cnonce}",`;
124
+ res += `nc=${nc},`;
125
+ res += `algorithm=${this.algorithm},`;
126
+ res += `response="${response}",`;
127
+ res += `qop="${this.qop}",`;
128
+ res += `opaque="${this.opaque}"`;
129
+ return res;
130
+ }
131
+ }
package/src/Digest.ts ADDED
@@ -0,0 +1,88 @@
1
+ import * as https from 'https';
2
+ import { RequestOptions } from 'https';
3
+ import * as url from "url";
4
+ import { AuthDigest } from './AuthDigest';
5
+
6
+ export class Digest {
7
+ private _authDigest?: AuthDigest;
8
+
9
+ constructor(username: string, password: string) {
10
+ this._authDigest = new AuthDigest(username, password, (err) => {
11
+ console.error(err);
12
+ });
13
+ }
14
+
15
+ private request(options: RequestOptions, data?: any, retryCount: number = 0): Promise<any> {
16
+ return new Promise<any>((resolve, reject) => {
17
+ let resData: string = '';
18
+ if (!options.headers)
19
+ options.headers = {};
20
+ options.headers.Authorization = this._authDigest?.getAuthorization(options.method as string, options.path as string);
21
+ const req = https.request(options, (res: any) => {
22
+
23
+ if (res.statusCode == 401) {
24
+ if (retryCount > 1) {
25
+ reject("Authentication failed");
26
+ return;
27
+ }
28
+ retryCount++;
29
+ const wwwAuth = res.headers["www-authenticate"] as string;
30
+ if (!wwwAuth.startsWith('Digest')) {
31
+ reject('Unsupported authentication method. Supported authentication schemes: Digest');
32
+ return;
33
+ }
34
+ this._authDigest?.init(wwwAuth);
35
+ if (!options.headers)
36
+ options.headers = {};
37
+ options.headers.Authorization = this._authDigest?.getAuthorization(options.method as string, options.path as string);
38
+ return this.request(options, data, retryCount).then(value => {
39
+ resolve(value);
40
+ }).catch(resaon => {
41
+ reject(resaon);
42
+ });
43
+ } else if (res.statusCode >= 200 && res.statusCode < 300) {
44
+ res.setEncoding('utf8');
45
+ res.on('data', (chunk: string) => {
46
+ resData += chunk;
47
+ });
48
+ res.on('end', () => {
49
+ resolve(resData);
50
+ });
51
+ } else {
52
+ console.error("status code failed!!");
53
+ reject("status code failed!!");
54
+ return;
55
+ }
56
+ });
57
+
58
+ req.on('error', (e: any) => {
59
+ console.error(`problem with request: ${e.message}`);
60
+ reject(e);
61
+ });
62
+
63
+ if (data) {
64
+ req.write(data);
65
+ }
66
+
67
+ req.end();
68
+ });
69
+ }
70
+
71
+ public get(requestUrl: string, data?: any): Promise<any> {
72
+ const uri = url.parse(requestUrl, true);
73
+ const options: RequestOptions = {
74
+ hostname: uri.hostname,
75
+ port: uri.port,
76
+ path: uri.pathname,
77
+ method: 'GET',
78
+ headers: {
79
+ 'Connection': 'Keep-Alive',
80
+ 'Content-Type': 'application/json',
81
+ 'Accept': 'application/json',
82
+ 'Host': uri.hostname as string,
83
+ }
84
+ };
85
+ return this.request(options, data);
86
+ }
87
+
88
+ }
package/src/MyEnergi.ts CHANGED
@@ -1,4 +1,18 @@
1
- import DigestFetch from 'digest-fetch';
1
+ import { Digest } from './Digest';
2
+ import { Zappi } from './models/Zappi';
3
+
4
+ export enum ZappiChargeMode {
5
+ Off = 4,
6
+ Fast = 1,
7
+ Eco = 2,
8
+ EcoPlus = 3
9
+ }
10
+
11
+ export enum ZappiBoostMode {
12
+ Manual = 10,
13
+ Smart = 11,
14
+ Stop = 2
15
+ }
2
16
 
3
17
  export class MyEnergi {
4
18
  private _config = {
@@ -8,12 +22,70 @@ export class MyEnergi {
8
22
  zappi_url: 'https://s18.myenergi.net/cgi-jstatus-Z',
9
23
  harvi_url: 'https://s18.myenergi.net/cgi-jstatus-H',
10
24
  status_url: 'https://s18.myenergi.net/cgi-jstatus-*',
11
- dayhour_url: 'https://s18.myenergi.net/cgi-jdayhour-'
25
+ dayhour_url: 'https://s18.myenergi.net/cgi-jdayhour-',
26
+ zappi_mode_url: 'https://s18.myenergi.net/cgi-zappi-mode-Z',
27
+ zappi_min_green_url: 'https://s18.myenergi.net/cgi-set-min-green-Z'
12
28
  //https://s18.myenergi.net/cgi-jdayhour-Znnnnnnnn-YYYY-MM-DD
13
29
  }
14
30
 
31
+ private _digest: Digest;
32
+
15
33
  constructor(username: string, password: string) {
16
34
  this._config.username = username;
17
35
  this._config.password = password;
36
+ this._digest = new Digest(this._config.username, this._config.password);
37
+ }
38
+
39
+ public async getStatusAll(): Promise<any> {
40
+ const data = await this._digest.get(this._config.status_url);
41
+ return data;
42
+ }
43
+
44
+ public async getStatusZappiAll(): Promise<Zappi[]> {
45
+ const data = await this._digest.get(this._config.zappi_url);
46
+ const jsonData = JSON.parse(data);
47
+ if (jsonData.zappi)
48
+ return Object.assign<Zappi[], any>([] as Zappi[], jsonData.zappi);
49
+ else
50
+ return [] as Zappi[];
18
51
  }
52
+
53
+ public async getStatusZappi(serialNumber: string): Promise<Zappi | null> {
54
+ const data = await this._digest.get(this._config.zappi_url);
55
+ const jsonData = JSON.parse(data);
56
+ if (jsonData.zappi) {
57
+ const zappi = (Object.assign<Zappi[], any>([] as Zappi[], jsonData.zappi) as Zappi[]).find(zappi => {
58
+ return zappi.sno === serialNumber;
59
+ });
60
+ if (zappi)
61
+ return Object.assign<Zappi, any>({} as Zappi, zappi);
62
+ else
63
+ return null;
64
+ }
65
+ else
66
+ return null;
67
+ }
68
+
69
+ public async setZappiChargeMode(serialNo: string, chargeMode: ZappiChargeMode): Promise<any> {
70
+ const url = `${this._config.zappi_mode_url}${serialNo}-${chargeMode}-0-0-0000`;
71
+ const data = await this._digest.get(url);
72
+ return data;
73
+ }
74
+
75
+ public async setZappiBoostMode(serialNo: string, boostMode: ZappiBoostMode, kwh: number = 0, completeTime: string = '0000'): Promise<any> {
76
+ if (boostMode === ZappiBoostMode.Stop) {
77
+ kwh = 0;
78
+ completeTime = '0000';
79
+ }
80
+ const url = `${this._config.zappi_mode_url}${serialNo}-0-${boostMode}-${kwh}-${completeTime}`;
81
+ const data = await this._digest.get(url);
82
+ return data;
83
+ }
84
+
85
+ public async setZappiGreenLevel(serialNo: string, percentage: number): Promise<any> {
86
+ const url = `${this._config.zappi_min_green_url}${serialNo}-${percentage}`;
87
+ const data = await this._digest.get(url);
88
+ return data;
89
+ }
90
+
19
91
  }
@@ -0,0 +1,41 @@
1
+ export interface Zappi {
2
+ sno: string;
3
+ dat: string;
4
+ tim: string;
5
+ ectp1: number;
6
+ ectp2: number;
7
+ ectp3: number;
8
+ ectp4: number;
9
+ ectp5: number;
10
+ ectp6: number;
11
+ ectt1: string;
12
+ ectt2: string;
13
+ ectt3: string;
14
+ ectt4: string;
15
+ ectt5: string;
16
+ ectt6: string;
17
+ bsm: number;
18
+ bst: number;
19
+ cmt: number;
20
+ dst: number;
21
+ div: number;
22
+ frq: number;
23
+ fwv: string;
24
+ gen: number;
25
+ grd: number;
26
+ pha: number;
27
+ pri: number;
28
+ sta: number;
29
+ tz: number;
30
+ vol: number;
31
+ che: number;
32
+ bss: number;
33
+ lck: number;
34
+ pst: string;
35
+ zmo: number;
36
+ zs: number;
37
+ rdc: number;
38
+ rac: number;
39
+ rrac: number;
40
+ zsl: number;
41
+ }
@@ -0,0 +1,12 @@
1
+ import { MyEnergi } from '../src/MyEnergi'
2
+ import { expect } from 'chai'
3
+
4
+ describe("MyEnergi Tests", () => {
5
+ it("should be able to test", () => {
6
+ expect(true).to.be.true;
7
+ });
8
+ it("should be able to create instance", () => {
9
+ const myenergi = new MyEnergi('test', 'pwd');
10
+ expect(myenergi).to.be.instanceOf(MyEnergi);
11
+ });
12
+ });
package/tests/debug.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { exit } from 'process';
2
+ import { MyEnergi } from '../src';
3
+ import * as dotenv from 'dotenv';
4
+ import { ZappiBoostMode, ZappiChargeMode } from '../src/MyEnergi';
5
+
6
+ dotenv.config();
7
+ const myenergi = new MyEnergi(process.env.USERNAME as string, process.env.PASSWORD as string);
8
+ myenergi.getStatusAll().then(val => {
9
+ console.log(val);
10
+ myenergi.getStatusZappiAll().then(v => {
11
+ console.log(v);
12
+ const sno: string = v[0].sno;
13
+ myenergi.setZappiChargeMode(sno, ZappiChargeMode.Fast).then(x => {
14
+ console.log(x);
15
+ myenergi.setZappiBoostMode(sno, ZappiBoostMode.Stop).then(x => {
16
+ console.log(x);
17
+ myenergi.setZappiGreenLevel(sno, 0).then(x => {
18
+ console.log(x);
19
+ myenergi.getStatusZappi(sno).then(x => {
20
+ console.log(x);
21
+ exit();
22
+ }).catch(err => {
23
+ console.error(err);
24
+ exit();
25
+ });
26
+ }).catch(err => {
27
+ console.error(err);
28
+ exit();
29
+ });
30
+ }).catch(err => {
31
+ console.error(err);
32
+ exit();
33
+ });
34
+ }).catch(err => {
35
+ console.error(err);
36
+ exit();
37
+ });
38
+ }).catch(err => {
39
+ console.error(err);
40
+ exit();
41
+ });
42
+ }).catch(err => {
43
+ console.error(err);
44
+ exit();
45
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "es2015",
5
+ "lib": ["es2017"],
6
+ "outDir": "./dist",
7
+ "declaration": false,
8
+ "noImplicitAny": false,
9
+ "removeComments": true,
10
+ "inlineSourceMap": true,
11
+ "moduleResolution": "node",
12
+ },
13
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "tests/**/*.ts"]
14
+ }
@@ -1,4 +0,0 @@
1
- export declare class MyEnergi {
2
- private _config;
3
- constructor(username: string, password: string);
4
- }
package/dist/MyEnergi.js DELETED
@@ -1,16 +0,0 @@
1
- export class MyEnergi {
2
- constructor(username, password) {
3
- this._config = {
4
- username: '',
5
- password: '',
6
- eddi_url: 'https://s18.myenergi.net/cgi-jstatus-E',
7
- zappi_url: 'https://s18.myenergi.net/cgi-jstatus-Z',
8
- harvi_url: 'https://s18.myenergi.net/cgi-jstatus-H',
9
- status_url: 'https://s18.myenergi.net/cgi-jstatus-*',
10
- dayhour_url: 'https://s18.myenergi.net/cgi-jdayhour-'
11
- //https://s18.myenergi.net/cgi-jdayhour-Znnnnnnnn-YYYY-MM-DD
12
- };
13
- this._config.username = username;
14
- this._config.password = password;
15
- }
16
- }
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import { MyEnergi } from './MyEnergi';
2
- export { MyEnergi };
package/dist/index.js DELETED
@@ -1,2 +0,0 @@
1
- import { MyEnergi } from './MyEnergi';
2
- export { MyEnergi };
package/npm-publish.sh DELETED
@@ -1,73 +0,0 @@
1
- #!/bin/sh
2
-
3
- DEV_BRANCH_NAME="develop"
4
- MASTER_BRANCH_NAME="master"
5
- RELEASE_TYPE="$1"
6
-
7
- confirm() {
8
- # call with a prompt string or use a default
9
- read -r -p "${1:-Are you sure? [y/N]} " response
10
- case "$response" in
11
- [yY][eE][sS]|[yY])
12
- true
13
- ;;
14
- *)
15
- false
16
- ;;
17
- esac
18
- }
19
-
20
- execute() {
21
- if git checkout ${DEV_BRANCH_NAME} &&
22
- git fetch origin ${DEV_BRANCH_NAME} &&
23
- # [ `git rev-list HEAD...origin/${DEV_BRANCH_NAME} --count` != 0 ] &&
24
- git merge origin/${DEV_BRANCH_NAME}
25
- then
26
- npm install
27
- echo "Performing ${RELEASE_TYPE} release preparations..."
28
- if npm version ${RELEASE_TYPE} -m "Release version %s"
29
- then
30
- echo "NPM version done. Proceeding with git operations..."
31
- if git add . &&
32
- git push &&
33
- git checkout origin/${MASTER_BRANCH_NAME} &&
34
- git pull &&
35
- git merge ${DEV_BRANCH_NAME} &&
36
- git push &&
37
- git push --tags
38
- then
39
- git checkout origin/${DEV_BRANCH_NAME}
40
- git merge ${MASTER_BRANCH_NAME}
41
- git push
42
- echo "Changes merged to ${MASTER_BRANCH_NAME} branch and pushed to origin."
43
- true
44
- else
45
- echo "Failed to merge to ${MASTER_BRANCH_NAME} branch and push to origin."
46
- false
47
- fi
48
- else
49
- echo "NPM version failed!"
50
- false
51
- fi
52
- else
53
- echo "Could not prepare for new release. Please make sure the repository is up to date with the origin before you continue."
54
- false
55
- fi
56
- }
57
-
58
- run() {
59
- if confirm && execute
60
- then
61
- echo "Successfully completed!"
62
- else
63
- echo "Failed to complete!"
64
- fi
65
- }
66
-
67
- if [ "${RELEASE_TYPE}" = "patch" ] || [ "${RELEASE_TYPE}" = "minor" ] || [ "${RELEASE_TYPE}" = "major" ] ||
68
- [ "${RELEASE_TYPE}" = "prepatch" ] || [ "${RELEASE_TYPE}" = "preminor" ] || [ "${RELEASE_TYPE}" = "premajor" ] || [ "${RELEASE_TYPE}" = "prerelease" ]
69
- then
70
- run
71
- else
72
- echo "'${RELEASE_TYPE}' is not a valid release type. Please use 'patch', 'minor', 'major', 'prepatch', 'preminor' or 'premajor'"
73
- fi
@@ -1 +0,0 @@
1
- declare module "digest-fetch";
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2015",
4
- "module": "ES2020",
5
- "declaration": true,
6
- "outDir": "./dist",
7
- "forceConsistentCasingInFileNames": true,
8
- "moduleResolution": "node",
9
- "esModuleInterop": true,
10
- "strict": true
11
- },
12
- "include": [
13
- "./src"
14
- ]
15
- }