UNPKG

node-red-contrib-smartnora

Version:

Google Smart Home integration via Smart Nora https://smart-nora.eu/

150 lines (149 loc) 7.79 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FirebaseDevice = void 0; const nora_firebase_common_1 = require("@andrei-tatar/nora-firebase-common"); const database_1 = require("firebase/database"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const __1 = require(".."); const async_commands_registry_1 = require("./async-commands.registry"); const safe_update_1 = require("./safe-update"); class FirebaseDevice { constructor(cloudId, sync, device, logger, disableValidationErrors) { this.cloudId = cloudId; this.sync = sync; this.device = device; this.logger = logger; this.disableValidationErrors = disableValidationErrors; this.connectedAndSynced = false; this.pendingUpdate = true; this._state$ = new rxjs_1.Observable(observer => { const stateSubscription = (0, database_1.onValue)(this.state, s => observer.next(s.val())); const noraSubscription = (0, database_1.onValue)(this.noraSpecific, s => { var _a; if (this.connectedAndSynced) { this.device.noraSpecific = (_a = s.val()) !== null && _a !== void 0 ? _a : {}; } }); return () => { stateSubscription(); noraSubscription(); }; }).pipe((0, operators_1.filter)(v => !!v && typeof v === 'object'), (0, __1.singleton)()); this._localStateUpdate$ = new rxjs_1.Subject(); this._localAsyncCommand$ = new rxjs_1.Subject(); this.state$ = this._state$.pipe((0, operators_1.map)(({ state }) => state)); this.stateUpdates$ = (0, rxjs_1.merge)(this._state$.pipe((0, operators_1.filter)(({ update }) => update.by !== 'client' && this.connectedAndSynced), (0, operators_1.distinctUntilChanged)((a, b) => a.update.timestamp === b.update.timestamp), (0, operators_1.map)(({ state }) => state), (0, operators_1.tap)(state => { this.device.state = Object.assign({}, state); })), this._localStateUpdate$); this.connectedAndSynced$ = (0, rxjs_1.defer)(() => { this.connectedAndSynced = true; return (0, rxjs_1.concat)((0, rxjs_1.defer)(async () => { if (this.pendingUpdate) { await this.syncState(); } }), rxjs_1.NEVER); }).pipe((0, operators_1.finalize)(() => this.connectedAndSynced = false), (0, __1.singleton)()); this.error$ = new rxjs_1.Observable(observer => { const ref = (0, database_1.child)(this.noraSpecific, 'error'); return (0, database_1.onValue)(ref, s => { var _a, _b, _c; const value = s.val(); if (value) { (_a = this.logger) === null || _a === void 0 ? void 0 : _a.trace(`[nora][${this.device.id}] error syncing device: ${JSON.stringify((_b = value === null || value === void 0 ? void 0 : value.details) !== null && _b !== void 0 ? _b : {})}`); } observer.next((_c = value === null || value === void 0 ? void 0 : value.msg) !== null && _c !== void 0 ? _c : null); }); }); this.local$ = new rxjs_1.Subject(); this.state = (0, database_1.child)(this.sync.states, this.device.id); this.noraSpecific = (0, database_1.child)(this.sync.noraSpecific, this.device.id); this.asyncCommands$ = (0, rxjs_1.merge)(this._localAsyncCommand$, async_commands_registry_1.AsyncCommandsRegistry.getCloudAsyncCommandHandler(this)); } updateState(update, mapping) { return this.updateStateInternal(update, { mapping }); } async sendNotification(notification) { await this.sync.sendGoogleHomeNotification(this.device.id, notification); } async executeCommand(command, params) { var _a, _b; this.local$.next(true); let updates = null; if (((_a = this.device.noraSpecific) === null || _a === void 0 ? void 0 : _a.asyncCommandExecution) === true || Array.isArray(this.device.noraSpecific.asyncCommandExecution) && this.device.noraSpecific.asyncCommandExecution.includes(command)) { const commandId = `${this.device.id}:${new Date().getTime()}`; const response = async_commands_registry_1.AsyncCommandsRegistry.getLocalResponse(commandId, this.device); this._localAsyncCommand$.next({ id: commandId, command: { command, params }, }); const result = await (0, rxjs_1.firstValueFrom)(response); if (result.errorCode) { throw new nora_firebase_common_1.ExecuteCommandError(result.errorCode); } else { updates = { updateState: result.state, result: result.result, }; } } else { updates = (0, nora_firebase_common_1.executeCommand)({ command, params, device: this.device }); (_b = this.logger) === null || _b === void 0 ? void 0 : _b.trace(`[nora][local-execution][${this.device.id}] executed ${command}`); } if (updates === null || updates === void 0 ? void 0 : updates.updateState) { this.updateStateInternal(updates.updateState).catch(err => { var _a; return (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn(`error while executing local command, ${err.message}: ${err.stack}`); }); this._localStateUpdate$.next(this.device.state); return this.device.state; } return Object.assign(Object.assign({}, this.device.state), updates === null || updates === void 0 ? void 0 : updates.result); } async updateStateInternal(update, { mapping } = {}) { var _a; if (typeof update !== 'object') { return false; } const currentState = this.device.state; const safeUpdate = {}; (0, safe_update_1.getSafeUpdate)({ update, currentState, safeUpdateObject: safeUpdate, isValid: () => (0, nora_firebase_common_1.validate)(this.device.traits, 'state-update', safeUpdate).valid, mapping, warn: (msg) => { var _a; return !this.disableValidationErrors && ((_a = this === null || this === void 0 ? void 0 : this.logger) === null || _a === void 0 ? void 0 : _a.warn(`[${this.device.name.name}] ignoring property ${msg}`)); }, }); const { hasChanges, state } = (0, nora_firebase_common_1.updateState)(safeUpdate, this.device.state); if (hasChanges) { const { valid } = (0, nora_firebase_common_1.validate)(this.device.traits, 'state', state); if (!valid) { const name = this.device.name.name; const safeStr = JSON.stringify(safeUpdate); const stateStr = JSON.stringify(state); (_a = this === null || this === void 0 ? void 0 : this.logger) === null || _a === void 0 ? void 0 : _a.warn(`[${name}] invalid state after update. aborting update. ${safeStr} => ${stateStr}`); return; } this.device.state = state; await this.syncState(); } return true; } async syncState() { if (!this.connectedAndSynced) { this.pendingUpdate = true; return; } try { this.pendingUpdate = false; await this.sync.updateState(this.device.id, this.device.state); } catch (err) { this.pendingUpdate = true; throw err; } } } exports.FirebaseDevice = FirebaseDevice;