homebridge-sunsynk
Version:
A plugin for homebridge to integrate Sunsynk inverter into HomeKit
218 lines (179 loc) • 6.81 kB
JavaScript
const crypto = require('crypto');
const CryptoJS = require('crypto-js');
const axios = require('axios');
class sunsynkAPI {
constructor(username, password, appKey, appSecret, log) {
this.username = username;
this.password = password;
this.appKey = appKey;
this.appSecret = appSecret;
this.log = log;
this.tokenInfo = {
access_token: '',
refresh_token: '',
uuid: '',
expires_in: 0
}
this.preURL = "https://api.sunsynk.net/";
this.nonce = this.createUuid();
this.body = '';
this.md5 = '';
this.signature = '';
this.signatureHeaders = '';
this.headers = {
'content-type': 'application/json',
'accept': 'application/json',
'Content-MD5': this.md5,
'X-Ca-Nonce': this.nonce,
'X-Ca-Key': this.appKey,
/*'X-Ca-Signature': this.signature,
'X-Ca-Signature-Headers': this.signatureHeaders*/
};
}
sign_stuff(method, url) {
var textToSign = '';
textToSign += method + "\n";
textToSign += this.headers['accept'] + "\n";
textToSign += this.md5 + "\n";
textToSign += this.headers['content-type'] + "\n";
textToSign += "\n";
var headers = this.headersToSign();
this.signatureHeaders = '';
var sortedKeys = Array.from(headers.keys()).sort();
for (var headerName of sortedKeys) {
textToSign += headerName + ":" + headers.get(headerName) + "\n";
this.signatureHeaders = this.signatureHeaders ? this.signatureHeaders + "," + headerName : headerName;
}
textToSign += this.urlToSign(url);
var hash = CryptoJS.HmacSHA256(textToSign, this.appSecret);
this.signature = hash.toString(CryptoJS.enc.Base64);
this.headers['X-Ca-Signature'] = this.signature;
this.headers['X-Ca-Signature-Headers'] = this.signatureHeaders;
}
async request(method, path, params = null, body = null) {
try {
await this.refreshAccessTokenIfNeed(path);
} catch (e) {
this.log.log.error(e + "\n");
this.log.log.error("Could not refresh token!!\n");
}
/*this.md5 = this.calcMd5(params);
this.headers['Content-MD5'] = this.md5;
this.headers['Authorization'] = `Bearer ${this.tokenInfo.access_token}`;
this.sign_stuff(method, path);*/
var headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
"Authorization": `Bearer ${this.tokenInfo.access_token}`
}
let res = await axios({
baseURL: this.preURL + "api/v1/",
url: path,
method: method,
headers: headers,
params: params,
data: body
});
this.log.debug(`API response: ${JSON.stringify(res.data)} path = ${path}`);
return res.data.data;
}
async get(path, params, body) {
return this.request('GET', path, params, body);
}
async post(path, params, body) {
return this.request('POST', path, params, body);
}
async login() {
this.body = JSON.stringify({
username: this.username,
password: this.password,
grant_type: 'password',
client_id: 'openapi'
});
this.md5 = this.calcMd5(this.body);
this.headers['Content-MD5'] = this.md5;
this.sign_stuff("POST", "/oauth/token");
let res = await axios({
baseURL: this.preURL,
url: "/oauth/token",
method: "POST",
headers: this.headers,
data: this.body
});
let { access_token, refresh_token, expires_in } = res.data.data;
this.tokenInfo = {
access_token: access_token,
refresh_token: refresh_token,
expires_in: expires_in + new Date().getTime(),
};
this.log.debug(`API response: ${JSON.stringify(res.data)}`);
return res.data.success;
//return this.post("/oauth/token", this.headers, this.body);
}
async refreshAccessTokenIfNeed(path) {
if (path.startsWith('/oauth/token')) {
return;
}
if (this.tokenInfo.expires_in - 60 * 1000 > new Date().getTime()) {
return;
}
//this.tokenInfo.access_token = '';
}
calcMd5(data) {
return crypto.createHash('md5').update(data).digest('base64');
}
createUuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
headersToSign() {
var headers = new Map();
for (var name in this.headers) {
name = name.toLowerCase();
if (!name.startsWith('x-ca-')) {
continue;
}
if (name === "x-ca-signature" || name === "x-ca-signature-headers" || name == "x-ca-key" || name === 'x-ca-nonce' || name === 'x-ca-stage') {
continue;
}
var value = this.headers[name];
headers.set(name, value);
}
headers.set('x-ca-key', this.appKey);
headers.set('x-ca-nonce', this.nonce);
return headers;
}
urlToSign(url_send) {
var params = new Map();
var contentType = this.headers["content-type"];
if (contentType && contentType.startsWith('application/x-www-form-urlencoded')) {
const formParams = this.body.split("&");
formParams.forEach((p) => {
const ss = p.split('=');
params.set(ss[0], ss[1]);
})
}
const ss = url_send.split('?');
if (ss.length > 1 && ss[1]) {
const queryParams = ss[1].split('&');
queryParams.forEach((p) => {
const ss = p.split('=');
params.set(ss[0], ss[1]);
})
}
var sortedKeys = Array.from(params.keys())
sortedKeys.sort();
var first = true;
var qs
for (var k of sortedKeys) {
var s = k + "=" + params.get(k);
qs = qs ? qs + "&" + s : s;
console.log("key=" + k + " value=" + params.get(k));
}
var url = ss[0];
return qs ? url + "?" + qs : url;
}
}
module.exports = sunsynkAPI;