homebridge-smartsystem
Version:
SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)
822 lines (676 loc) • 24.3 kB
text/typescript
// Johan Coppieters.
// v1 - server version, Apr 2019
// v2 - app version, Jan 2020
// v3 - smart server version, Mar 2020
// v3.1 - added scenes from app version, May 2020
import { kEmptyProxy, ProxyConfig } from "../server/proxy";
import { Unit } from "./protocol";
// for Active=Y/N or New=X
export type YNX = "Y" | "N" | "X";
export type YN = "Y" | "N";
// Node types
export enum NodeType { kNoNode = 0, kStandardNode = 1, kGatewayNode = 4, kModemNode = 8, kGUINode = 32 };
// States
export enum UnitState { kOpening = 4, kClosing = 3, kOpen = 2, kClosed = 1, kStopped = 0 };
export enum UnitMotorCmd { kClose = 5, kOpen = 4, kStop = 3};
export enum UnitType {
kNoType = 0, kDimmer = 1, kSwitch = 2, kInput = 3,
kTemperature = 4, kExtendedAudio = 5, kMood = 7, kSwitchingMotor = 8,
kAudio = 10, kAV = 11, kIRTX = 12, kVideo = 14
};
export enum UnitExtendedType /* extends UnitType */ {
kNoType = 0, kDimmer = 1, kSwitch = 2, kInput = 3,
kTemperature = 4, kExtendedAudio = 5, kMood = 7, kSwitchingMotor = 8,
kAudio = 10, kAV = 11, kIRTX = 12, kVideo = 14,
kPower = 24,
kLightbulb = 101, kCondition = 102,
kGarageDoor = 201, kDoor = 202, kLock = 203, kUnlocker = 204
};
// kPower == kTemperature name starting with "<" or ">"
// kLightbulb == kSwitch with no "*" or "$" in the name
// kDoor == kSwitchingMotor with "*" in the name
// kGarageDoor == kSwitchingMotor with "$" in the name
// kCondition == kMood with "*" in the name
// kLock == kMood with $ in the name
// kUnlocker = kMood with $ in the name
//////////////
// Duotecno //
//////////////
// Configuration (see duotecno/system.ts)
export interface SystemConfig {
socketserver: string;
socketport: number;
mood: string;
debug?: boolean;
cmasters: Array<MasterConfig>;
cunits: Array<UnitConfig>;
};
export interface MasterConfig {
name?: string;
address: string;
port: number;
password: string;
debug?: boolean;
active: boolean;
nodenames: { [node: number]: string };
};
export interface NodeConfig {
active: YNX; // if found with "N": don't even query, "X": new node
masterAddress: string; // address of master node
masterPort: number; // port of master node
logicalAddress?: number;
};
export interface UnitDef {
name: string;
masterAddress: string;
masterPort: number;
logicalAddress: number;
logicalNodeAddress: number;
displayName?: string;
extendedType?: UnitExtendedType;
//unit?: Unit;
};
export const kEmptyUnit: UnitDef = { masterAddress: "0.0.0.0", masterPort: 5001,
name: "unit", logicalAddress: 0, logicalNodeAddress: 0 } as const;
export interface UnitConfig extends UnitDef {
active: YN; // export / don't export to homebridge
used: YN; // load into the active units
group: number; // id of group
};
export interface UnitSetting extends UnitConfig {
value: number;
};
export interface UnitScene extends UnitDef {
value: number | boolean;
};
export const kEmptyUnitScene: UnitScene = { ...kEmptyUnit, value: true} as const;
export interface SceneConfig {
name: string;
trigger: UnitScene;
order: number;
units: Array<UnitScene>;
};
export const kEmptyScene: SceneConfig = {name: 'Scene', trigger: {... kEmptyUnitScene}, order: 0, units: []};
export interface GroupConfig {
name: string;
id: number;
visible: boolean;
order: number;
};
export const kEmptyGroup: GroupConfig = {name: "Home", id: 0, order: 0, visible: true} as const;
// for OS settings: IP settings - dhcp, ...
export interface NetworkConfig {
ip1: string; gateway1: string;
ip2?: string; gateway2?: string;
nameservers: string;
};
export const kEmptyNetworkConfig = { ip1: "", ip2: "", gateway1: "", gateway2: "", nameservers: "" } as const;
export interface SettingsConfig {
network: NetworkConfig;
};
export const kEmptySettings: SettingsConfig = { network: kEmptyNetworkConfig } as const;
// from Protocol//Hardware to Nodes and Units
export interface DBInfo {
nrNodes: number;
};
export interface NodeInfo {
name?: string;
index?: number;
logicalAddress?: number;
physicalAddress?: number;
nrUnits?: number;
type?: NodeType;
flags?: number;
};
export interface UnitInfo {
name?: string;
displayName?: string;
logicalNodeAddress?: number;
index?: number;
logicalAddress?: number;
physicalAddress?: number;
type?: UnitType;
extendedType?: UnitExtendedType;
flags?: number;
};
// handlers from incoming and outgoing messages
export type Message = Array<number>;
export type ISignature = {
status: boolean,
logicalNodeAddress: number,
logicalAddress: number
};
export type PromiseObject = {
resolver: (result: Message) => void,
rejecter: (result: Error) => void,
signature: ISignature
};
export enum WriteError { writeFatal = -1, writeError = 0, writeOK = 1 }
export type OkFunction = (ok: WriteError) => void;
export interface CommRecord {
isStatus: boolean;
cmd: number;
message: Message;
rest: string;
}
export const kEmptyCommRecord = { status: false, cmd: -1, message: [-1,0,0], rest: ""};
//////////////
// SmartApp //
//////////////
export enum Boundaries {kLow = 0, kMid = 1, kHigh = 2};
export enum RuleType {kPower = "power", kCurrent = "current", kWater = "water"};
///////////
// Rules //
///////////
export interface Action extends UnitDef {
value: number | boolean;
}
export const kEmptyAction: Action = {
...kEmptyUnit, value: false
} as const;
export interface Rule {
type: string;
channel: string;
low: number;
high: number;
current?: number;
power?: number;
actions: Array<Action>;
};
export const kEmptyRule: Rule = {
type: "power", channel: "0", low: 30, high: 900,
actions: [{...kEmptyUnit, value: false}, {...kEmptyUnit, value: 50}, {...kEmptyUnit, value: true}]
};
// Switches
export enum SwitchType {
kNoType = "",
kSmappee = "smappee",
kRF = "RF",
kHTTPSwitch = "http",
kHTTPDimmer = "httpdim",
kHTTPUpDown = "httpupdown",
kOhSwitch = "oswitch",
kOhDimmer = "odimmer",
kOhUpDown = "oupdown",
kSomfy = "somfy"
}
export interface Switch extends UnitDef {
unitName: string;
type: SwitchType;
plug: number | string;
data: string;
header: string;
method: string;
username?: string;
password?: string;
unit?: Unit;
status?: number | boolean;
value?: number | boolean | string;
nomacro: string;
nostop: string;
}
export const kEmptySwitch: Switch = {
...kEmptyUnit, plug: 0, type: SwitchType.kNoType, unitName: "", name: "", data: "", method: "GET",
header: "", username: "", password: "", nomacro: "N", nostop: "N"
} as const;
// Links
export enum LinkType {
kNoType = "",
kAccessory = "accessory"
}
export interface Link extends UnitDef{
accName: string;
accId: string;
accService: Array<string>; // e.g. ["On", "Brightness"], ["TargetPosition", "CurrentPosition", ...]
type: LinkType;
max?: number;
min?: number;
unit?: Unit;
dtValue?: number | boolean | string;
hbValue?: number | boolean | string
}
export const kEmptyLink: Link = {
...kEmptyUnit, type: LinkType.kNoType, max: 100, min: 0, accName: "acc", accId: "", accService: []
};
///////////
// Power //
///////////
export interface Binding {
masterAddress: string;
masterPort: number;
register: number;
channel: string;
value?: number;
}
export const kEmptyBinding: Binding = {
channel: "0", masterAddress: "0.0.0.0", masterPort: 5001, register: 0
} as const;
export interface PowerBaseConfig extends BaseConfig {
address: string;
rules: Array<Rule>;
bindings: Array<Binding>;
};
export interface SmappeeConfig extends PowerBaseConfig {
uid: string;
}
export interface P1Config extends PowerBaseConfig {
}
export interface ShellyConfig extends PowerBaseConfig {
addresses: string;
}
/////////////////////
// Smartapp config //
/////////////////////
export interface BaseConfig {
debug?: boolean;
}
export interface SmartAppConfig extends BaseConfig {
port: number;
apiHost: string;
apiPort: number;
username: string; password: string;
switches: Array<Switch>;
links: Array<Link>;
}
////////////////
// Homebridge //
////////////////
// coming from Homebridge or from unit tests
export type LogFunction = (message: any, ...optionalParams: any[]) => void;
// needed as return objects to Homebridge
export interface Bridge {
name: string,
username: string, // CC:22:3D:E3:A3:03
port: number,
pin: string // "577-02-003"
};
export interface PlatformConfig extends BaseConfig {
manufacturer: string;
platform: string;
[x: string]: any; // for other platforms
};
export interface AccessoryConfig {
accessory: string;
name: string;
[x: string]: any; // for other accessories
};
export interface HomebridgeConfig {
bridge: Bridge;
description: string;
platforms?: Array<PlatformConfig>;
accessories?: Array<AccessoryConfig>;
password: string;
};
//////////////////
// SocketServer //
//////////////////
export interface ServerConfig extends BaseConfig {
port: number;
}
/////////////
// Smappee //
/////////////
export function actionValue(val: string | boolean | number): boolean | number {
if ((typeof val === "number") || (typeof val === "boolean"))
return val;
let x = parseInt(val);
if (isNaN(x))
return (val === "true") || (val === "TRUE") || (val === "True");
else
return x;
}
export function actionValueStr(val: boolean | number): string {
if (typeof val === "boolean")
return (val) ? "true" : "false";
else
return val.toString();
}
////////////////
// Sanitizers //
////////////////
export const Sanitizers = {
accessory: function(config: AccessoryConfig): AccessoryConfig {
if (!config) config = <AccessoryConfig>{};
config.accessory = config.accessory || "DuotecnoAccessory";
config.name = config.name || "Duotecno-Coppieters";
// for sp4pro -> "device": "/dev/serial0", "baudrate": "9600"
return config;
},
platform: function(config: PlatformConfig): PlatformConfig {
if (!config) config = <PlatformConfig>{};
config.manufacturer = config.manufacturer || "Duotecno-Coppieters";
config.platform = config.platform || "DuotecnoPlatform";
config.smappee = config.smappee || false;
config.p1 = config.p1 || false;
config.shelly = config.shelly || false;
config.smartapp = config.smartapp || null;
config.system = Sanitizers.system(config.system);
config.proxy = Sanitizers.proxy(config.proxy, config.system);
return config;
},
homebridge: function(config: HomebridgeConfig): HomebridgeConfig {
if (!config) config = <HomebridgeConfig>{};
config.bridge = config.bridge || <Bridge>{};
config.bridge.name = config.bridge.name || "Duotecno Bridge";
config.bridge.username = config.bridge.username || "CC:22:3D:E3:A3:01";
config.bridge.port = config.bridge.port || 51827;
config.bridge.pin = config.bridge.pin || "577-03-001";
config.platforms = config.platforms || [<PlatformConfig>{}];
for (let p in config.platforms)
if (config.platforms[p].platform == "DuotecnoPlatform")
config.platforms[p] = Sanitizers.platform(config.platforms[p]);
config.accessories = []; //config.accessories || [<AccessoryConfig>{}];
//for (let a in config.accessories) Sanitizers.accessory(config.accessories[a]);
return config;
},
settings: function(config: SettingsConfig): SettingsConfig {
if (!config) config = <SettingsConfig>{};
config.network = Sanitizers.networkConfig(config.network);
return config;
},
networkConfig: function(config: NetworkConfig): NetworkConfig {
if (!config) config = <NetworkConfig>{};
config.ip1 = config.ip1 || kEmptyNetworkConfig.ip1;
config.ip2 = config.ip2 || kEmptyNetworkConfig.ip2;
config.gateway1 = config.gateway1 || kEmptyNetworkConfig.gateway1;
config.gateway2 = config.gateway2 || kEmptyNetworkConfig.gateway2;
config.nameservers = config.nameservers || kEmptyNetworkConfig.nameservers;
return config;
},
server: function(config: ServerConfig): ServerConfig {
if (!config) return {debug: true, port: 9999};
config.port = config.port || 9999;
if (typeof config.port === "string") config.port = parseInt(config.port);
if (typeof config.debug === "undefined") config.debug = true;
return config;
},
smartapp: function(config: SmartAppConfig): SmartAppConfig {
if (!config) config = <SmartAppConfig>{};
config.port = config.port || 5002;
config.switches = config.switches || [];
config.switches = config.switches.map(sw => Sanitizers.switchConfig(sw));
config.links = config.links || [];
config.links = config.links.map(sw => Sanitizers.linkConfig(sw));
config.debug = config.debug || false;
config.apiHost = config.apiHost || "localhost";
config.apiPort = config.apiPort || 8581;
config.username = config.username || "admin";
config.password = config.password || "";
return config;
},
powerbase: function(config: PowerBaseConfig): PowerBaseConfig {
if (!config) config = <PowerBaseConfig>{};
config.debug = config.debug || false;
config.rules = config.rules || [];
config.rules = config.rules.map(sw => Sanitizers.ruleConfig(sw));
config.bindings = config.bindings || [];
config.bindings = config.bindings.map(b => Sanitizers.bindingConfig(b));
config.address = config.address || "";
return config;
},
smappee: function(config: SmappeeConfig): SmappeeConfig {
if (!config) config = <SmappeeConfig>{};
const cfg = <SmappeeConfig> Sanitizers.powerbase(config);
cfg.uid = config.uid || "--none--";
return cfg;
},
p1: function(config: P1Config): P1Config {
if (!config) config = <P1Config>{};
const cfg = Sanitizers.powerbase(config);
return cfg;
},
shelly: function(config: ShellyConfig): ShellyConfig {
if (!config) config = <ShellyConfig>{};
const cfg = <ShellyConfig> Sanitizers.powerbase(config);
cfg.addresses = config?.addresses || "";
return cfg;
},
linkConfig: function(aLink): Link {
const link = <Link>Sanitizers.unitDef(aLink);
link.accId = aLink.accId || "";
link.accName = aLink.accName || "";
link.accService = aLink.accService || [];
link.type = aLink.type || kEmptyLink.type;
link.max = aLink.max || kEmptyLink.max;
link.min = aLink.min || kEmptyLink.min;
return link;
},
switchConfig: function(aSwitch): Switch {
const sw = <Switch>Sanitizers.unitDef(aSwitch);
sw.nomacro = aSwitch.nomacro || "N";
sw.nostop = aSwitch.nostop || "N";
sw.name = aSwitch.name || "--";
sw.unitName = aSwitch.unitName || "";
sw.value = aSwitch.value || 0;
sw.data = aSwitch.data || "";
sw.header = aSwitch.header || "";
sw.method = aSwitch.method || "GET";
sw.type = aSwitch.type || SwitchType.kNoType;
// don't destroy 0 plug
if (typeof aSwitch.plug === "string")
sw.plug = aSwitch.plug || "";
return sw;
},
makeSwitchConfig: function(aSwitch): Switch {
return Sanitizers.switchConfig({ name: aSwitch.name, unitName: aSwitch.unitName,
masterAddress: aSwitch.masterAddress, masterPort: aSwitch.masterPort,
nomacro: aSwitch.nomacro, nostop: aSwitch.nostop,
logicalAddress: aSwitch.logicalAddress, logicalNodeAddress: aSwitch.logicalNodeAddress,
type: aSwitch.type, plug: aSwitch.plug,
data: aSwitch.data, method: aSwitch.method, header: aSwitch.header });
},
makeLinkConfig: function(aLink): Link {
return Sanitizers.linkConfig({ name: aLink.name, unitName: aLink.unitName,
masterAddress: aLink.masterAddress, masterPort: aLink.masterPort,
logicalAddress: aLink.logicalAddress, logicalNodeAddress: aLink.logicalNodeAddress,
accName: aLink.accName, accService: aLink.accService, accId: aLink.accId, type: aLink.type });
},
ruleConfig(rule): Rule {
rule = {... rule};
rule.channel = rule.channel || "0";
rule.low = makeInt(rule.low);
rule.high = makeInt(rule.high);
if (typeof rule.actions === "undefined") rule.actions = [];
while (rule.actions.length < 2) rule.actions.push({... kEmptyAction});
rule.actions.forEach( action => {
action.value = actionValue(action.value);
action.logicalAddress = makeInt(action.logicalAddress);
action.logicalNodeAddress = makeInt(action.logicalNodeAddress);
if (typeof action.name != "string") action.name = "";
});
return rule;
},
bindingConfig(binding): Binding {
const b = <Binding>{... kEmptyBinding};
b.masterAddress = binding.masterAddress || '',
b.register = binding.register || 0,
b.masterPort = binding.masterPort || 5001,
b.channel = binding.channel || "0";
return b;
},
///////////
// Proxy //
///////////
proxy: function(config?: ProxyConfig, system?: SystemConfig): ProxyConfig {
const c = kEmptyProxy as ProxyConfig;
c.cloudServer = config?.cloudServer || kEmptyProxy.cloudServer;
c.cloudPort = config?.cloudPort || kEmptyProxy.cloudPort;
c.masterAddress = config?.masterAddress || system?.cmasters[0]?.address || "";
c.masterPort = config?.masterPort || system?.cmasters[0]?.port || 5001;
c.uniqueId = config?.uniqueId || kEmptyProxy.uniqueId;
return c;
},
//////////////
// Duotecno //
//////////////
masterConfig: function(config?: MasterConfig): MasterConfig {
config = (config) ? {...config} : <MasterConfig>{};
config.name = config.name || "IP Master";
config.address = config.address || "";
config.port = config.port || 0;
if (typeof config.port === "string") config.port = parseInt(config.port);
config.password = config.password || "";
config.debug = config.debug || false;
if (typeof config.active === "undefined") config.active = true;
config.active = !!config.active;
config.nodenames = config.nodenames || {};
return config;
},
system: function(config?: SystemConfig): SystemConfig {
if (!config) config = <SystemConfig>{};
config.mood = config.mood || "sfeer";
config.debug = config.debug || false;
config.socketserver = config.socketserver || 'ws.duotecno.eu';
// correct old settings
if (config.socketserver == 'akiworks.be') config.socketserver = 'ws.duotecno.eu';
config.socketport = config.socketport || 9999;
config.cmasters = config.cmasters || [];
config.cmasters = config.cmasters.map(m => Sanitizers.masterConfig(m));
config.cunits = config.cunits || [];
config.cunits = config.cunits.map(u => Sanitizers.unitConfig(u));
return config;
},
//////////
// Node //
nodeConfig: function(config?: NodeConfig) {
if (!config) config = <NodeConfig>{};
config.active = config.active || "N";
config.masterAddress = config.masterAddress || "";
config.masterPort = config.masterPort || 5001;
config.logicalAddress = config.logicalAddress || 0;
return config;
},
//////////
// Unit //
unitDef: function(info: UnitDef) {
return {
logicalNodeAddress: info.logicalNodeAddress || 0,
logicalAddress: info.logicalAddress || 0,
masterAddress: info.masterAddress || '',
masterPort: info.masterPort || 5001,
name: info.name || ('unit '+info.logicalNodeAddress+":"+info.logicalAddress),
displayName: info.displayName || info.name || ('unit '+info.logicalNodeAddress+":"+info.logicalAddress),
extendedType: info.extendedType || UnitExtendedType.kNoType
};
},
unitConfig: function(config?: UnitConfig) { // unitDef + active + group
if (!config) { config = <UnitConfig>{}; }
const cfg = <UnitConfig>Sanitizers.unitDef(config);
cfg.active = config.active || 'N';
cfg.used = config.used || config.active;
if (typeof config.group === 'string') { config.group = parseInt(config.group); }
cfg.group = config.group || 0;
return cfg;
},
unitScene: function(config?: UnitScene) { // unitDef + value
// change + create new clean record for writing to config files
if (!config) { config = <UnitScene>{}; }
const cfg = <UnitScene>Sanitizers.unitDef(config);
if (typeof config.value === "undefined") cfg.value = 0;
if (typeof config.value === "string") cfg.value = parseInt(config.value);
cfg.name = config.name || "New scene";
return { logicalNodeAddress: cfg.logicalNodeAddress,
logicalAddress: cfg.logicalAddress,
masterAddress: cfg.masterAddress,
masterPort: cfg.masterPort,
value: cfg.value,
name: cfg.name };
},
////////////
// Scenes //
sceneConfig: function(config?: SceneConfig): SceneConfig {
// don't change -> create new clean record for writing to config files
if (!config) { return {...kEmptyScene}; }
const newConfig: SceneConfig = {...kEmptyScene};
newConfig.name = config.name || kEmptyScene.name;
if (typeof config.order === 'string') { newConfig.order = parseInt(config.order); }
newConfig.order = config.order || kEmptyScene.order;
newConfig.trigger = Sanitizers.unitScene(config.trigger);
config.units = config.units || kEmptyScene.units;
newConfig.units = config.units.map(u => Sanitizers.unitScene(u));
return newConfig;
},
scenes: function(config?: Array<SceneConfig>): Array<SceneConfig> {
if (!config) { return [Sanitizers.sceneConfig()]; }
config = config.map(s => Sanitizers.sceneConfig(s));
return config;
},
///////////////////////////////////
// Data coming from the hardware //
nodeInfo: function(info: NodeInfo, into?: object) {
info.name = info.name || "";
info.index = info.index || -1;
info.logicalAddress = info.logicalAddress || 0;
info.physicalAddress = info.physicalAddress || 0;
info.type = info.type || NodeType.kNoNode;
info.flags = info.flags || 0;
info.nrUnits = info.nrUnits || 0;
if (into) {
Object.keys(info).forEach(prop => into[prop] = info[prop]);
}
return info;
},
unitInfo: function(info: UnitInfo, into?: Unit) {
info.name = info.name || "";
info.displayName = info.displayName || "";
info.index = info.index || -1;
info.logicalNodeAddress = info.logicalNodeAddress || 0;
info.logicalAddress = info.logicalAddress || 0;
info.type = info.type || UnitType.kNoType;
info.extendedType = info.extendedType || <UnitExtendedType><unknown>info.type;
info.flags = info.flags || 0;
if (into) {
Object.keys(info).forEach(prop => into[prop] = info[prop]);
}
return info;
}
};
//////////////////////
// Helper functions //
//////////////////////
export async function wait(sec) {
return new Promise(resolve => setTimeout(resolve, sec*1000));
}
export function ascii(char: string): number {
return char.charCodeAt(0);
}
export function char(ascii: number): string {
return String.fromCharCode(ascii)
}
export function two(n: number | string) {
return (+n < 10) ? ("0" + n) : n.toString();
}
export function hex(n: number): string {
n = Math.floor(n);
return "0x" + n.toString(16);
}
export function watt(w: number) {
if (w < 1000) return w + "W";
if (w < 1000000) return Math.floor(w/1000) + "." + (Math.round(w/100) % 10) + "KW";
if (w < 1000000000) return Math.floor(w/1000000) + "." + (Math.round(w/100000) % 10) + "MW";
return Math.floor(w/1000000000) + "." + (Math.round(w/100000000) % 10) + "GW";
}
export function date(aDate: Date) {
if (! aDate) return "-";
return two(aDate.getDate()) + "-" + two(aDate.getMonth() + 1) + "-" + aDate.getFullYear();
}
export function time(aDate: Date) {
if (! aDate) return "-";
return two(aDate.getHours()) + ":" + two(aDate.getMinutes()) + ":" + two(aDate.getSeconds());
}
export function datetime(aDate: Date) {
if (! aDate) return "-";
return date(aDate) + " " + time(aDate);
}
export function now(): string {
return datetime(new Date());
}
export function makeInt(val: string | number): number {
if (typeof val === "string") {
if ((val[0]==='0') && (val[1]==='x'))
val = parseInt(val.substr(2), 16);
else
val = parseInt(val, 10);
}
return isNaN(val) ? 0 : val;
}