homebridge-smartsystem
Version:
SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)
316 lines (259 loc) • 10.1 kB
text/typescript
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();
}
}
}