homebridge-jci-hitachi-platform
Version:
Homebridge platform plugin providing HomeKit support for Jci Hitachi air conditioners.
611 lines • 24.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AWSThings = exports.AWSThingDictionary = void 0;
const axios_1 = __importDefault(require("axios"));
const aws_iot_device_sdk_v2_1 = require("aws-iot-device-sdk-v2");
const events_1 = require("events");
const util_utf8_browser_1 = require("@aws-sdk/util-utf8-browser");
const cert_1 = require("./cert");
const https = require("https");
const AWS_REGION = "ap-northeast-1";
const AWS_COGNITO_IDP_ENDPOINT = `cognito-idp.${AWS_REGION}.amazonaws.com`;
const AWS_COGNITO_ENDPOINT = `cognito-identity.${AWS_REGION}.amazonaws.com`;
const AWS_COGNITO_CLIENT_ID = "7kfnjsb66ei1qt5s5gjv6j1lp6";
const AWS_COGNITO_USERPOOL_ID = `${AWS_REGION}_aTZeaievK`;
const AWS_IOT_ENDPOINT = "iot-api.jci-hitachi-smarthome.com";
const AWS_MQTT_ENDPOINT = `a8kcu267h96in-ats.iot.${AWS_REGION}.amazonaws.com`;
const QOS = aws_iot_device_sdk_v2_1.mqtt5.QoS.AtLeastOnce;
function generateRandomHex(length) {
const characters = 'abcdef0123456789';
let result = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
result += characters[randomIndex];
}
return result;
}
class AWSTokens {
constructor(access_token = "", id_token = "", refresh_token = "", expiration = 0) {
this.access_token = access_token;
this.id_token = id_token;
this.refresh_token = refresh_token;
this.expiration = expiration;
}
}
class AWSIdentity {
constructor(identity_id, user_name, user_attributes) {
this.identity_id = identity_id;
this.host_identity_id = user_attributes["custom:host_identity_id"],
this.user_name = user_name;
this.user_attributes = user_attributes;
}
}
class AWSCredentials {
constructor(awscredentialsContent) {
this.access_key_id = "";
this.secret_access_key = "";
this.session_token = "";
this.expiration = 0;
const awscredentialsJson = JSON.parse(awscredentialsContent);
this.access_key_id = awscredentialsJson['AccessKeyId'];
this.secret_access_key = awscredentialsJson['SecretKey'];
this.session_token = awscredentialsJson['SessionToken'];
this.expiration = awscredentialsJson['Expiration'];
}
}
class AWSThingDictionary {
constructor(awsallthingsContent, log = undefined) {
var _a;
this.things = {};
this.log = log;
const awsallthingsJson = JSON.parse(awsallthingsContent);
const things = awsallthingsJson['results']['Things'];
for (let i = 0; i < things.length; i++) {
const thing = new AWSThings(JSON.stringify(things[i]));
this.things[thing.ThingName] = thing;
}
(_a = this.log) === null || _a === void 0 ? void 0 : _a.info(`You have ${Object.keys(this.things).length} devices.`);
}
getThingNameByCustomDeviceName(customDeviceName) {
for (let i = 0; i < Object.keys(this.things).length; i++) {
if (this.things[i].CustomDeviceName == customDeviceName) {
return this.things[i].ThingName;
}
}
return undefined;
}
getAllThings() {
return this.things;
}
hasThingName(thingName) {
return this.things[thingName] !== undefined;
}
getDevice(thingName) {
return this.things[thingName];
}
updateDeviceStatusPayload(thingName, payload) {
if (this.hasThingName(thingName) == false) {
return;
}
this.things[thingName].updateStatusPayload(payload);
}
updateDeviceRegistrationPayload(thingName, payload) {
if (this.hasThingName(thingName) == false) {
return;
}
this.things[thingName].updateRegistrationPayload(payload);
}
}
exports.AWSThingDictionary = AWSThingDictionary;
class AWSThings {
constructor(awsthingsContent) {
this.statusPayload = undefined;
this.registrationPayload = undefined;
this.thingObject = JSON.parse(awsthingsContent);
}
get ThingName() {
return this.thingObject['ThingName'];
}
get CustomDeviceName() {
return this.thingObject['CustomDeviceName'];
}
get DeviceType() {
return this.thingObject['DeviceType'];
}
get SwitchOn() {
return this.statusPayload ? this.statusPayload['Switch'] : false;
}
get TemperatureSetting() {
return this.statusPayload ? this.statusPayload['TemperatureSetting'] : undefined;
}
get TemperatureSettingMin() {
return this.registrationPayload ? (this.registrationPayload['TemperatureSetting'] >> 8 & 255) : 16;
}
get TemperatureSettingMax() {
return this.registrationPayload ? (this.registrationPayload['TemperatureSetting'] & 255) : 32;
}
get IndoorTemperature() {
return this.statusPayload ? this.statusPayload['IndoorTemperature'] : undefined;
}
get IndoorHumidity() {
return this.statusPayload ? this.statusPayload['IndoorHumidity'] : undefined;
}
get PM25() {
return this.statusPayload ? this.statusPayload['PM25'] : undefined;
}
get FanSpeed() {
return this.statusPayload ? this.statusPayload['FanSpeed'] : undefined;
}
get QuickMode() {
return this.statusPayload ? this.statusPayload['QuickMode'] : false;
}
get CleanNotification() {
return this.statusPayload ? this.statusPayload['CleanNotification'] : false;
}
get CleanSwitch() {
return this.statusPayload ? this.statusPayload['CleanSwitch'] : false;
}
get FirmwareVersion() {
return this.registrationPayload ? this.registrationPayload['FirmwareVersion'] : undefined;
}
get Model() {
return this.registrationPayload ? this.registrationPayload['Model'] : undefined;
}
updateStatusPayload(payload) {
this.statusPayload = payload;
}
updateRegistrationPayload(payload) {
this.registrationPayload = payload;
}
}
exports.AWSThings = AWSThings;
class JciHitachiAWSHttpConnection {
constructor(log) {
this.log = log;
}
//Request https connection with AWS_SSL_CERT, and return the response
async requestHttps(url, method, headers, data) {
return await axios_1.default.request({
httpsAgent: new https.Agent({
ca: [cert_1.AWS_SSL_CERT],
}),
method: method,
url: url,
headers: headers,
data: JSON.stringify(data)
});
}
}
class JciHitachiAWSCognitoConnection extends JciHitachiAWSHttpConnection {
constructor(email, password, aws_tokens, log) {
super(log);
this.email = email;
this.password = password;
this.aws_tokens = aws_tokens;
}
_generateHeaders(target) {
return {
"X-Amz-Target": target,
"User-Agent": "Dalvik/2.1.0",
"content-type": "application/x-amz-json-1.1",
"Accept": "application/json",
};
}
_handle_response(response) {
if (response.status == 200) {
//this.log.debug(`login_req: ${JSON.stringify(response.data)}`);
return response;
}
else {
this.log.error(`login_req: ${JSON.stringify(response.data)}`);
return response;
}
}
_send(target, data) {
const endpoint = `https://${this.constructor.name === 'GetCredentials' ? AWS_COGNITO_ENDPOINT : AWS_COGNITO_IDP_ENDPOINT}`;
return this.requestHttps(endpoint, 'post', this._generateHeaders(target), data);
}
getAWSTokens() {
return this.aws_tokens;
}
async login(use_refresh_token) {
let login_json_data;
const login_headers = this._generateHeaders("AWSCognitoIdentityProviderService.InitiateAuth");
if (use_refresh_token && this.aws_tokens) {
login_json_data = {
"AuthFlow": 'REFRESH_TOKEN_AUTH',
"AuthParameters": {
'REFRESH_TOKEN': this.aws_tokens.refresh_token,
},
"ClientId": AWS_COGNITO_CLIENT_ID,
};
}
else {
login_json_data = {
"AuthFlow": 'USER_PASSWORD_AUTH',
"AuthParameters": {
'USERNAME': this.email,
'PASSWORD': this.password,
},
"ClientId": AWS_COGNITO_CLIENT_ID,
};
}
const login_req = this.requestHttps(`https://${AWS_COGNITO_IDP_ENDPOINT}`, 'post', login_headers, login_json_data);
const response = this._handle_response(await login_req);
if (response.status == 200) {
const auth_result = response.data['AuthenticationResult'];
this.aws_tokens = new AWSTokens(auth_result['AccessToken'], auth_result['IdToken'], use_refresh_token && this.aws_tokens ? this.aws_tokens.refresh_token : auth_result['RefreshToken'], new Date().valueOf() + auth_result['ExpiresIn']);
}
else {
this.log.error(`login_req: ${JSON.stringify(response.data)}`);
}
return this.aws_tokens;
}
}
class GetUser extends JciHitachiAWSCognitoConnection {
async get_data() {
if (!this.aws_tokens) {
return undefined;
}
const json_data = {
"AccessToken": this.aws_tokens.access_token,
};
const response = await this._send("AWSCognitoIdentityProviderService.GetUser", json_data);
if (response.status == 200) {
const user_attributes = response.data['UserAttributes'].reduce((acc, cur) => {
acc[cur.Name] = cur.Value;
return acc;
}, {});
return new AWSIdentity(user_attributes['custom:cognito_identity_id'], user_attributes['Username'], user_attributes);
}
}
}
class GetCredentials extends JciHitachiAWSCognitoConnection {
async get_data(aws_identity) {
if (!this.aws_tokens) {
return undefined;
}
const json_data = JSON.parse(`{
"IdentityId": "${aws_identity.identity_id}",
"Logins": {
"${AWS_COGNITO_IDP_ENDPOINT}/${AWS_COGNITO_USERPOOL_ID}": "${this.aws_tokens.id_token}"
}
}`);
const response = await this._send("AWSCognitoIdentityService.GetCredentialsForIdentity", json_data);
if (response.status == 200) {
return new AWSCredentials(JSON.stringify(response.data['Credentials']));
}
}
}
class JciHitachiAWSIoTConnection extends JciHitachiAWSHttpConnection {
constructor(aws_tokens, log) {
super(log);
this.aws_tokens = aws_tokens;
}
_generateAWSIOTHeaders(need_access_token) {
let headers = {};
if (need_access_token) {
headers = {
"authorization": `Bearer ${this.aws_tokens.id_token}`,
"accesstoken": `Bearer ${this.aws_tokens.access_token}`,
"User-Agent": "Dalvik/2.1.0",
"content-type": "application/json",
"Accept": "application/json",
};
}
else {
headers = {
"authorization": `Bearer ${this.aws_tokens.id_token}`,
"User-Agent": "Dalvik/2.1.0",
"content-type": "application/json",
"Accept": "application/json",
};
}
return headers;
}
_handle_response(response) {
if (response.status == 200) {
this.log.debug(`login_req: ${JSON.stringify(response.data)}`);
return response;
}
else {
this.log.error(`login_req: ${JSON.stringify(response.data)}`);
return response;
}
}
_send(target, data, need_access_token) {
const endpoint = `https://${AWS_IOT_ENDPOINT}${target}`;
return this.requestHttps(endpoint, 'post', this._generateAWSIOTHeaders(need_access_token), data);
}
}
class GetAllDevice extends JciHitachiAWSIoTConnection {
async get_data() {
const response = await this._send('/GetAllDevice', {}, false);
return new AWSThingDictionary(JSON.stringify(response.data), this.log);
}
}
class ListSubUser extends JciHitachiAWSIoTConnection {
async getHostUserID() {
const response = await this._send('/ListSubUser', {}, false);
const familyMemberList = response.data['results']['FamilyMemberList'];
for (const familyMember of familyMemberList) {
if (familyMember['isHost']) {
this.log.info(`Host User : ${familyMember['firstName']},${familyMember['lastName']}`);
return familyMember['userId'];
}
}
this.log.error(`No Host User : ${JSON.stringify(response.data)}`);
return '';
}
}
class GetAllGroup extends JciHitachiAWSIoTConnection {
async get_data() {
const response = await this._send('/GetAllGroup', {}, false);
return response.data;
}
}
class JciHitachiAWSAPI {
constructor(email, password, log) {
this.task_id = 0;
this.is_host = false;
this.last_received_time = 0;
this.isConnected = false;
this.isLoginFailed = false;
this.email = email;
this.password = password;
this.log = log;
}
deconstructor() {
this.Logout();
}
setCallback(callback) {
this.callback = callback;
}
async Login() {
try {
await this.Logout();
if (this.isConnected) {
return true;
}
this.aws_tokens = await (new JciHitachiAWSCognitoConnection(this.email, this.password, undefined, this.log)).login(false);
if (!this.aws_tokens) {
this.log.info('Login failed');
return false;
}
this.aws_identity = await (new GetUser(this.email, this.password, this.aws_tokens, this.log)).get_data();
this.aws_thing_dict = await (new GetAllDevice(this.aws_tokens, this.log)).get_data();
this.log.debug("aws_identity:" + JSON.stringify(this.aws_identity));
if (this.aws_identity) {
this.aws_credentials = await (new GetCredentials(this.email, this.password, this.aws_tokens, this.log)).get_data(this.aws_identity);
this.log.debug(JSON.stringify(this.aws_identity) + ` host_user_id: ${this.aws_identity.host_identity_id}`);
this.is_host = this.aws_identity.identity_id === this.aws_identity.host_identity_id;
if (this.aws_credentials && this.aws_identity.host_identity_id.length > 0) {
this.log.debug(JSON.stringify(this));
this.mqttclient = this.createMQTTClient();
}
if (this.mqttclient) {
const attemptingConnect = (0, events_1.once)(this.mqttclient, "attemptingConnect");
const connectionSuccess = (0, events_1.once)(this.mqttclient, "connectionSuccess");
this.mqttclient.start();
await attemptingConnect;
await connectionSuccess;
const suback = await this.mqttclient.subscribe({
subscriptions: [
{ qos: QOS, topicFilter: `${this.aws_identity.host_identity_id}/+/+/response` }
]
});
this.log.debug('Suback result: ' + JSON.stringify(suback));
await this.RefeshAWSThingDictionary('registration');
await this.RefeshAWSThingDictionary('status');
return true;
}
}
}
catch (e) {
this.log.error(`Login Error: ${e}`);
}
return false;
}
async Logout() {
var _a;
try {
this.isConnected = false;
if (this.mqttclient) {
const unsuback = await this.mqttclient.unsubscribe({
topicFilters: [
`${(_a = this.aws_identity) === null || _a === void 0 ? void 0 : _a.host_identity_id}/#`
]
});
this.log.debug('Unsuback result: ' + JSON.stringify(unsuback));
const disconnection = (0, events_1.once)(this.mqttclient, "disconnection");
const stopped = (0, events_1.once)(this.mqttclient, "stopped");
this.mqttclient.stop();
await disconnection;
await stopped;
this.mqttclient = undefined;
}
return true;
}
catch (e) {
this.mqttclient = undefined;
this.log.error(`Logout Error: ${e}`);
}
return true;
}
get isHost() {
return this.is_host;
}
getDevices() {
return this.aws_thing_dict;
}
getDevice(thingName) {
var _a;
return (_a = this.aws_thing_dict) === null || _a === void 0 ? void 0 : _a.getDevice(thingName);
}
async RefeshAWSThingDictionary(actionName = 'status') {
if (!this.aws_thing_dict) {
return;
}
for (const thingName in this.aws_thing_dict.getAllThings()) {
this.publish(thingName, actionName);
}
}
async RefeshDevice(thingName) {
var _a;
if (this.last_received_time != 0 && Math.ceil(Date.now() / 1000) - this.last_received_time > 600) {
this.log.error('MQTT Connection Timeout');
await this.Logout();
this.log.info('Re-Login');
await this.Login();
return false;
}
if ((_a = this.aws_thing_dict) === null || _a === void 0 ? void 0 : _a.hasThingName(thingName)) {
return await this.publish(thingName, 'status');
}
return false;
}
async GetDeviceStatus(thingName, status_name, need_refresh = false) {
var _a;
if (need_refresh) {
await this.RefeshDevice(thingName);
}
const device = (_a = this.aws_thing_dict) === null || _a === void 0 ? void 0 : _a.getDevice(thingName);
if (!device || device.statusPayload === undefined) {
return undefined;
}
return device.statusPayload[status_name];
}
async SetDeviceStatus(thingName, status_name, status_value) {
const payload = {
"Condition": {
"ThingName": thingName,
"Index": 0,
"Geofencing": {
"Arrive": null,
"Leave": null
},
},
"TaskID": this.task_id++,
"Timestamp": Math.ceil(Date.now() / 1000)
};
payload[status_name] = status_value;
return await this.publish(thingName, 'control', payload);
}
handleMQTTMessage(topic, payload) {
try {
const topic_parts = topic.split('/');
const thingName = topic_parts[1];
const actionName = topic_parts[2];
const actionType = topic_parts[3];
const payloadContent = payload ? JSON.parse((0, util_utf8_browser_1.toUtf8)(payload)) : {};
this.log.debug(`Received: ${topic} ${JSON.stringify(payloadContent)}`);
if (actionType !== 'response') {
return;
}
if (this.aws_thing_dict === undefined) {
return;
}
this.last_received_time = Math.ceil(Date.now() / 1000);
if (this.getDevice(thingName)) {
if (actionName === 'status') {
this.aws_thing_dict.updateDeviceStatusPayload(thingName, payloadContent);
if (this.callback) {
this.callback(this.getDevice(thingName));
}
}
else if (actionName === 'registration') {
this.aws_thing_dict.updateDeviceRegistrationPayload(thingName, payloadContent);
}
else if (actionName === 'control') {
this.RefeshDevice(thingName);
}
}
}
catch (e) {
this.log.error(`MQTT Message Error: ${e}`);
}
}
createMQTTClient() {
if (this.aws_credentials === undefined || this.aws_identity === undefined) {
throw new Error('aws_credentials is undefined');
}
const wsConfig = {
credentialsProvider: aws_iot_device_sdk_v2_1.auth.AwsCredentialsProvider.newStatic(this.aws_credentials.access_key_id, this.aws_credentials.secret_access_key, this.aws_credentials.session_token),
region: AWS_REGION
};
const builder = aws_iot_device_sdk_v2_1.iot.AwsIotMqtt5ClientConfigBuilder.newWebsocketMqttBuilderWithSigv4Auth(AWS_MQTT_ENDPOINT, wsConfig);
const clientId = `${this.aws_identity.identity_id}_${generateRandomHex(16)}`;
this.log.debug(`clientId: ${clientId}`);
builder.withConnectProperties({ keepAliveIntervalSeconds: 120, clientId: `${clientId}` });
const client = new aws_iot_device_sdk_v2_1.mqtt5.Mqtt5Client(builder.build());
client.on('error', (error) => {
this.log.error("Error event: " + error.toString());
this.isConnected = false;
this.isLoginFailed = true;
});
client.on("messageReceived", (eventData) => {
this.handleMQTTMessage(eventData.message.topicName, eventData.message.payload);
});
client.on('attemptingConnect', (eventData) => {
this.log.debug("Attempting Connect event");
});
client.on('connectionSuccess', (eventData) => {
this.log.debug("Connection Success event");
this.log.debug("Connack: " + JSON.stringify(eventData.connack));
this.log.debug("Settings: " + JSON.stringify(eventData.settings));
this.isConnected = true;
});
client.on('connectionFailure', (eventData) => {
this.log.error("Connection failure event: " + eventData.error.toString());
this.isConnected = false;
this.isLoginFailed = true;
//throw new Error("Connection failure event: " + eventData.error.toString());
if (this.callback) {
this.callback(undefined);
}
});
client.on('disconnection', (eventData) => {
this.log.debug("Disconnection event: " + eventData.error.toString());
if (eventData.disconnect !== undefined) {
this.log.debug('Disconnect packet: ' + JSON.stringify(eventData.disconnect));
}
this.isConnected = false;
if (this.callback) {
this.callback(undefined);
}
});
client.on('stopped', (eventData) => {
this.log.debug("Stopped event");
});
return client;
}
async publish(thingName, request, payload = undefined) {
var _a;
try {
const defaultPayload = JSON.stringify({ "Timestamp": Math.ceil(Date.now() / 1000) });
if (this.mqttclient && this.isConnected) {
const topic = `${(_a = this.aws_identity) === null || _a === void 0 ? void 0 : _a.host_identity_id}/${thingName}`;
const qosPublishRegistrationResult = await this.mqttclient.publish({
qos: QOS,
topicName: `${topic}/${request}/request`,
payload: payload ? JSON.stringify(payload) : defaultPayload
});
this.log.debug(`${topic}/${request}/request ${payload ? JSON.stringify(payload) : defaultPayload}`);
return true;
}
}
catch (e) {
this.log.error(`Publish Error: ${e}`);
this.Logout();
if (this.callback) {
this.callback(undefined);
}
}
return false;
}
}
exports.default = JciHitachiAWSAPI;
//# sourceMappingURL=jci-hitachi-aws-api.js.map