node-red-contrib-smartnora
Version:
Google Smart Home integration via Smart Nora https://smart-nora.eu/
150 lines (149 loc) • 7.79 kB
JavaScript
"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;