UNPKG

homebridge-smartsystem

Version:

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

822 lines (676 loc) 24.3 kB
// 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; }