UNPKG

homebridge-jci-hitachi-platform

Version:

Homebridge platform plugin providing HomeKit support for Jci Hitachi air conditioners.

611 lines 24.4 kB
"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