UNPKG

iobroker.bshb

Version:

Connects Bosch Smart Home Interface-Processes to ioBroker

455 lines 20.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Bshb = void 0; const utils = __importStar(require("@iobroker/adapter-core")); const bshb_controller_1 = require("./bshb-controller"); const rxjs_1 = require("rxjs"); const migration_1 = require("./migration"); const utils_1 = require("./utils"); const client_cert_1 = require("./client-cert"); const bosch_smart_home_bridge_1 = require("bosch-smart-home-bridge"); const log_level_1 = require("./log-level"); const fs = __importStar(require("fs")); /** * @author Christopher Holomek * @since 27.09.2019 */ class Bshb extends utils.Adapter { bshbController; pollingTrigger = new rxjs_1.BehaviorSubject(true); pollTimeout = null; startPollingTimeout = null; alive = new rxjs_1.Subject(); constructor(options = {}) { super({ ...options, name: 'bshb', }); this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); this.on('unload', this.onUnload.bind(this)); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { // Overwrite configuration // make sure that identifier is valid regarding Bosch T&C this.log.silly('onReady called. Load configuration'); if (!this.config.identifier) { this.config.identifier = bosch_smart_home_bridge_1.BshbUtils.generateIdentifier(); this.updateConfig({ identifier: this.config.identifier, }).then(); } this.config.host = this.config.host ? this.config.host.trim() : ''; const notPrefixedIdentifier = this.config.identifier ? this.config.identifier.trim() : ''; this.config.identifier = 'ioBroker.bshb_' + notPrefixedIdentifier; this.config.systemPassword = this.config.systemPassword ? this.config.systemPassword.trim() : ''; this.config.certsPath = this.config.certsPath ? this.config.certsPath.trim() : ''; if (typeof this.config.skipServerCertificateCheck === 'undefined') { this.config.skipServerCertificateCheck = false; } else if (this.config.skipServerCertificateCheck) { this.log.warn('Server certificate check skipped due to configuration. Use at your own risk.'); } // The adapters config (in the instance object everything under the attribute "native") is accessible via // this.config: this.log.debug('config host: ' + this.config.host); this.log.debug('config identifier: ' + this.config.identifier); this.log.debug('config systemPassword: ' + (this.config.systemPassword != undefined)); this.log.debug('config pairingDelay: ' + this.config.pairingDelay); if (this.config.rateLimit) { // When I started testing the rateLimit was a string. So we add the ability to parse it if (typeof this.config.rateLimit === 'string') { try { this.config.rateLimit = parseInt(this.config.rateLimit); } catch (e) { this.config.rateLimit = 1000; } this.updateConfig({ rateLimit: this.config.rateLimit, }).then(); } this.log.debug('config rateLimit: ' + this.config.rateLimit); } else { this.log.info('rateLimit is NOT set'); this.config.rateLimit = 1000; this.log.debug('config rateLimit not set using default: 1000'); this.updateConfig({ rateLimit: this.config.rateLimit, }).then(); } if (!notPrefixedIdentifier) { throw utils_1.Utils.createError(this.log, 'Identifier not defined but it is a mandatory parameter.'); } this.loadCertificates(notPrefixedIdentifier).subscribe({ next: clientCert => { this.handleAdapterInformation(); // Create controller for bosch-smart-home-bridge this.bshbController = new bshb_controller_1.BshbController(this, clientCert.certificate, clientCert.privateKey); this.init(this.bshbController); }, error: error => { this.log.error(utils_1.Utils.handleError('Could not initialize adapter. See more details in error', error)); }, }); } /** * load certificates:<br/> * 1. load from system.certificates<br/> * 2. If not found search for old configuration to allow smooth migration<br/> * 3. If not found generate a new certificate<br/> * * @param notPrefixedIdentifier * identifier without "ioBroker.bshb_" prefix which is used for system.certificates */ loadCertificates(notPrefixedIdentifier) { return new rxjs_1.Observable(subscriber => { this.getForeignObject('system.certificates', (err, obj) => { if (err || !obj) { subscriber.error(utils_1.Utils.createError(this.log, 'Could not load certificates. This should not happen. Error: ' + err)); subscriber.complete(); return; } const certificateKeys = utils_1.Utils.getCertificateKeys(notPrefixedIdentifier); const clientCert = new client_cert_1.ClientCert(obj.native.certificates[certificateKeys.cert], obj.native.certificates[certificateKeys.key]); if (clientCert.certificate && clientCert.privateKey) { this.readCertificate(clientCert, subscriber); } else { this.generateCertificate(clientCert, obj, certificateKeys, subscriber); } }); }); } generateCertificate(clientCert, obj, certificateKeys, subscriber) { // no certificates found. this.log.info('Could not find client certificate. Check for old configuration'); const migrationResult = this.migration(); if (migrationResult) { clientCert = migrationResult; } else { this.log.info('No client certificate found in old configuration or it failed. Generate new certificate'); clientCert = Bshb.generateCertificate(); } // store information this.storeCertificate(obj, certificateKeys, clientCert).subscribe({ next: () => { subscriber.next(clientCert); subscriber.complete(); }, error: error => { subscriber.error(error); subscriber.complete(); }, }); } readCertificate(clientCert, subscriber) { // found certificates this.log.info('Client certificate found in system.certificates'); this.log.info('Check if certificate is file reference or actual content'); const actualCert = this.loadFromFile(clientCert.certificate, 'certificate'); const actualPrivateKey = this.loadFromFile(clientCert.privateKey, 'private key'); clientCert = new client_cert_1.ClientCert(actualCert, actualPrivateKey); subscriber.next(clientCert); subscriber.complete(); } loadFromFile(file, type) { try { if (fs.existsSync(file)) { this.log.info(`${type} is a file reference. Read from file`); return fs.readFileSync(file, 'utf-8'); } else { this.log.info(`${type} seems to be actual content. Use value from state.`); return file; } } catch (e) { this.log.info(`${type} seems to be actual content or reading from file failed. Use value from state. For more details restart adapter with debug log level.`); this.log.debug(`Error during reading file: ${e}`); return file; } } storeCertificate(obj, certificateKeys, clientCert) { // store information obj.native.certificates[certificateKeys.cert] = clientCert.certificate; obj.native.certificates[certificateKeys.key] = clientCert.privateKey; return (0, rxjs_1.from)(this.setForeignObjectAsync('system.certificates', obj)).pipe((0, rxjs_1.catchError)(err => { throw utils_1.Utils.createError(this.log, 'Could not store client certificate in system.certificates due to an error:' + err); }), (0, rxjs_1.tap)(() => this.log.info('Client certificate stored in system.certificates.')), (0, rxjs_1.map)(() => undefined)); } migration() { // migration: const certsPath = this.config.certsPath; if (!certsPath) { // We abort if we could nof find the certificate path return undefined; } this.log.info(`Found old configuration in certsPath: ${certsPath}. Try to read information`); try { const result = new client_cert_1.ClientCert(migration_1.Migration.loadCertificate(this, certsPath), migration_1.Migration.loadPrivateKey(this, certsPath)); this.log.info(`Load client certificate from old configuration successful. Consider removing them from: ${certsPath}. They are not needed anymore`); return result; } catch (err) { // something went wrong we abort. Logging was already done. return undefined; } } static generateCertificate() { const certificateDefinition = bosch_smart_home_bridge_1.BshbUtils.generateClientCertificate(); return new client_cert_1.ClientCert(certificateDefinition.cert, certificateDefinition.private); } init(bshbController) { // start pairing if needed bshbController .pairDeviceIfNeeded(this.config.systemPassword) .pipe((0, rxjs_1.catchError)((err) => { this.log.error(utils_1.Utils.handleError('Something went wrong during initialization', err)); return rxjs_1.EMPTY; }), // Everything is ok. We check for devices first (0, rxjs_1.switchMap)(() => bshbController.startDetection().pipe((0, rxjs_1.catchError)((err) => { this.log.error(utils_1.Utils.handleError('Something went wrong during detection', err)); return rxjs_1.EMPTY; }))), (0, rxjs_1.takeUntil)(this.alive)) .subscribe({ next: () => { this.log.info('Subscribe to ioBroker states'); // register for changes this.subscribeStates('*'); // now we want to subscribe to BSHC for changes this.startPolling(bshbController); }, }); } poll = (delay) => { delay = delay ? delay : 0; this.pollTimeout = setTimeout(() => { this.pollTimeout = null; this.pollingTrigger.next(true); }, delay); }; startPolling = (bshbController, delay) => { this.log.info('Listen to changes'); delay = delay ? delay : 0; this.startPollingTimeout = setTimeout(() => { this.startPollingTimeout = null; this.subscribeAndPoll(bshbController); }, delay); }; handleAdapterInformation() { this.setObjectNotExists('info', { type: 'channel', common: { name: 'Information', }, native: {}, }, (_err, obj) => { if (obj) { // channel created we create all other stuff now. this.setObjectNotExists('info.connection', { type: 'state', common: { name: 'If connected to BSHC', type: 'boolean', role: 'indicator.connected', read: true, write: false, def: false, }, native: {}, }, (_err, obj) => { if (obj) { // we start with disconnected this.setState('info.connection', { val: false, ack: true }); } }); } }); } updateInfoConnectionState(connected) { this.getState('info.connection', (_err, state) => { if (state) { if (state.val === connected) { return; } } this.setState('info.connection', { val: connected, ack: true }); }); } subscribeAndPoll = (bshbController) => { this.pollingTrigger.next(false); this.pollingTrigger.complete(); this.pollingTrigger = new rxjs_1.BehaviorSubject(true); bshbController .getBshcClient() .subscribe() .subscribe(response => { this.pollingTrigger.subscribe(keepPolling => { if (keepPolling) { bshbController .getBshcClient() .longPolling(response.parsedResponse.result, 30000, 2000) .subscribe({ next: infoResponse => { if (infoResponse.incomingMessage.statusCode !== 200) { this.updateInfoConnectionState(false); if (infoResponse.incomingMessage.statusCode === 503) { this.log.warn(`BSHC is starting. Try to reconnect asap. HTTP=${infoResponse.incomingMessage.statusCode}, data=${infoResponse.parsedResponse}`); } else { this.log.warn(`Something went wrong during long polling. HTTP=${infoResponse.incomingMessage.statusCode}, data=${infoResponse.parsedResponse}`); } // something went wrong we delay polling this.poll(10000); } else { this.updateInfoConnectionState(true); const information = infoResponse.parsedResponse; // handle updates information.result.forEach(resultEntry => { if (utils_1.Utils.isLevelActive(this.log.level, log_level_1.LogLevel.debug)) { this.log.debug(JSON.stringify(resultEntry)); } bshbController.setStateAck(resultEntry); }); // poll further data. this.poll(); } }, error: error => { this.updateInfoConnectionState(false); if (error.errorType === bosch_smart_home_bridge_1.BshbErrorType.POLLING) { const bshbError = error; if (bshbError.cause && bshbError.cause instanceof bosch_smart_home_bridge_1.BshbError) { if (bshbError.errorType === bosch_smart_home_bridge_1.BshbErrorType.TIMEOUT) { this.log.info('LongPolling connection timed-out before BSHC closed connection. Try to reconnect.'); } else if (bshbError.errorType === bosch_smart_home_bridge_1.BshbErrorType.ABORT) { this.log.warn('Connection to BSHC closed by adapter. Try to reconnect.'); } else { this.log.warn('Something went wrong during long polling. Try to reconnect.'); } } else { this.log.warn('Something went wrong during long polling. Try to reconnect.'); } this.startPolling(bshbController, 5000); } else { this.log.warn('Something went wrong during long polling. Try again later.'); this.poll(10000); } }, }); } else { bshbController .getBshcClient() .unsubscribe(response.parsedResponse.result) .subscribe(() => { this.updateInfoConnectionState(false); }); } }); }); }; /** * Is called when adapter shuts down - callback has to be called under any circumstances! */ onUnload(callback) { try { this.log.info('unloading...'); this.alive.next(false); this.alive.complete(); // we want to stop polling. So false this.pollingTrigger.next(false); this.pollingTrigger.complete(); // and we clear timeouts as well if (this.pollTimeout) { clearTimeout(this.pollTimeout); this.pollTimeout = null; } if (this.startPollingTimeout) { clearTimeout(this.startPollingTimeout); this.startPollingTimeout = null; } this.bshbController?.close(); this.log.info('unload complete'); callback(); } catch (e) { callback(); } } /** * Is called if a subscribed state changes */ onStateChange(id, state) { if (!this.bshbController) { this.log.warn('Could not handle state change because controller was not initialized yet: ' + id); return; } if (state) { if (!state.ack) { // The state was changed this.log.debug(`state ${id} changed: ${state.val} (ack = ${state.ack})`); this.bshbController.setState(id, state); } } else { // The state was deleted // Currently we do not need this } } } exports.Bshb = Bshb; if (require.main !== module) { // Export the constructor in compact mode module.exports = (options) => new Bshb(options); } else { // otherwise start the instance directly (() => new Bshb())(); } //# sourceMappingURL=main.js.map