UNPKG

homebridge-smartsystem

Version:

SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)

316 lines (259 loc) 10.1 kB
import { System } from "../duotecno/system"; import { PowerBaseConfig, Rule, Binding, Sanitizers, Boundaries, Action, kEmptyBinding, kEmptyRule } from "../duotecno/types"; import { err, debug, log } from "../duotecno/logger"; import { Base } from "./base"; import { Context } from "./webapp"; // Power base class // used by Smappee, Shelly PM, P1 Meter, ... // Johan Coppieters, Jul 2022. // const kPercentChange = 0.05; export class PowerBase extends Base { system: System; config: PowerBaseConfig; rules: Array<Rule>; bindings: Array<Binding>; switches: { [nodeId: number]: { value: boolean, name: string } }; realtimeCounter: number; production: number; consumption: number; voltages: { [phaseId: number]: number }; realtime: { totalPower: number, totalReactivePower: number, totalExportEnergy: number, totalImportEnergy: number, monitorStatus: number, utcTimeStamp: number } channels: { [publishIndex: number]: { name: string, type: string, flow: string, voltage: number, power: number, exportEnergy: number, importEnergy: number, phaseId: number, current: number, apparentPower: number, cosPhi: number, formula: string} }; constructor(type: string, system: System) { super(type); // called by Base: this.readConfig(); this.system = system; // will grow when we encounter one in the mqtt stream or if we receive a config message this.switches = {}; this.channels = {}; this.voltages = {}; this.realtime = <any>{}; this.realtimeCounter = 0; this.rules = (this.config.rules || []).map(rule => Sanitizers.ruleConfig(rule)); this.bindings = (this.config.bindings || []).map(binding => Sanitizers.bindingConfig(binding)); this.init(true).then(() => { log("power", "Power[" + type + "] created and started"); }); } // // start and restart services // async init(firstTime: boolean) { if (! firstTime) await this.unsubscribe(); this.realtimeCounter = 0; return this.subscribe(); } async subscribe(): Promise<void> { // overwrite } async unsubscribe(): Promise<void> { // overwrite } // // return data for ejs pages // async getData(context: Context, message: String): Promise<any> { return { config: this.config, bindings: this.bindings, rules: this.rules, realtimeCounter: this.realtimeCounter, message, realtime: this.realtime, voltages: this.voltages, switches: this.switches, channels: this.channels } } // // config mgt // async updateConfig(config: any) { if (config.address) this.config.address = config.address; this.writeConfig(); return this.init(false); } evalPower(exp: string): number { let parsed = exp.split(/(\+|-)/); let result = this.channels[parseInt(parsed[0])||0]?.power ?? 0; for (let i=1; i < parsed.length; i=i+2) { if (parsed[i] === "-") result -= this.channels[parseInt(parsed[i+1])||0]?.power ?? 0; else if (parsed[i] === "+") result += this.channels[parseInt(parsed[i+1])||0]?.power ?? 0; } debug("power", "evalPower: " + exp + " -> " + parsed + " = " + result); return result; } ////////////////////////////////////////////// // Bindings to Duotecno virtual temp sensor // ////////////////////////////////////////////// significant(current: number, previous: number): boolean { // no values yet -> don't update if (isNaN(current)) return false; // no previous value -> always update if (isNaN(previous)) return true; // less than 1000W -> update on: difference > 5% previous if (current < 1000) return Math.abs(previous - current) > (previous * 0.05); // more than 1000W -> update on: difference > 2% previous if (current >= 1000) return Math.abs(previous - current) > (previous * 0.02); return false; } applyBindings() { if (this.realtimeCounter % 2 === 0) { // for all bindings this.bindings.forEach( b => { const power = this.evalPower(b.channel); if (this.significant(power, b.value)) { debug("power", "APPLY to " + b.channel + " = " + b.value + " -> " + power + " for " + b.register + "): difference: " + Math.abs(b.value - power) + " > threshold: 5% =" + Math.abs(b.value * 0.05) + " > threshold: 2% =" + Math.abs(b.value * 0.02)); this.applyBinding(b, power) .then(power => { b.value = power; debug("power", "set register " + b.register + " to " + power); }) .catch((e) => err(this.type, e.message || e)); // } else { // debug("power", "DONT to " + b.channel + " = " + b.power + " -> " + power + " for " + b.register + // " : difference: " + Math.abs(b.power - power) + // " > threshold: 5% =" + Math.abs(b.power * 0.05) + " > threshold: 2% =" + Math.abs(b.power * 0.02)); } }); } } async applyBinding(binding: Binding, value: number): Promise<number> { const master = this.system.findMaster(binding.masterAddress, binding.masterPort); if (master) { await master.setRegisterValue(binding.register, value); return value; } else if ((!binding.masterAddress) || (binding.register)) { throw new Error("***** MASTER NOT FOUND for binding for value " + value + " to " + binding.masterAddress + ":" + binding.masterPort + " - register = " + binding.register + " *****"); } // else empty binding -> no error } ///////////////////// // Rules execution // ///////////////////// applyRules() { if (this.realtimeCounter % 60 === 0) { log("power", this.type + " > RealTime totals: Power = " + this.realtime.totalPower + ", Export = " + this.realtime.totalExportEnergy + ", Import = " + this.realtime.totalImportEnergy); } if (this.realtimeCounter % 5 === 0) { // for all rules this.rules.forEach( rule => { let power = 0; // if about power if (rule.type === "power") { // add all channels this rule refers to power = this.evalPower(rule.channel); } else if (rule.type === "sun") { // calc fake power usage power = this.production - this.consumption; } debug("power", "checking " + rule.type + "-rule with channel = " + rule.channel + " = " + power + "W"); this.checkPowerRule(power, rule); }); } } checkPowerRule(power: number, rule: Rule) { let newCurrent; if (isNaN(power)) return; if (power < rule.low) { newCurrent = Boundaries.kLow; } else if (power > rule.high) { newCurrent = Boundaries.kHigh; } else { newCurrent = Boundaries.kMid; } rule.power = power; if (newCurrent != rule.current) { debug("power", "triggered rule for channel: " + rule.channel + ", power = " + power + ", low: " + rule.low + ", high: " + rule.high + ", current: " + rule.current + " -> new: " + newCurrent); // should be done after successful applyCommand... rule.current = newCurrent; if (newCurrent < rule.actions.length) { this.applyCommand(rule.actions[newCurrent]) .then((val) => { debug("power", "## sent command -> " + rule.type + ", power = " + power + ", channel = " + rule.channel + " -> current = " + newCurrent + " (unit: " + rule.actions[newCurrent].name + ", value: " + rule.actions[newCurrent].value + ")"); rule.current = newCurrent; }) .catch((e) => err(this.type, e.message)); } } } async applyCommand(action: Action) { const unit = this.system.findUnit(this.system.findMaster(action.masterAddress, action.masterPort), action.logicalNodeAddress, action.logicalAddress); if (unit) { await unit.setState(action.value); } else if ((action.logicalNodeAddress != 0) || (action.logicalAddress != 0)) { throw new Error("***** UNIT NOT FOUND for action for value " + action.value + " to " + action.masterAddress + " - " + action.logicalNodeAddress + ";" + action.logicalAddress + " *****"); } } // // Rule mgt // updateRules() { // sort on channel. this.rules.sort((a,b) => parseInt(a.channel)-parseInt(b.channel)); // sanitize and copy all rules into the config this.config.rules = []; this.rules.forEach(r => this.config.rules.push(Sanitizers.ruleConfig(r))); debug("power", "config.rules = " + JSON.stringify(this.config.rules)); this.writeConfig(); } newRule(): Rule { const rule = {... kEmptyRule}; this.rules.push(rule); return rule; } updateRule(inx: number, rule: Rule) { if (inx < this.rules.length) { this.rules[inx] = rule; this.updateRules(); } } deleteRule(inx: number) { if (inx < this.rules.length) { this.rules.splice(inx, 1); this.updateRules(); } } // // Binding mgt // updateBindings() { // sort on channel. this.bindings.sort((a,b) => a.register-b.register); // sanitize and copy all rules into the config this.config.bindings = this.bindings.map(b => Sanitizers.bindingConfig(b)); debug("power", "config.bindings = " + JSON.stringify(this.config.bindings)); this.writeConfig(); } newBinding(): Binding { const binding = {... kEmptyBinding}; this.bindings.push(binding); return binding; } updateBinding(inx: number, binding: Binding) { if (inx < this.bindings.length) { this.bindings[inx] = binding; this.updateBindings(); } } deleteBinding(inx: number) { if (inx < this.bindings.length) { this.bindings.splice(inx, 1); this.updateBindings(); } } }