diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9a6b88b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog for homebridge-home-security-lite +This is the change log for the plugin, all relevant changes will be listed here. + +For documentation please see the [README](https://github.com/SteidlD/homebridge-home-security-lite/blob/master/README.md) + +## 0.3.0 +- Bugfix: Open window warning wasn't set back in certain cases +- JSDoc documentation added to code +- go public + +## 0.2.0 +- first private check-in diff --git a/HomeSecurityLitePlatform.js b/HomeSecurityLitePlatform.js new file mode 100644 index 0000000..8ce9731 --- /dev/null +++ b/HomeSecurityLitePlatform.js @@ -0,0 +1,259 @@ +// Implements the HomeSecurityLitePlatform class that is for keeping all the things together +// +//----------------------------------------------------------------------- +// Date Author Change +//----------------------------------------------------------------------- +// 15.05.2020 D.Steidl Created +// 22.05.2020 D.Steidl JSDoc added +//----------------------------------------------------------------------- + +//----------------------------------------------------------------------- +// Global variables +//----------------------------------------------------------------------- + +// variables have to be declared explicitly +'use strict' + +//----------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------- + +// from JavaScript + +// from HomeSecurityLite + +const cWindowAndDoorAccessory = require('./WindowAndDoorAccessory') +const cSecuritySystemAccessory = require('./SecuritySystemAccessory') + +//----------------------------------------------------------------------- +// Classes +//----------------------------------------------------------------------- + +/** + * Class for the Platform of the home security lite system + */ +class cHomeSecurityLitePlatform +{ + //----------------------------------------------------------------------- + /** + * Platform constructor + * + * @param {Object} cLog Pointer to logging class + * @param {Object} dConfig Configuration for the plugin + * @param {Object} cApi Pointer to the API for registering new accessory + * @returns {void} nothing + */ + constructor(cLog, dConfig, cApi) + { + var self = this; + cLog("HomeSecurityLite Init"); + + // Store and initialize values + self.cLog = cLog; + self.dConfig = dConfig; + self.cAccessories = []; // Store all accessories to find it again + this.bFastRemind = false; // State of the fast remind feature + + // Check if API pointer is valid + if (cApi) + { // API valid + // Save the API object as plugin needs to register new accessory via self object + self.cApi = cApi; + + // Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories. + // Platform Plugin should only register new accessory that doesn't exist in homebridge after this event. + // Or start discover new accessories. + self.cApi.on + ('didFinishLaunching', + function () + { + self.cLog("DidFinishLaunching"); + + // Analyse config, use config first, if not set then fall back to default values + var lWindowsNDoors = dConfig["windows_and_doors"]; // Configuration of the window and door sensors + var strHomeSecurityLiteName = dConfig["home_security_lite_name"] || "Home Security Lite"; // Name of the Home Security Lite accessory + var strSecuritySystemName = dConfig["security_system_name"] || "Security system"; // Name of the Security System accessory + var strFastCyclicReminderName = dConfig["fast_cyclic_reminder_name"] || "Remind me again fast"; // Name of the remind me fast switch + var strForgotWindowWarningName = dConfig["forgot_window_warning_name"] || "You forgot to close a window"; // Name of the opened window reminder + + // ------------------------------------------------------------------------ + // Initialize Home security lite accessory + // ------------------------------------------------------------------------ + + // Create a unique id for the home security lite accessory and try to get it from list of known accessories + var HomeSecurityLiteUUID = global.cUUIDGen.generate("name" + strHomeSecurityLiteName); // UUID of the Home Security Lite accessory + var cActAccessory = self.cAccessories[HomeSecurityLiteUUID]; // Pointer to the accessory if it was restored by homebridge + + if (cActAccessory === undefined) + { // If the accessory is not known to homebridge + self.cLog("Found new Home Security Lite accessory : %s", strHomeSecurityLiteName); + // Instantiate Accessory object + cActAccessory = new global.cAccessory(strHomeSecurityLiteName, HomeSecurityLiteUUID); + // Add a service for a switch input + cActAccessory.addService(global.cService.Switch, strFastCyclicReminderName); + // Hang accessory into list to find it again later + self.cAccessories[cActAccessory.UUID] = cActAccessory; + // Now register this accessory with homebridge + self.cApi.registerPlatformAccessories("homebridge-home-security-lite", "HomeSecurityLitePlatform", [cActAccessory]); + self.cLog("Added new Home Security Lite accessory switch: %s", strFastCyclicReminderName); + } + else + { // If the accessory is already known to homebridge + self.cLog("Home Security Lite accessory is online: %s", cActAccessory.displayName); + } + + // Set Information Accessory + var cInfo = cActAccessory.getService(global.cService.AccessoryInformation); // Temporary variable with the AccessoryInformation service + cInfo.setCharacteristic(global.cCharacteristic.Manufacturer, "D. Steidl"); + cInfo.setCharacteristic(global.cCharacteristic.Model, "HomeSecurityLite"); + cInfo.setCharacteristic(global.cCharacteristic.SerialNumber, "1"); + cInfo.setCharacteristic(global.cCharacteristic.FirmwareRevision, global.strFWVersion); + + // Initialize SwitchService (set value and connect setter and getter method) + cActAccessory.getService(global.cService.Switch) + .getCharacteristic(global.Characteristic.On) + .setValue(this.bFastRemind) + .on('get', function (fCallback) { fCallback(null, this.bFastRemind); }.bind(this)) + .on('set', this.setFastRemind.bind(this)); + + // ------------------------------------------------------------------------ + // Initialize System security accessory + // ------------------------------------------------------------------------ + + // Create a unique id for the security system accessory and try to get it from list of known accessories + var SecuritySystemUUID = global.cUUIDGen.generate("name" + strSecuritySystemName); // UUID of the Security System accessory + var cActAccessory = self.cAccessories[SecuritySystemUUID]; // Pointer to the accessory if it was restored by homebridge + + if (cActAccessory === undefined) + { // If the accessory is not known to homebridge + self.cLog("Found new security system : %s", strSecuritySystemName); + // Instantiate Accessory object + var cActAccessory = new global.cAccessory(strSecuritySystemName, SecuritySystemUUID); + // Add a security system + cActAccessory.addService(global.cService.SecuritySystem, strSecuritySystemName); + // Add a service for a contact sensor output (forgot_window_warning) + cActAccessory.addService(global.cService.ContactSensor, strForgotWindowWarningName); + // Hang accessory into list to find it again later + self.cAccessories[cActAccessory.UUID] = new cSecuritySystemAccessory(self.cLog, cActAccessory, dConfig); + // Now register this accessory with homebridge + self.cApi.registerPlatformAccessories("homebridge-home-security-lite", "HomeSecurityLitePlatform", [cActAccessory]); + self.cLog("Added new security system : %s", strSecuritySystemName); + } + else + { // If the accessory is already known to homebridge + self.cLog("Security system is online: %s", cActAccessory.displayName); + self.cAccessories[cActAccessory.UUID] = new cSecuritySystemAccessory(self.cLog, (cActAccessory instanceof cSecuritySystemAccessory ? cActAccessory.accessory : cActAccessory), dConfig); + } + + // ------------------------------------------------------------------------ + // Initialize window and door accessory + // ------------------------------------------------------------------------ + + if (lWindowsNDoors) + { // If there's a config for Window and door sensors + var iSerial = 0; // Fake serial number (staring by 1) + lWindowsNDoors.forEach + ( // Parse through all configured window and door sensors + function (dWinDoorConfig) + { // Function to initialize one window and door sensor accessory + if (dWinDoorConfig.name) + { // If there's a name in the config + // Create a unique id for the accessory and try to get it from list of known accessories + var UUID = global.cUUIDGen.generate("name" + dWinDoorConfig.name); // UUID of the window and door accessory + var cActAccessory = self.cAccessories[UUID]; // Pointer to the accessory if it was restored by homebridge + + // Calculate serial number + iSerial = iSerial + 1; + + if (cActAccessory === undefined) + { // If the accessory is not known to homebridge + self.cLog("Found new window or door : %s - %s", dWinDoorConfig.name, dWinDoorConfig.description); + + // Read the configuration, if not configured then fall back on default values + var strSwitchName = (dConfig["prefix_remind_me"] || "Remind me of" ) + " " + dWinDoorConfig.name; // Name of the switch + var strSensorName = (dConfig["prefix_reminder"] || "Reminder:" ) + " " + dWinDoorConfig.name; // Name of the sensor + var strAccessoryName = (dConfig["prefix_reminder_accessory"] || "Reminder accessory" ) + " " + dWinDoorConfig.name; // Name of the accessory + + // Instantiate Accessory object + var cActAccessory = new global.cAccessory(strAccessoryName, UUID); + // Add a service for a switch input + cActAccessory.addService(global.cService.Switch, strSwitchName); + // Add a service for a contact sensor output (delayed contact switch) + cActAccessory.addService(global.cService.ContactSensor, strSensorName); + // Hang accessory into list to find it again later + self.cAccessories[cActAccessory.UUID] = new cWindowAndDoorAccessory(self.cLog, cActAccessory, self.cAccessories[SecuritySystemUUID], dWinDoorConfig, dConfig, iSerial); + // Now register this accessory with homebridge + self.cApi.registerPlatformAccessories("homebridge-home-security-lite", "HomeSecurityLitePlatform", [cActAccessory]); + self.cLog("Added new window or door switch and contact sensor : %s - %s", strSwitchName, strSensorName); + } + else + { // If the accessory is already known to homebridge + self.cLog("Window or door is online: %s", cActAccessory.displayName); + self.cAccessories[cActAccessory.UUID] = new cWindowAndDoorAccessory(self.cLog, (cActAccessory instanceof cWindowAndDoorAccessory ? cActAccessory.accessory : cActAccessory), self.cAccessories[SecuritySystemUUID], dWinDoorConfig, dConfig, iSerial); + } + } + else { + // If there's no name in config then log an error + self.cLog("Window or door %s in configuration has no name", dWinDoorConfig); + } + } + ); + } + else { // If there's no windows or doors in config then log an error + self.cLog("No windows or doors found in configuration", dConfig); + } + }.bind(self) // function() + ); // self.cApi.on('didFinishLaunching', + } // if (cApi) + } + + /** + * Function invoked when homebridge tries to restore cached accessory. + * The accessory will be stored in a list identified by it's UUID to be processed later + * + * @param {Object} cActAccessory Accessory object restored from cache + * @returns {void} nothing + */ + configureAccessory(cActAccessory) + { + this.cLog(cActAccessory.displayName, "Configure Accessory"); + this.cAccessories[cActAccessory.UUID] = cActAccessory; + } + + /** + * "Set" function for fast remind. Value will be stored and distributed to all the window/door objects + * + * @param {boolean} bValue Value of the fast remind feature + * @param {function} fCallback Callback function to confirm setting of the value + * @returns {void} nothing + */ + setFastRemind(bValue, fCallback) + { + var self = this; + // Has value changed? + var bChanged = (self.bFastRemind != bValue); + if (bChanged) + { // If the value has changed + // Store value + self.bFastRemind = bValue; + self.cLog("%s - setting fast remind: %s", this.cActAccessory.displayName, (bValue ? "true" : "false")); + self.cAccessories.forEach + ( // Go through all the window and door accessories + function (cAccessory) + { + if (cAccessory instanceof cWindowAndDoorAccessory) + { // If the accessory has the right type, then give it the value + cAccessory.setFastRemind(bValue); + } + } + ); + } + fCallback(null, self.bFastRemind); + } +} + +//----------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------- + +module.exports = cHomeSecurityLitePlatform; diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c6a45d --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# homebridge-home-security-lite +A homebridge-plugin for using your available homekit sensors and actors to build a security system. + +### Features: +- include door and window sensors +- get reminded of opened windows after a configurable time +- get reminded of opened windows when leaving your home +- get an alarm if a window or door is opened when security system is armed + +## Installation: + +### 1. Install homebridge and home-security-lite plugin. +- 1.a `sudo npm install -g homebridge --unsafe-perm` +- 1.b `sudo npm install -g homebridge-home-security-lite` + +### 2. Update homebridge configuration file. +``` +"platforms": [ + { + "platform" : "HomeSecurityLite", + "name" : "HomeSecurityLite", + "plugin_map" : + { + "plugin_name" : "homebridge-home-security-lite" + }, + "prefix_remind_me" : "Remind me of", + "prefix_reminder" : "Reminder:", + "prefix_reminder_accessory" : "Remind timer", + "forgot_window_warning_name" : "Forgot open window or door", + "home_security_lite_name" : "Home Security Lite", + "security_system_name" : "Security system", + "fast_cyclic_reminder_name" : "Remind me fast again", + "windows_and_doors" : + [ + { + "name" : "bathroom window", + "description" : "input/output for the window contact sensor in the bathroom" + }, + { + "name" : "patio door", + "first_remind" : 1200, + "fast_cyclic_remind" : 180, + "slow_cyclic_remind" : 43200 + } + ], + "first_remind" : 900, + "fast_cyclic_remind" : 120, + "slow_cyclic_remind" : 1800 + } +] +``` diff --git a/SecuritySystemAccessory.js b/SecuritySystemAccessory.js new file mode 100644 index 0000000..bcecb62 --- /dev/null +++ b/SecuritySystemAccessory.js @@ -0,0 +1,327 @@ +// Implements the SecuritySystemAccessory class that is executing the security system +// +//----------------------------------------------------------------------- +// Date Author Change +//----------------------------------------------------------------------- +// 15.05.2020 D.Steidl Created +// 31.05.2020 D.Steidl JSDoc added +// 02.06.2020 D.Steidl Bugfix: Open window warning wasn't set back in certain cases +//----------------------------------------------------------------------- + +//----------------------------------------------------------------------- +// Global variables +//----------------------------------------------------------------------- + +// variables have to be declared explicitly +'use strict' + +/** @const {Object} LSTATES Strings for the logging of the actual security system state */ +const LSTATES = ["STAY_ARMED", "AWAY_ARMED", "NIGHT_ARMED", "DISARMED", "ALARM_TRIGGERED"]; +/** @const {Object} LTARGETSTATES Strings for the logging of the target security system state */ +const LTARGETSTATES = ["STAY_ARM", "AWAY_ARM", "NIGHT_ARM", "DISARM"]; + +//----------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------- + +// from JavaScript + +// from HomeSecurityLite + +//----------------------------------------------------------------------- +// Classes +//----------------------------------------------------------------------- + +/** + * This class provides a security system based on doors and windows. If you arm it (i.e. automatically when + * the last person leave your home), then it checks whether all of the doors and windows are closed. If not + * it will sent you a message to remind you, that you left a window or a door open. Otherwise - once armed - + * it will react on any door or window opening and will trigger the alarm. + */ +class cSecuritySystemAccessory +{ + /** + * Constructor of the class + * + * @param {Object} cLog Pointer to logging class + * @param {Object} cActAccessory Pointer to own accessory + * @param {Object} dConfig Configuration for the plugin + * @returns {void} Nothing + */ + constructor(cLog, cActAccessory, dConfig) + { + // Store values + this.cActAccessory = cActAccessory; // Pointer to logging class + this.cLog = cLog; // Pointer to own accessory + this.Config = dConfig; // Configuration for the plugin + + // Initialize + this.eTargetState = cCharacteristic.SecuritySystemTargetState.DISARM; // Target state of the security system (disarmed) + this.eCurrentState = cCharacteristic.SecuritySystemCurrentState.DISARMED; // Actual state of the security system (disarmed) + this.bCurrentSensorState = false; // Actual state of the reminder (off) + this.iTimeoutId = undefined; // Id of a set timeout to find it again (no timeout started) + this.lWindowStates = []; // Array with the states of the windows/doors + + // Set Information Accessory + var cInfo = this.cActAccessory.getService(global.cService.AccessoryInformation); // Temporary variable with the AccessoryInformation service + cInfo.setCharacteristic(global.cCharacteristic.Manufacturer, "D. Steidl"); + cInfo.setCharacteristic(global.cCharacteristic.Model, "SecuritySystem"); + cInfo.setCharacteristic(global.cCharacteristic.SerialNumber, "1"); + cInfo.setCharacteristic(global.cCharacteristic.FirmwareRevision, global.strFWVersion); + + // Initialize SecuritySystem (set values, getter and setter for the target state and the actual state) + this.cSecuritySystem = this.cActAccessory.getService(global.cService.SecuritySystem); // Pointer to the SecuritySystem service + this.cSecuritySystem + .getCharacteristic(global.cCharacteristic.SecuritySystemTargetState) + .setValue(this.eTargetState) + .on('get', this.getSecuritySystemTargetState.bind(this)) + .on('set', this.setSecuritySystemTargetState.bind(this)); + this.cSecuritySystem + .getCharacteristic(global.cCharacteristic.SecuritySystemCurrentState) + .setValue(this.eCurrentState) + .on('get', this.getSecuritySystemCurrentState.bind(this)); + + // Initialize the ContactSensor Service (set value and the getter method) + this.cContactSensorService = this.cActAccessory.getService(global.cService.ContactSensor); // Pointer to the ContactSensor service + this.cContactSensorService + .getCharacteristic(global.cCharacteristic.ContactSensorState) + .setValue(this.bCurrentSensorState) + .on('get', this.getCurrentSensorState.bind(this)); + + // Accessory is online now + // this.cActAccessory.updateReachability(true); + return; + } + + /** + * "Get" function for the target state. Delivers the stored state set before only + * + * @param {function} fCallback Callback function to give back the stored security system target state + * @returns {void} Nothing (value will be given by Callback) + */ + getSecuritySystemTargetState(fCallback) + { + var self = this; + self.cLog("%s - target state: %s", self.cActAccessory.displayName, LTARGETSTATES[self.eTargetState]); + // Give back stored state + fCallback(null, self.eTargetState); + return; + } + + /** + * "Set" function for the target state. + * + * @param {boolean} bValue New value for the security system target state + * @param {function} fCallback Callback function (send back value if value was set) + * @returns {void} Nothing (value will be given by Callback) + */ + setSecuritySystemTargetState(bValue, fCallback) + { + var self = this; + // Compare with old value + var bChanged = (self.eTargetState != bValue); // Value has been changed + + if (bChanged) + { // If value has changed + // Store value + self.eTargetState = bValue; + self.cLog("%s - setting target state: %s, Changed: %s", this.cActAccessory.displayName, LTARGETSTATES[self.eTargetState], (bChanged ? "true" : "false")); + // Run statemachine + self.RunStatemachine(bChanged, false); + } + fCallback(null, self.bCurrentSwitchState); + return; + } + + /** + * "Get" function for the current state. Delivers the state calculated by the state machine + * + * @param {function} fCallback Callback function to give back the stored security system state + * @returns {void} Nothing (value will be given by Callback) + */ + getSecuritySystemCurrentState(fCallback) + { + var self = this; + self.cLog("%s - current state: %s", self.cActAccessory.displayName, LSTATES[self.eCurrentState]); + // Give back stored state + fCallback(null, self.eCurrentState); + return; + } + + /** + * Function to actively change the current state of the security system + * + * @param {Enumerator} eValue New value of the actual security system state + * @returns {void} Nothing + */ + UpdateSecuritySystemCurrentState(eValue) + { + var self = this; + self.cLog("%s - updating current state to %s", this.cActAccessory.displayName, LSTATES[eValue]); + // Store value local and send it to homebridge + self.eCurrentState = eValue; + self.cSecuritySystem.getCharacteristic(global.cCharacteristic.SecuritySystemCurrentState).setValue(eValue, undefined, self.cActAccessory.context); + return; + } + + /** + * "Get" function for current state of the forgot_window_warning + * + * @param {function} fCallback Callback function to give back the actual state of the forgot_window_warning reminder + * @returns {void} Nothing (value will be given by Callback) + */ + getCurrentSensorState(fCallback) + { + var self = this; + self.cLog("%s - getting current forgot_window_warning state", this.cActAccessory.displayName); + // Give back current state + fCallback(null, self.bCurrentSensorState); + return; + } + + /** + * Function to actively change the state of the virtual window + * + * @param {boolean} bValue New state of the forgot_window_warning reminder + * @returns {void} Nothing + */ + UpdateCurrentSensorState(bValue) + { + var self = this; + self.cLog("%s - updating current forgot_window_warning state to %s", this.cActAccessory.displayName, (bValue ? "true" : "false")); + // Store value local and send it to homebridge + self.bCurrentSensorState = bValue; + self.cContactSensorService.getCharacteristic(global.cCharacteristic.ContactSensorState).setValue(bValue, undefined, self.cActAccessory.context); + return; + } + + /** + * Function that is called from the WindowAndDoorAccessory class every time a window state changes. The state of the corresponding + * window will be stored and the state machine will be run to see what effects this will take. + * + * @param {number} iSerial Serial number of the window or door + * @param {boolean} bState New state of the window or door + * @returns {void} Nothing + */ + setWindowState(iSerial, bState) + { + var self = this; + self.lWindowStates[iSerial] = bState; + self.RunStatemachine(false, false); + return; + } + + /** + * State machine that is doing the actual security system. + * + * @param {boolean} bChangedTargetState True if the statemachine is run because of a new target state + * @param {boolean} bTimeout True if the statemachine is run because of a timeout + * @returns Nothing + */ + RunStatemachine(bChangedTargetState, bTimeout) + { + var eNewCurrentState; // New actual state of the security system to be changed to + var bNewCurrentSensorState; // New reminder state to be changed to + var self = this; + + // If there's still a timeout running, then stop it + clearTimeout(self.iTimeoutId); + self.iTimeoutId = undefined; + + // In case of alarm triggered wait for disarm only + if ((self.eCurrentState == global.cCharacteristic.SecuritySystemCurrentState.ALARM_TRIGGERED) && + (self.eTargetState != global.cCharacteristic.SecuritySystemTargetState.DISARM)) + { + return; + } + + // Determine whether a window is opened + var bWindowOpened = false; + self.lWindowStates.forEach(bThisWindowOpened => { bWindowOpened = bWindowOpened || bThisWindowOpened; }); + + // The state machine + switch (self.eTargetState) + { + //--------------------------------------------------------------- + // STATE: Disarmed + //----------------- + case global.cCharacteristic.SecuritySystemTargetState.DISARM: + // Always go to new state without condition + eNewCurrentState = global.cCharacteristic.SecuritySystemCurrentState.DISARMED; + // If switched off then release forgot_window_warning + bNewCurrentSensorState = false; + break; + + //--------------------------------------------------------------- + // STATE: At home + //----------------- + case global.cCharacteristic.SecuritySystemTargetState.STAY_ARM: + // Always go to new state without condition + eNewCurrentState = global.cCharacteristic.SecuritySystemCurrentState.STAY_ARM; + // If I'm back home release forgot_window_warning + bNewCurrentSensorState = false; + break; + + //--------------------------------------------------------------- + // STATE: Away + //----------------- + case global.cCharacteristic.SecuritySystemTargetState.AWAY_ARM: + + if (self.eCurrentState != global.cCharacteristic.SecuritySystemCurrentState.AWAY_ARM) + { // If the security system is not armed yet, then try to arm it + if (bWindowOpened) + { // If a window is opened, then the security system cannot be activated and a warning for the opened window / door shall be sent + bNewCurrentSensorState = true; + } + else + { // otherwise switch of warning and activate the security system + bNewCurrentSensorState = false; + eNewCurrentState = global.cCharacteristic.SecuritySystemCurrentState.AWAY_ARM; + } + } + else + { // If it already was armed before and now a window opens, an alarm will be triggered + if (bWindowOpened) + eNewCurrentState = global.cCharacteristic.SecuritySystemCurrentState.ALARM_TRIGGERED; + } + break; + + //--------------------------------------------------------------- + // STATE: At night + //----------------- + case global.cCharacteristic.SecuritySystemTargetState.NIGHT_ARM: + + if (self.eCurrentState != global.cCharacteristic.SecuritySystemCurrentState.NIGHT_ARM) + { // If the security system is not armed for the night yet, then try to arm it + if (bWindowOpened) + { // If a window is opened, then the security system cannot be activated and a warning for the opened window / door shall be sent + bNewCurrentSensorState = true; + } + else + { // otherwise switch of warning and activate the security system + bNewCurrentSensorState = false; + eNewCurrentState = global.cCharacteristic.SecuritySystemCurrentState.NIGHT_ARM; + } + } + else + { // If it already was armed before and now a window opens, an alarm will be triggered + if (bWindowOpened) + eNewCurrentState = global.cCharacteristic.SecuritySystemCurrentState.ALARM_TRIGGERED; + } + break; + } + + // If values have been changed, call update functions + if (eNewCurrentState != this.eCurrentState) + self.UpdateSecuritySystemCurrentState(eNewCurrentState); + if (bNewCurrentSensorState != this.bCurrentSensorState) + self.UpdateCurrentSensorState(bNewCurrentSensorState); + return; + } +} + +//----------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------- + +module.exports = cSecuritySystemAccessory; diff --git a/WindowAndDoorAccessory.js b/WindowAndDoorAccessory.js new file mode 100644 index 0000000..cf9cbbf --- /dev/null +++ b/WindowAndDoorAccessory.js @@ -0,0 +1,253 @@ +// Implements the cWindowAndDoorAccessory class that is handling the door and window sensors, +// activating and deactivating the window reminders. +// +//----------------------------------------------------------------------- +// Date Author Change +//----------------------------------------------------------------------- +// 15.05.2020 D.Steidl Created +// 31.05.2020 D.Steidl JSDoc added +//----------------------------------------------------------------------- + +//----------------------------------------------------------------------- +// Global variables +//----------------------------------------------------------------------- + +// variables have to be declared explicitly +'use strict' + +/** @const {Object} ESTATES Enumeration for state machine */ +const ESTATES = {CLOSED: 1, OPENED: 2, REMIND: 3} + +//----------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------- + +// from JavaScript + +// from HomeSecurityLite + +//----------------------------------------------------------------------- +// Classes +//----------------------------------------------------------------------- + +/** + * Class for the window and door contact sensors of the home security lite system. This class provides a virtual switch + * as input for the state of a real contact sensor and one virtual contact sensor, that "opens" a virtual window with delay + * to the real window, so a message can be sent as a reminder to close the window. + */ +class cWindowAndDoorAccessory +{ + /** + * Constructor of the cWindowAndDoorAccessory class. This is reading in all the values and configuration and add all the + * services and characteristics. + * + * @param {Object} cLog Pointer to logging class + * @param {Object} cActAccessory Pointer to own accessory + * @param {Object} cSecuritySystem Pointer to the security system object + * @param {Object} dWinDoorConfig Configuration for this window / door sensor + * @param {Object} dConfig Configuration for the plugin + * @param {number} iSerial A fake serial number (staring with 1 counting up) for the AccessoryInformation Service + * @returns {void} Nothing + */ + constructor(cLog, cActAccessory, cSecuritySystem, dWinDoorConfig, dConfig, iSerial) + { + // Store values + this.cLog = cLog; // Pointer to logging class + this.cActAccessory = cActAccessory; // Pointer to own accessory + this.cSecuritySystem = cSecuritySystem; // Pointer to the security system object + this.dWinDoorConfig = dWinDoorConfig; // Configuration for this window / door sensor + this.iSerial = iSerial; // A fake serial number (staring with 1 counting up) for the AccessoryInformation Service + + // Read the configuration, use door/window config first, if not set then fall back to global config, if not set fall back to default values + this.iFirstRemind = dWinDoorConfig["first_remind"] || dConfig["first_remind"] || 900; // for remind of open window after x seconds + this.iFastCyclicRemind = dWinDoorConfig["fast_cyclic_remind"] || dConfig["fast_cyclic_remind"] || 120; // time in seconds for fast cyclic remind (i.e in winter) + this.iSlowCyclicRemind = dWinDoorConfig["slow_cyclic_remind"] || dConfig["slow_cyclic_remind"] || 1800; // time in seconds for slow cyclic remind + + // Initialize + this.bCurrentSwitchState = false; // State of the input switch (window closed) + this.cSecuritySystem.setWindowState(this.iSerial, this.bCurrentSwitchState); // Sent state to security system + this.bCurrentSensorState = false; // State of the reminder (switched off) + this.eState = ESTATES.CLOSED; // State of the state machine (window closed= + this.iTimeoutId = undefined; // Id of a started timeout to find it again (No Timeout started yet) + this.bFastRemind = false; // Use fast remind (don't use) + + // Set Information Accessory + var cInfo = this.cActAccessory.getService(global.cService.AccessoryInformation); + cInfo.setCharacteristic(global.cCharacteristic.Manufacturer, "D. Steidl"); + cInfo.setCharacteristic(global.cCharacteristic.Model, "ContactSensorDelayer"); + cInfo.setCharacteristic(global.cCharacteristic.SerialNumber, this.iSerial.toString()); + cInfo.setCharacteristic(global.cCharacteristic.FirmwareRevision, global.strFWVersion); + + // Initialize SwitchService (set value and connect get and set methods) + this.cSwitchService = this.cActAccessory.getService(global.cService.Switch); + this.cSwitchService + .getCharacteristic(global.cCharacteristic.On) + .setValue(this.bCurrentSwitchState) + .on('get', this.getCurrentSwitchState.bind(this)) + .on('set', this.setCurrentSwitchState.bind(this)); + + // Initialize ContactSensor Service (set value and connect get method) + this.cContactSensorService = this.cActAccessory.getService(global.cService.ContactSensor); + this.cContactSensorService + .getCharacteristic(global.cCharacteristic.ContactSensorState) + .setValue(this.bCurrentSensorState) + .on('get', this.getCurrentSensorState.bind(this)); + + // Accessory is online now + // this.cActAccessory.updateReachability(true); + return; + } + + /** + * "Get" function for the switch state. Delivers the stored state set before only + * + * @param {function} fCallback Callback funtion pointer to sent back the value + * @returns {void} Nothing (value will be given by Callback) + */ + getCurrentSwitchState(fCallback) + { + var self = this; + self.cLog("%s - current state: %s", self.cActAccessory.displayName, (self.bCurrentSwitchState ? "true" : "false")); + // Give back stored state + fCallback(null, self.bCurrentSwitchState); + return; + } + + // + /** + * "Set" function for the switch state. On the rising edge (window opened) a timer will be started with the configured + * "remind_after_secs" delay. + * + * @param {boolean} bValue Window state (true = open, false = closed) + * @param {function} fCallback Callback function (send back value if value was set) + * @returns {void} Nothing (value will be given by Callback) + */ + setCurrentSwitchState(bValue, fCallback) + { + var self = this; + // Compare with old value + var bChanged = (self.bCurrentSwitchState != bValue); // Value has been changed + + if (bChanged) + { // If value changed + // Store value + self.bCurrentSwitchState = bValue; + // Inform the security system + self.cSecuritySystem.setWindowState(self.iSerial, self.bCurrentSwitchState); + self.cLog("%s - setting switch: %s, Changed: %s", this.cActAccessory.displayName, (bValue ? "true" : "false"), (bChanged ? "true" : "false")); + // Run statemachine + self.RunStatemachine(false); + } + // always acknowledge the reception of the value + fCallback(null, self.bCurrentSwitchState); + return; + } + + /** + * "Get" function for current state of the virtual window + * + * @param {function} fCallback Callback function (sent back actual state of the reminder) + * @returns {void} Nothing (value will be given by Callback) + */ + getCurrentSensorState(fCallback) + { + var self = this; + self.cLog("%s - getting current window/door state", this.cActAccessory.displayName); + // Give back current state + fCallback(null, self.bCurrentSensorState); + return; + } + + /** + * Function to actively change the state of the virtual window + * + * @param {boolean} bValue New state of the reminder + * @returns {void} Nothing + */ + UpdateCurrentSensorState(bValue) + { + var self = this; + self.cLog("%s - updating current window/door state to %s", this.cActAccessory.displayName, (bValue ? "true" : "false")); + // Store value local and send it to homebridge + self.bCurrentSensorState = bValue; + self.cContactSensorService.getCharacteristic(global.cCharacteristic.ContactSensorState).setValue(bValue, undefined, self.cActAccessory.context); + return; + } + + /** + * "Set" function for the fast reminder switch + * + * @param {boolean} bValue New value of the fast reminder switch + * @returns {void} Nothing + */ + setFastRemind(bValue) + { + this.bFastRemind = bValue; + return; + } + + /** + * State machine that is doing the reminder feature + * + * @param {boolean} bTimeout Will be set if the statemachine is run by the runout timeout, otherwise it's false + * @returns {void} Nothing + */ + RunStatemachine(bTimeout) + { + var self = this; + + // If there's still a timeout running, then stop it + clearTimeout(self.iTimeoutId); + self.iTimeoutId = undefined; + + // The state machine + switch (self.eState) + { + case ESTATES.CLOSED: + // The window is closed + if (self.bCurrentSwitchState == true) + { // If window was opened, start Timeout and go to state opened + self.iTimeoutId = setInterval(self.RunStatemachine.bind(self), self.iFirstRemind * 1000, true); + self.eState = ESTATES.OPENED; + } + break; + + case ESTATES.OPENED: + // The window is opened + if (self.bCurrentSwitchState == false) + { // If window was closed before Timeout, go back to closed (Timeout stopped above) + self.eState = ESTATES.CLOSED; + } + else if (bTimeout == true) + { // Time is over. Send a remind, start another timer for reminding again and go to remind state + self.UpdateCurrentSensorState(true); + self.iTimeoutId = setInterval(self.RunStatemachine.bind(self), ((self.bFastRemind ? self.iFastCyclicRemind : self.iSlowCyclicRemind) - 1) * 1000, true); + self.eState = ESTATES.REMIND; + } + break; + + case ESTATES.REMIND: + // The reminder was sent, wait for window to be closed or send another reminder + if (self.bCurrentSwitchState == false) + { // If window was closed now, quit the reminder and go back to closed (Timeout stopped above) + self.UpdateCurrentSensorState(false); + self.eState = ESTATES.CLOSED; + } + else if (bTimeout == true) + { // Time is over again and the window still not closed. "Close" virtual window for a second and start timer to reopen it in a second. + // Then go to OPENED to wait for the timer to run out again + self.UpdateCurrentSensorState(false); + self.iTimeoutId = setInterval(self.RunStatemachine.bind(self), 1000, true); + self.eState = ESTATES.OPENED; + } + break; + } + return; + } +} + +//----------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------- + +module.exports = cWindowAndDoorAccessory; diff --git a/config.schema.json b/config.schema.json new file mode 100644 index 0000000..89ba2fa --- /dev/null +++ b/config.schema.json @@ -0,0 +1,91 @@ +{ + "pluginAlias": "HomeSecurityLitePlatform", + "pluginType": "platform", + "schema": { + "type": "object", + "properties": { + "prefix_remind_me": { + "type": "string", + "title": "Prefix for Remind me", + "required": false + }, + "prefix_reminder": { + "type": "string", + "title": "Prefix for Reminder", + "required": false + }, + "prefix_reminder_accessory": { + "type": "string", + "title": "Prefix for the Reminder accessory", + "required": false + }, + "forgot_window_warning_name": { + "type": "string", + "title": "Name of the Forgot Window Warning contact sensor", + "required": false + }, + "home_security_lite_name": { + "type": "string", + "title": "Name of the Home Security Lite accessory", + "required": false + }, + "security_system_name": { + "type": "string", + "title": "Name of the Security system", + "required": false + }, + "fast_cyclic_reminder_name": { + "type": "string", + "title": "Name of the Remind me fast again switch", + "required": false + }, + "first_remind": { + "type": "number", + "title": "First reminder after x seconds", + "required": false + }, + "fast_cyclic_remind": { + "type": "number", + "title": "Remind me again fast after x seconds", + "required": false + }, + "slow_cyclic_remind": { + "type": "number", + "title": "Remind me again slow after x seconds", + "required": false + }, + "windows_and_doors": { + "title": "Window and door contact sensors", + "required": false, + "type": "array", + "items": { + "name": { + "type": "string", + "title": "Name of door or window", + "required": true + }, + "description": { + "type": "string", + "title": "Optional description of door or window", + "required": false + }, + "first_remind": { + "type": "number", + "title": "First reminder after x seconds", + "required": false + }, + "fast_cyclic_remind": { + "type": "number", + "title": "Remind me again fast after x seconds", + "required": false + }, + "slow_cyclic_remind": { + "type": "number", + "title": "Remind me again slow after x seconds", + "required": false + } + } + } + } + } +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..b3e3f12 --- /dev/null +++ b/index.js @@ -0,0 +1,67 @@ +// Main file for plugin +// +//----------------------------------------------------------------------- +// Date Author Change +//----------------------------------------------------------------------- +// 15.05.2020 D.Steidl Created +// 22.05.2020 D.Steidl JSDoc added +//----------------------------------------------------------------------- + +//----------------------------------------------------------------------- +// Global variables +//----------------------------------------------------------------------- + +// variables have to be declared explicitly +'use strict' + +/** @type {Object} Pointer to Homebridge.platformAccessory */ +var cAccessory; +/** @type {Object} Pointer to Homebridge.hap.Service */ +var cService; +/** @type {Object} Pointer to Homebridge.hap.Characteristic */ +var cCharacteristic; +/** @type {Object} Pointer to Homebridge.hap.uuid */ +var cUUIDGen; +/** @type {String} FW-Version of the plugin (shown in Homekit) */ +var strFWVersion; +//----------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------- + +// from JavaScript + +// from HomeSecurityLite +const cHomeSecurityLitePlatform = require('./HomeSecurityLitePlatform') +const packageJson = require('./package.json') + +//----------------------------------------------------------------------- +// Classes +//----------------------------------------------------------------------- + +//----------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------- + +/** + * Anonymous funtion called by homebridge + * + * @param {Object} cHomebridge Pointer to homebridge object + * @returns {void} nothing + */ +module.exports = function (cHomebridge) +{ + global.strFWVersion = packageJson.version; + console.log("Homebridge API version: " + cHomebridge.version + " HomeSecurityLite V" + global.strFWVersion); + + // Accessory must be created from PlatformAccessory Constructor + global.cAccessory = cHomebridge.platformAccessory; + + // Service and Characteristic are from hap-nodejs + global.cService = cHomebridge.hap.Service; + global.cCharacteristic = cHomebridge.hap.Characteristic; + global.cUUIDGen = cHomebridge.hap.uuid; + + // For platform plugin to be considered as dynamic platform plugin, + // registerPlatform(pluginName, platformName, constructor, dynamic), dynamic must be true + cHomebridge.registerPlatform(packageJson.name, "HomeSecurityLitePlatform", cHomeSecurityLitePlatform, true); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..59824e6 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "homebridge-home-security-lite", + "version": "0.3.0", + "description": "A homebridge-plugin for using your available homekit sensors and actors to build a security system", + "displayName": "Homebridge Home Security Lite", + "author": "D. Steidl", + "main": "index.js", + "scripts": { + "test": "./node_modules/homebridge/bin/homebridge -P ./ -I -U ./.homebridge" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SteidlD/homebridge-home-security-lite.git" + }, + "keywords": [ + "homebridge-plugin", + "security system", + "alarm", + "contact sensor", + "homebridge" + ], + "license": "gpl-3.0", + "bugs": { + "url": "https://github.com/SteidlD/homebridge-home-security-lite/issues" + }, + "homepage": "https://github.com/SteidlD/homebridge-home-security-lite#readme", + "engines": { + "homebridge": ">=1.0.1", + "node": ">=12.16.2" + } +}